mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 2038: feat(shared,crm): add Code 128 barcode generation library
feat(shared,crm): add Code 128 barcode generation library Implements new @isa/shared/barcode library with directive and component for generating Code 128 barcodes using JsBarcode. Features: - Standalone Angular directive (svg[sharedBarcode]) - Standalone Angular component (<shared-barcode>) - Signal-based reactive inputs - SVG-based vector rendering - Customizable colors, size, margins, fonts - Comprehensive Vitest test coverage (39 tests) - Storybook stories for both directive and component - Integrated into customer loyalty card component Changes: - Created @isa/shared/barcode library with directive and component - Added JsBarcode dependency (v3.12.1) - Integrated barcode into customer loyalty card display - Added Storybook stories for interactive documentation - Fixed ui-switch story component reference - Updated library reference documentation Refs #5496 Related work items: #5496
This commit is contained in:
committed by
Nino Righi
parent
5f1d3a2c7b
commit
7a6a2dc49d
376
libs/shared/barcode/README.md
Normal file
376
libs/shared/barcode/README.md
Normal file
@@ -0,0 +1,376 @@
|
||||
# @isa/shared/barcode
|
||||
|
||||
Angular library for generating Code 128 barcodes using [JsBarcode](https://github.com/lindell/JsBarcode).
|
||||
|
||||
Provides both a **component** (easier to use) and a **directive** (more flexible) for rendering barcodes.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Standalone Angular component & directive
|
||||
- ✅ Code 128 barcode format support
|
||||
- ✅ Signal-based reactive inputs
|
||||
- ✅ SVG-based rendering (vector graphics)
|
||||
- ✅ Highly customizable (colors, size, margins, fonts)
|
||||
- ✅ Automatic re-rendering on input changes
|
||||
- ✅ Built-in logging with `@isa/core/logging`
|
||||
- ✅ Comprehensive Vitest test coverage
|
||||
|
||||
## Installation
|
||||
|
||||
### Component (Recommended)
|
||||
|
||||
The component provides the easiest way to add barcodes:
|
||||
|
||||
```typescript
|
||||
import { BarcodeComponent } from '@isa/shared/barcode';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-component',
|
||||
standalone: true,
|
||||
imports: [BarcodeComponent],
|
||||
template: `
|
||||
<shared-barcode [value]="productEan" />
|
||||
`,
|
||||
})
|
||||
export class MyComponent {
|
||||
productEan = '9783161484100';
|
||||
}
|
||||
```
|
||||
|
||||
### Directive (Advanced)
|
||||
|
||||
Use the directive when you need to apply the barcode to an existing SVG element:
|
||||
|
||||
```typescript
|
||||
import { BarcodeDirective } from '@isa/shared/barcode';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-component',
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<svg sharedBarcode [value]="productEan"></svg>
|
||||
`,
|
||||
})
|
||||
export class MyComponent {
|
||||
productEan = '9783161484100';
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Component - Minimal Example
|
||||
|
||||
```html
|
||||
<shared-barcode [value]="'123456789'" />
|
||||
```
|
||||
|
||||
### Directive - Minimal Example
|
||||
|
||||
```html
|
||||
<svg sharedBarcode [value]="'123456789'"></svg>
|
||||
```
|
||||
|
||||
### Component - With Custom Options
|
||||
|
||||
```html
|
||||
<shared-barcode
|
||||
[value]="barcodeValue"
|
||||
[width]="3"
|
||||
[height]="150"
|
||||
[displayValue]="true"
|
||||
[lineColor]="'#000000'"
|
||||
[background]="'#ffffff'"
|
||||
/>
|
||||
```
|
||||
|
||||
### Directive - With Custom Options
|
||||
|
||||
```html
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="barcodeValue"
|
||||
[width]="3"
|
||||
[height]="150"
|
||||
[displayValue]="true"
|
||||
[lineColor]="'#000000'"
|
||||
[background]="'#ffffff'"
|
||||
></svg>
|
||||
```
|
||||
|
||||
### TypeScript Example
|
||||
|
||||
```typescript
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { BarcodeComponent } from '@isa/shared/barcode';
|
||||
|
||||
@Component({
|
||||
selector: 'app-product-label',
|
||||
standalone: true,
|
||||
imports: [BarcodeComponent],
|
||||
template: `
|
||||
<div class="label">
|
||||
<h3>Product Label</h3>
|
||||
<shared-barcode
|
||||
[value]="ean()"
|
||||
[width]="2"
|
||||
[height]="100"
|
||||
[displayValue]="true"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class ProductLabelComponent {
|
||||
ean = signal('4006381333634');
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Inputs
|
||||
|
||||
| Input | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `value` | `string` | *(required)* | The barcode value to encode |
|
||||
| `format` | `string` | `'CODE128'` | Barcode format (supports CODE128, CODE39, EAN13, etc.) |
|
||||
| `width` | `number` | `2` | Width of a single bar in pixels |
|
||||
| `height` | `number` | `100` | Height of the barcode in pixels |
|
||||
| `displayValue` | `boolean` | `true` | Whether to display the human-readable text below the barcode |
|
||||
| `lineColor` | `string` | `'#000000'` | Color of the barcode bars and text |
|
||||
| `background` | `string` | `'#ffffff'` | Background color |
|
||||
| `fontSize` | `number` | `20` | Font size for the human-readable text |
|
||||
| `margin` | `number` | `10` | Margin around the barcode in pixels |
|
||||
|
||||
### Selector
|
||||
|
||||
```typescript
|
||||
selector: 'svg[sharedBarcode]'
|
||||
```
|
||||
|
||||
The directive must be applied to an `<svg>` element.
|
||||
|
||||
## Examples
|
||||
|
||||
### Component Examples
|
||||
|
||||
#### Large Barcode
|
||||
|
||||
```html
|
||||
<shared-barcode
|
||||
[value]="'9876543210'"
|
||||
[width]="4"
|
||||
[height]="200"
|
||||
[fontSize]="28"
|
||||
/>
|
||||
```
|
||||
|
||||
#### Compact Barcode (No Text)
|
||||
|
||||
```html
|
||||
<shared-barcode
|
||||
[value]="'123456'"
|
||||
[width]="1"
|
||||
[height]="60"
|
||||
[displayValue]="false"
|
||||
[margin]="5"
|
||||
/>
|
||||
```
|
||||
|
||||
#### Colored Barcode
|
||||
|
||||
```html
|
||||
<shared-barcode
|
||||
[value]="'PRODUCT001'"
|
||||
[lineColor]="'#0066CC'"
|
||||
[background]="'#F0F0F0'"
|
||||
/>
|
||||
```
|
||||
|
||||
### Directive Examples
|
||||
|
||||
#### Large Barcode
|
||||
|
||||
```html
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="'9876543210'"
|
||||
[width]="4"
|
||||
[height]="200"
|
||||
[fontSize]="28"
|
||||
></svg>
|
||||
```
|
||||
|
||||
#### Compact Barcode (No Text)
|
||||
|
||||
```html
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="'123456'"
|
||||
[width]="1"
|
||||
[height]="60"
|
||||
[displayValue]="false"
|
||||
[margin]="5"
|
||||
></svg>
|
||||
```
|
||||
|
||||
#### Colored Barcode
|
||||
|
||||
```html
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="'PRODUCT001'"
|
||||
[lineColor]="'#0066CC'"
|
||||
[background]="'#F0F0F0'"
|
||||
></svg>
|
||||
```
|
||||
|
||||
### Reactive Value Updates
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-barcode-scanner',
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<input [(ngModel)]="scannedValue" placeholder="Scan barcode..." />
|
||||
<svg sharedBarcode [value]="scannedValue"></svg>
|
||||
`,
|
||||
})
|
||||
export class BarcodeScannerComponent {
|
||||
scannedValue = '';
|
||||
}
|
||||
```
|
||||
|
||||
The barcode automatically re-renders when the value changes.
|
||||
|
||||
### Multiple Barcodes
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-product-list',
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective, CommonModule],
|
||||
template: `
|
||||
@for (product of products; track product.id) {
|
||||
<div class="product-card">
|
||||
<h4>{{ product.name }}</h4>
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="product.ean"
|
||||
[width]="2"
|
||||
[height]="80"
|
||||
></svg>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class ProductListComponent {
|
||||
products = [
|
||||
{ id: 1, name: 'Product A', ean: '1234567890123' },
|
||||
{ id: 2, name: 'Product B', ean: '9876543210987' },
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The directive renders an SVG element. You can apply CSS styles to the parent SVG element:
|
||||
|
||||
```css
|
||||
svg[sharedBarcode] {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The directive includes built-in error handling:
|
||||
|
||||
- **Invalid values**: Logged as warnings (check browser console with logging enabled)
|
||||
- **Empty values**: Handled gracefully without crashing
|
||||
- **Rendering errors**: Caught and logged with `@isa/core/logging`
|
||||
|
||||
## Testing
|
||||
|
||||
The library uses **Vitest** with **Angular Testing Utilities** for testing.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
npx nx test shared-barcode
|
||||
|
||||
# Run tests with coverage
|
||||
npx nx test shared-barcode --coverage.enabled=true
|
||||
|
||||
# Skip cache for fresh test run
|
||||
npx nx test shared-barcode --skip-nx-cache
|
||||
```
|
||||
|
||||
### Test Reports
|
||||
|
||||
- **JUnit XML**: `testresults/junit-shared-barcode.xml`
|
||||
- **Cobertura Coverage**: `coverage/libs/shared/barcode/cobertura-coverage.xml`
|
||||
|
||||
### Example Test
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BarcodeDirective } from '@isa/shared/barcode';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: '<svg sharedBarcode [value]="value"></svg>',
|
||||
})
|
||||
class TestComponent {
|
||||
value = '123456789';
|
||||
}
|
||||
|
||||
describe('BarcodeDirective', () => {
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TestComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render barcode', () => {
|
||||
const svg = fixture.nativeElement.querySelector('svg');
|
||||
expect(svg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
Supports all modern browsers with SVG rendering capabilities:
|
||||
- Chrome/Edge (latest)
|
||||
- Firefox (latest)
|
||||
- Safari (latest)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **JsBarcode**: `^3.12.1` - Core barcode generation library
|
||||
- **@types/jsbarcode**: TypeScript type definitions
|
||||
|
||||
## Related Libraries
|
||||
|
||||
- `@isa/shared/scanner`: Barcode scanning functionality
|
||||
- `@isa/shared/product-image`: Product image display with CDN support
|
||||
|
||||
## License
|
||||
|
||||
Internal ISA-Frontend library. Not for external distribution.
|
||||
|
||||
## Support
|
||||
|
||||
For issues or feature requests, consult the ISA-Frontend development team.
|
||||
34
libs/shared/barcode/eslint.config.cjs
Normal file
34
libs/shared/barcode/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'shared',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'shared',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/shared/barcode/project.json
Normal file
20
libs/shared/barcode/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "shared-barcode",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/barcode/src",
|
||||
"prefix": "shared",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/shared/barcode"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
libs/shared/barcode/src/index.ts
Normal file
2
libs/shared/barcode/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/barcode.directive';
|
||||
export * from './lib/barcode.component';
|
||||
213
libs/shared/barcode/src/lib/barcode.component.spec.ts
Normal file
213
libs/shared/barcode/src/lib/barcode.component.spec.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { BarcodeComponent } from './barcode.component';
|
||||
|
||||
// Mock JsBarcode
|
||||
vi.mock('jsbarcode', () => ({
|
||||
default: vi.fn((element, value, options) => {
|
||||
// Simulate JsBarcode by adding a rect element to the SVG
|
||||
if (element && element.tagName === 'svg') {
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('data-value', value);
|
||||
rect.setAttribute('data-format', options?.format || 'CODE128');
|
||||
element.appendChild(rect);
|
||||
|
||||
// Call valid callback if provided
|
||||
if (options?.valid) {
|
||||
const isValid = value && value.length > 0;
|
||||
options.valid(isValid);
|
||||
}
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('BarcodeComponent', () => {
|
||||
let component: BarcodeComponent;
|
||||
let fixture: ComponentFixture<BarcodeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BarcodeComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BarcodeComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should create', () => {
|
||||
fixture.componentRef.setInput('value', '123456789');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render an SVG element', () => {
|
||||
fixture.componentRef.setInput('value', '123456789');
|
||||
fixture.detectChanges();
|
||||
|
||||
const svg = fixture.nativeElement.querySelector('svg');
|
||||
expect(svg).toBeTruthy();
|
||||
expect(svg.tagName).toBe('svg');
|
||||
});
|
||||
|
||||
it('should apply the barcode directive to the SVG', () => {
|
||||
fixture.componentRef.setInput('value', '123456789');
|
||||
fixture.detectChanges();
|
||||
|
||||
const svg = fixture.nativeElement.querySelector('svg[sharedBarcode]');
|
||||
expect(svg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Bindings', () => {
|
||||
it('should pass value input to directive', () => {
|
||||
fixture.componentRef.setInput('value', 'TEST123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.value()).toBe('TEST123');
|
||||
});
|
||||
|
||||
it('should pass format input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('format', 'CODE39');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.format()).toBe('CODE39');
|
||||
});
|
||||
|
||||
it('should pass width input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('width', 4);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.width()).toBe(4);
|
||||
});
|
||||
|
||||
it('should pass height input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('height', 200);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.height()).toBe(200);
|
||||
});
|
||||
|
||||
it('should pass displayValue input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('displayValue', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayValue()).toBe(false);
|
||||
});
|
||||
|
||||
it('should pass lineColor input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('lineColor', '#FF0000');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lineColor()).toBe('#FF0000');
|
||||
});
|
||||
|
||||
it('should pass background input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('background', '#F0F0F0');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.background()).toBe('#F0F0F0');
|
||||
});
|
||||
|
||||
it('should pass fontSize input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('fontSize', 24);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.fontSize()).toBe(24);
|
||||
});
|
||||
|
||||
it('should pass margin input to directive', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.componentRef.setInput('margin', 15);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.margin()).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default Values', () => {
|
||||
it('should use default format CODE128', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.format()).toBe('CODE128');
|
||||
});
|
||||
|
||||
it('should use default width 2', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.width()).toBe(2);
|
||||
});
|
||||
|
||||
it('should use default height 100', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.height()).toBe(100);
|
||||
});
|
||||
|
||||
it('should use default displayValue true', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.displayValue()).toBe(true);
|
||||
});
|
||||
|
||||
it('should use default lineColor #000000', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.lineColor()).toBe('#000000');
|
||||
});
|
||||
|
||||
it('should use default background #ffffff', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.background()).toBe('#ffffff');
|
||||
});
|
||||
|
||||
it('should use default fontSize 20', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.fontSize()).toBe(20);
|
||||
});
|
||||
|
||||
it('should use default margin 10', () => {
|
||||
fixture.componentRef.setInput('value', '123');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.margin()).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Barcode Rendering', () => {
|
||||
it('should render barcode with custom value', () => {
|
||||
fixture.componentRef.setInput('value', '987654321');
|
||||
fixture.detectChanges();
|
||||
|
||||
const svg = fixture.nativeElement.querySelector('svg');
|
||||
expect(svg.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should update when value changes', () => {
|
||||
fixture.componentRef.setInput('value', 'INITIAL');
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentRef.setInput('value', 'UPDATED');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.value()).toBe('UPDATED');
|
||||
});
|
||||
});
|
||||
});
|
||||
78
libs/shared/barcode/src/lib/barcode.component.ts
Normal file
78
libs/shared/barcode/src/lib/barcode.component.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { BarcodeDirective } from './barcode.directive';
|
||||
|
||||
/**
|
||||
* Component wrapper for the barcode directive that provides an easier-to-use API.
|
||||
* Renders a Code 128 barcode as an SVG element.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <shared-barcode [value]="'123456789'" [width]="2" [height]="100" />
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'shared-barcode',
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="value()"
|
||||
[format]="format()"
|
||||
[width]="width()"
|
||||
[height]="height()"
|
||||
[displayValue]="displayValue()"
|
||||
[lineColor]="lineColor()"
|
||||
[background]="background()"
|
||||
[fontSize]="fontSize()"
|
||||
[margin]="margin()"
|
||||
></svg>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BarcodeComponent {
|
||||
/**
|
||||
* The barcode value to encode (required)
|
||||
*/
|
||||
value = input.required<string>();
|
||||
|
||||
/**
|
||||
* Barcode format (default: CODE128)
|
||||
*/
|
||||
format = input<string>('CODE128');
|
||||
|
||||
/**
|
||||
* Width of a single bar in pixels (default: 2)
|
||||
*/
|
||||
width = input<number>(2);
|
||||
|
||||
/**
|
||||
* Height of the barcode in pixels (default: 100)
|
||||
*/
|
||||
height = input<number>(100);
|
||||
|
||||
/**
|
||||
* Whether to display the human-readable value below the barcode (default: true)
|
||||
*/
|
||||
displayValue = input<boolean>(true);
|
||||
|
||||
/**
|
||||
* Color of the barcode bars and text (default: #000000)
|
||||
*/
|
||||
lineColor = input<string>('#000000');
|
||||
|
||||
/**
|
||||
* Background color (default: #ffffff)
|
||||
*/
|
||||
background = input<string>('#ffffff');
|
||||
|
||||
/**
|
||||
* Font size for the human-readable text (default: 20)
|
||||
*/
|
||||
fontSize = input<number>(20);
|
||||
|
||||
/**
|
||||
* Margin around the barcode in pixels (default: 10)
|
||||
*/
|
||||
margin = input<number>(10);
|
||||
}
|
||||
243
libs/shared/barcode/src/lib/barcode.directive.spec.ts
Normal file
243
libs/shared/barcode/src/lib/barcode.directive.spec.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { BarcodeDirective } from './barcode.directive';
|
||||
|
||||
// Mock JsBarcode
|
||||
vi.mock('jsbarcode', () => ({
|
||||
default: vi.fn((element, value, options) => {
|
||||
// Simulate JsBarcode by adding a rect element to the SVG
|
||||
if (element && element.tagName === 'svg') {
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('data-value', value);
|
||||
rect.setAttribute('data-format', options?.format || 'CODE128');
|
||||
element.appendChild(rect);
|
||||
|
||||
// Call valid callback if provided
|
||||
if (options?.valid) {
|
||||
const isValid = value && value.length > 0;
|
||||
options.valid(isValid);
|
||||
}
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="barcodeValue"
|
||||
[format]="format"
|
||||
[width]="width"
|
||||
[height]="height"
|
||||
[displayValue]="displayValue"
|
||||
></svg>
|
||||
`,
|
||||
})
|
||||
class TestHostComponent {
|
||||
barcodeValue = '123456789';
|
||||
format = 'CODE128';
|
||||
width = 2;
|
||||
height = 100;
|
||||
displayValue = true;
|
||||
}
|
||||
|
||||
describe('BarcodeDirective', () => {
|
||||
let fixture: ComponentFixture<TestHostComponent>;
|
||||
let component: TestHostComponent;
|
||||
let svgElement: SVGElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TestHostComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestHostComponent);
|
||||
component = fixture.componentInstance;
|
||||
svgElement = fixture.nativeElement.querySelector('svg');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should create the directive', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should apply to SVG element', () => {
|
||||
expect(svgElement).toBeTruthy();
|
||||
expect(svgElement.tagName).toBe('svg');
|
||||
});
|
||||
|
||||
it('should render barcode on initialization', () => {
|
||||
// JsBarcode adds child elements to the SVG
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Barcode Rendering', () => {
|
||||
it('should render barcode with default CODE128 format', () => {
|
||||
const rect = svgElement.querySelector('rect');
|
||||
expect(rect).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render barcode with custom value', () => {
|
||||
component.barcodeValue = '987654321';
|
||||
fixture.detectChanges();
|
||||
|
||||
// SVG should be updated with new value
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should update barcode when value changes', () => {
|
||||
component.barcodeValue = 'NEWVALUE123';
|
||||
fixture.detectChanges();
|
||||
|
||||
// Barcode should be re-rendered (may have different number of bars)
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render with custom width', () => {
|
||||
component.width = 4;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render with custom height', () => {
|
||||
component.height = 200;
|
||||
fixture.detectChanges();
|
||||
|
||||
const rect = svgElement.querySelector('rect');
|
||||
expect(rect).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render without display value when disabled', () => {
|
||||
component.displayValue = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Check that SVG is still rendered
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Changes', () => {
|
||||
it('should re-render when format changes', () => {
|
||||
component.format = 'CODE128';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle multiple rapid value changes', () => {
|
||||
component.barcodeValue = 'VALUE1';
|
||||
fixture.detectChanges();
|
||||
|
||||
component.barcodeValue = 'VALUE2';
|
||||
fixture.detectChanges();
|
||||
|
||||
component.barcodeValue = 'VALUE3';
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle empty value gracefully', () => {
|
||||
// JsBarcode should handle this or log warning
|
||||
component.barcodeValue = '';
|
||||
fixture.detectChanges();
|
||||
|
||||
// Directive should not crash
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle very long values', () => {
|
||||
component.barcodeValue = '1'.repeat(100);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(svgElement.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Customization Options', () => {
|
||||
it('should render with custom line color', () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="'123456'"
|
||||
[lineColor]="'#FF0000'"
|
||||
></svg>
|
||||
`,
|
||||
})
|
||||
class ColorTestComponent {}
|
||||
|
||||
const colorFixture = TestBed.createComponent(ColorTestComponent);
|
||||
colorFixture.detectChanges();
|
||||
|
||||
const svg = colorFixture.nativeElement.querySelector('svg');
|
||||
expect(svg).toBeTruthy();
|
||||
expect(svg.children.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should render with custom background', () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<svg
|
||||
sharedBarcode
|
||||
[value]="'123456'"
|
||||
[background]="'#F0F0F0'"
|
||||
></svg>
|
||||
`,
|
||||
})
|
||||
class BackgroundTestComponent {}
|
||||
|
||||
const bgFixture = TestBed.createComponent(BackgroundTestComponent);
|
||||
bgFixture.detectChanges();
|
||||
|
||||
const svg = bgFixture.nativeElement.querySelector('svg');
|
||||
expect(svg).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with custom font size', () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<svg sharedBarcode [value]="'123456'" [fontSize]="30"></svg>
|
||||
`,
|
||||
})
|
||||
class FontSizeTestComponent {}
|
||||
|
||||
const fontFixture = TestBed.createComponent(FontSizeTestComponent);
|
||||
fontFixture.detectChanges();
|
||||
|
||||
const svg = fontFixture.nativeElement.querySelector('svg');
|
||||
expect(svg).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with custom margin', () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [BarcodeDirective],
|
||||
template: `
|
||||
<svg sharedBarcode [value]="'123456'" [margin]="20"></svg>
|
||||
`,
|
||||
})
|
||||
class MarginTestComponent {}
|
||||
|
||||
const marginFixture = TestBed.createComponent(MarginTestComponent);
|
||||
marginFixture.detectChanges();
|
||||
|
||||
const svg = marginFixture.nativeElement.querySelector('svg');
|
||||
expect(svg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
125
libs/shared/barcode/src/lib/barcode.directive.ts
Normal file
125
libs/shared/barcode/src/lib/barcode.directive.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
type OnDestroy,
|
||||
} from '@angular/core';
|
||||
import JsBarcode from 'jsbarcode';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
/**
|
||||
* Directive for rendering Code 128 barcodes on SVG elements using JsBarcode.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <svg sharedBarcode [value]="'123456789'" [width]="2" [height]="100"></svg>
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'svg[sharedBarcode]',
|
||||
standalone: true,
|
||||
})
|
||||
export class BarcodeDirective implements OnDestroy {
|
||||
readonly #elementRef = inject(ElementRef<SVGElement>);
|
||||
readonly #logger = logger({ directive: 'BarcodeDirective' });
|
||||
|
||||
/**
|
||||
* The barcode value to encode (required)
|
||||
*/
|
||||
value = input.required<string>();
|
||||
|
||||
/**
|
||||
* Barcode format (default: CODE128)
|
||||
*/
|
||||
format = input<string>('CODE128');
|
||||
|
||||
/**
|
||||
* Width of a single bar in pixels (default: 2)
|
||||
*/
|
||||
width = input<number>(2);
|
||||
|
||||
/**
|
||||
* Height of the barcode in pixels (default: 100)
|
||||
*/
|
||||
height = input<number>(100);
|
||||
|
||||
/**
|
||||
* Whether to display the human-readable value below the barcode (default: true)
|
||||
*/
|
||||
displayValue = input<boolean>(true);
|
||||
|
||||
/**
|
||||
* Color of the barcode bars and text (default: #000000)
|
||||
*/
|
||||
lineColor = input<string>('#000000');
|
||||
|
||||
/**
|
||||
* Background color (default: #ffffff)
|
||||
*/
|
||||
background = input<string>('#ffffff');
|
||||
|
||||
/**
|
||||
* Font size for the human-readable text (default: 20)
|
||||
*/
|
||||
fontSize = input<number>(20);
|
||||
|
||||
/**
|
||||
* Margin around the barcode in pixels (default: 10)
|
||||
*/
|
||||
margin = input<number>(10);
|
||||
|
||||
constructor() {
|
||||
// Re-render barcode whenever any input changes
|
||||
effect(() => {
|
||||
this.#renderBarcode();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.#logger.debug('Directive destroyed', () => ({
|
||||
value: this.value(),
|
||||
}));
|
||||
}
|
||||
|
||||
#renderBarcode(): void {
|
||||
const element = this.#elementRef.nativeElement;
|
||||
const value = this.value();
|
||||
|
||||
try {
|
||||
JsBarcode(element, value, {
|
||||
format: this.format(),
|
||||
width: this.width(),
|
||||
height: this.height(),
|
||||
displayValue: this.displayValue(),
|
||||
lineColor: this.lineColor(),
|
||||
background: this.background(),
|
||||
fontSize: this.fontSize(),
|
||||
margin: this.margin(),
|
||||
valid: (isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.#logger.warn('Invalid barcode data', () => ({
|
||||
value,
|
||||
format: this.format(),
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.#logger.debug('Barcode rendered', () => ({
|
||||
value,
|
||||
format: this.format(),
|
||||
}));
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to render barcode',
|
||||
error as Error,
|
||||
() => ({
|
||||
value,
|
||||
format: this.format(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
libs/shared/barcode/src/test-setup.ts
Normal file
13
libs/shared/barcode/src/test-setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
30
libs/shared/barcode/tsconfig.json
Normal file
30
libs/shared/barcode/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/shared/barcode/tsconfig.lib.json
Normal file
27
libs/shared/barcode/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/shared/barcode/tsconfig.spec.json
Normal file
29
libs/shared/barcode/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
29
libs/shared/barcode/vite.config.mts
Normal file
29
libs/shared/barcode/vite.config.mts
Normal file
@@ -0,0 +1,29 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default
|
||||
// @ts-expect-error - Vitest reporter tuple types have complex inference issues
|
||||
defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/shared/barcode',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: [
|
||||
'default',
|
||||
['junit', { outputFile: '../../../testresults/junit-shared-barcode.xml' }],
|
||||
],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/shared/barcode',
|
||||
provider: 'v8' as const,
|
||||
reporter: ['text', 'cobertura'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user