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:
Lorenz Hilpert
2025-11-21 13:42:32 +00:00
committed by Nino Righi
parent 5f1d3a2c7b
commit 7a6a2dc49d
23 changed files with 2184 additions and 340 deletions

View File

@@ -22,19 +22,19 @@
</div>
</div>
<!-- Barcode placeholder -->
<div
class="h-[5.35156rem] w-[12.3125rem] aspect-[197.00/85.63]"
<!-- Barcode -->
<shared-barcode
[value]="card().code"
[displayValue]="false"
[height]="barcodeHeight"
[width]="barcodeWidth"
[margin]="barcodeMargin"
[format]="'CODE128'"
[background]="'#ffffff'"
[attr.data-what]="'card-barcode'"
[attr.data-which]="card().code"
>
<!-- TODO: Replace with actual barcode component -->
<div
class="isa-text-caption-regular flex h-full items-center justify-center bg-isa-white text-isa-neutral-500 rounded-[0.25rem]"
>
Barcode: {{ card().code }}
</div>
</div>
class="rounded-[0.25rem] overflow-hidden"
/>
</div>
<!-- White footer section: customer name -->

View File

@@ -14,8 +14,7 @@ describe('CustomerCardComponent', () => {
isActive: true,
isPrimary: true,
totalPoints: 1500,
cardNumber: '1234-5678-9012-3456',
} as BonusCardInfo;
};
beforeEach(() => {
TestBed.configureTestingModule({
@@ -84,7 +83,7 @@ describe('CustomerCardComponent', () => {
fixture.componentRef.setInput('card', mockCard);
fixture.detectChanges();
expect(component.card().cardNumber).toBe('1234-5678-9012-3456');
expect(component.card().code).toBe('CARD-1');
});
});
});

View File

@@ -1,10 +1,6 @@
import {
ChangeDetectionStrategy,
Component,
input,
output,
} from '@angular/core';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { BonusCardInfo } from '@isa/crm/data-access';
import { BarcodeComponent } from '@isa/shared/barcode';
import { LockCustomerCardComponent } from '../lock-customer-card/lock-customer-card.component';
/**
@@ -31,11 +27,19 @@ import { LockCustomerCardComponent } from '../lock-customer-card/lock-customer-c
templateUrl: './customer-card.component.html',
styleUrl: './customer-card.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LockCustomerCardComponent],
imports: [BarcodeComponent, LockCustomerCardComponent],
})
export class CustomerCardComponent {
/**
* Bonus card data to display.
*/
readonly card = input.required<BonusCardInfo>();
/**
* Barcode dimensions in rem, converted to pixels for JsBarcode.
* Base: 1rem = 16px
*/
protected readonly barcodeHeight = 4.5 * 16; // 4.5rem = 72px
protected readonly barcodeWidth = 0.125 * 16; // 0.125rem = 2px
protected readonly barcodeMargin = 0.5 * 16; // 0.5rem = 8px
}

View 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.

View 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: {},
},
];

View 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"
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './lib/barcode.directive';
export * from './lib/barcode.component';

View 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');
});
});
});

View 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);
}

View 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();
});
});
});

View 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(),
})
);
}
}
}

View 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(),
);

View 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"
}
]
}

View 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"]
}

View 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"]
}

View 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'],
},
},
}));