# @isa/utils/ean-validation Lightweight Angular utility library for validating EAN (European Article Number) barcodes with reactive forms integration and standalone validation functions. ## Overview The EAN Validation library provides tools for validating 13-digit EAN barcodes in Angular applications. It includes both an Angular Forms validator for reactive form validation and a standalone utility function for programmatic validation. The library uses a simple regex-based approach for fast, efficient validation without external dependencies. ## Table of Contents - [Features](#features) - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [API Reference](#api-reference) - [Usage Examples](#usage-examples) - [EAN Format](#ean-format) - [Architecture Notes](#architecture-notes) - [Testing](#testing) - [Dependencies](#dependencies) ## Features - **Angular Forms validator** - `eanValidator` for reactive form controls - **Standalone validation** - `isEan()` utility function for programmatic checks - **Regex-based validation** - Fast, efficient 13-digit EAN validation - **Zero dependencies** - No external libraries required - **Type-safe** - Full TypeScript support - **Null-safe** - Graceful handling of undefined/null values - **Lightweight** - Minimal bundle size impact - **Framework agnostic core** - Validation logic can be used outside Angular ## Quick Start ### 1. Reactive Form Validation ```typescript import { Component } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { eanValidator } from '@isa/utils/ean-validation'; @Component({ selector: 'app-product-form', standalone: true, imports: [ReactiveFormsModule], template: `
@if (eanControl.errors?.['invalidEan']) {
Please enter a valid 13-digit EAN
}
` }) export class ProductFormComponent { productForm = new FormGroup({ ean: new FormControl('', [eanValidator]) }); get eanControl() { return this.productForm.get('ean')!; } } ``` ### 2. Programmatic Validation ```typescript import { Component } from '@angular/core'; import { isEan } from '@isa/utils/ean-validation'; @Component({ selector: 'app-barcode-scanner', template: ` ` }) export class BarcodeScannerComponent { validateBarcode(code: string): void { if (isEan(code)) { console.log('Valid EAN:', code); // Process valid EAN } else { console.error('Invalid EAN:', code); // Handle invalid EAN } } } ``` ### 3. Combined Usage ```typescript import { Component } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { eanValidator, isEan } from '@isa/utils/ean-validation'; @Component({ selector: 'app-product-search', standalone: true, imports: [ReactiveFormsModule], template: ` ` }) export class ProductSearchComponent { searchControl = new FormControl('', [eanValidator]); canSearch(): boolean { const value = this.searchControl.value; return !!value && isEan(value); } search(): void { if (this.canSearch()) { const ean = this.searchControl.value!; console.log('Searching for:', ean); // Perform search } } } ``` ## Core Concepts ### EAN (European Article Number) EAN is a 13-digit barcode standard used internationally for product identification: ``` 1234567890123 └───┬────┘││└─ Check digit │ ││ │ │└── Product code │ └─── Manufacturer code └───────── Country/GS1 prefix ``` ### Validation Rules The library validates EANs based on these criteria: 1. **Length**: Must be exactly 13 digits 2. **Content**: Must contain only numeric characters (0-9) 3. **Format**: No spaces, dashes, or other separators allowed **Valid EANs:** ```typescript '1234567890123' ✓ (13 digits, all numeric) '0000000000000' ✓ (13 digits, all numeric) '9999999999999' ✓ (13 digits, all numeric) ``` **Invalid EANs:** ```typescript '123456789012' ✗ (12 digits - too short) '12345678901234' ✗ (14 digits - too long) '123456789012A' ✗ (contains letter) '1234-5678-9012' ✗ (contains dashes) ' 1234567890123' ✗ (contains whitespace) '' ✗ (empty string - via isEan()) undefined ✗ (undefined - via isEan()) ``` ### Regex Pattern The validation uses a simple regex pattern: ```typescript const EAN_REGEX = /^[0-9]{13}$/; ``` **Pattern breakdown:** - `^` - Start of string - `[0-9]` - Any digit from 0 to 9 - `{13}` - Exactly 13 times - `$` - End of string ## API Reference ### eanValidator Angular Forms validator function for validating EAN input. **Type:** `ValidatorFn` **Signature:** ```typescript eanValidator(control: AbstractControl): ValidationErrors | null ``` **Parameters:** - `control: AbstractControl` - The form control to validate **Returns:** - `null` - If the value is valid or empty/null - `{ invalidEan: true }` - If the value is invalid **Validation Behavior:** - **Empty values**: Returns `null` (considered valid - use `Validators.required` for required fields) - **Valid EAN**: Returns `null` - **Invalid EAN**: Returns `{ invalidEan: true }` **Example:** ```typescript import { FormControl } from '@angular/forms'; import { eanValidator } from '@isa/utils/ean-validation'; const eanControl = new FormControl('', [eanValidator]); eanControl.setValue('1234567890123'); console.log(eanControl.valid); // true eanControl.setValue('123'); console.log(eanControl.errors); // { invalidEan: true } eanControl.setValue(''); console.log(eanControl.valid); // true (empty is valid) ``` ### isEan Standalone utility function for programmatic EAN validation. **Signature:** ```typescript isEan(value: string | undefined): boolean ``` **Parameters:** - `value: string | undefined` - The value to validate **Returns:** - `true` - If the value is a valid 13-digit EAN - `false` - If the value is invalid, undefined, null, or empty **Validation Behavior:** - **undefined/null**: Returns `false` - **Empty string**: Returns `false` - **Valid EAN**: Returns `true` - **Invalid EAN**: Returns `false` **Example:** ```typescript import { isEan } from '@isa/utils/ean-validation'; isEan('1234567890123'); // true isEan('123'); // false isEan(''); // false isEan(undefined); // false isEan(null); // false (TypeScript allows via type coercion) ``` ### EAN_REGEX Regular expression constant for EAN validation. **Type:** `RegExp` **Value:** `/^[0-9]{13}$/` **Note:** This is an internal constant. Prefer using `eanValidator` or `isEan()` for validation. **Example:** ```typescript import { EAN_REGEX } from '@isa/utils/ean-validation'; // Direct regex usage (not recommended) const isValid = EAN_REGEX.test('1234567890123'); console.log(isValid); // true // Prefer using isEan() instead const isValidPreferred = isEan('1234567890123'); ``` ## Usage Examples ### Basic Form Validation ```typescript import { Component } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { eanValidator } from '@isa/utils/ean-validation'; @Component({ selector: 'app-product-form', standalone: true, imports: [ReactiveFormsModule], template: `
@if (ean.touched && ean.errors) {
@if (ean.errors['required']) {

EAN is required

} @if (ean.errors['invalidEan']) {

EAN must be exactly 13 digits

}
}
` }) export class ProductFormComponent { form = new FormGroup({ ean: new FormControl('', [ Validators.required, eanValidator ]) }); get ean() { return this.form.get('ean')!; } onSubmit(): void { if (this.form.valid) { const productData = this.form.value; console.log('Saving product:', productData); // Save logic } } } ``` ### Barcode Scanner Integration ```typescript import { Component, inject, signal } from '@angular/core'; import { isEan } from '@isa/utils/ean-validation'; interface ScanResult { code: string; valid: boolean; timestamp: Date; } @Component({ selector: 'app-barcode-scanner', template: `

Barcode Scanner

Scan History

@for (scan of scanHistory(); track scan.timestamp) {
{{ scan.code }} {{ scan.valid ? '✓ Valid' : '✗ Invalid' }} {{ scan.timestamp | date: 'short' }}
}
` }) export class BarcodeScannerComponent { scanHistory = signal([]); startScanning(): void { // Simulated barcode scan this.processScan('1234567890123'); } processScan(code: string): void { const valid = isEan(code); this.scanHistory.update(history => [ { code, valid, timestamp: new Date() }, ...history ]); if (valid) { console.log('Valid EAN scanned:', code); // Process valid EAN this.lookupProduct(code); } else { console.error('Invalid EAN scanned:', code); // Show error message } } lookupProduct(ean: string): void { // Product lookup logic } } ``` ### Search with Validation ```typescript import { Component, signal } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { eanValidator, isEan } from '@isa/utils/ean-validation'; @Component({ selector: 'app-product-search', standalone: true, imports: [ReactiveFormsModule], template: `
@if (searchControl.touched && searchControl.errors?.['invalidEan']) {

Please enter a valid 13-digit EAN

} @if (searchResult()) {

Product Found

EAN: {{ searchResult()!.ean }}

Name: {{ searchResult()!.name }}

} @if (notFound()) {

No product found with EAN: {{ lastSearch() }}

}
` }) export class ProductSearchComponent { searchControl = new FormControl('', [eanValidator]); searchResult = signal<{ ean: string; name: string } | null>(null); notFound = signal(false); lastSearch = signal(''); isValidEan(): boolean { const value = this.searchControl.value; return !!value && isEan(value); } search(): void { if (!this.isValidEan()) return; const ean = this.searchControl.value!; this.lastSearch.set(ean); // Simulated product lookup const product = this.mockProductLookup(ean); if (product) { this.searchResult.set(product); this.notFound.set(false); } else { this.searchResult.set(null); this.notFound.set(true); } } mockProductLookup(ean: string): { ean: string; name: string } | null { // Mock implementation return { ean, name: `Product ${ean}` }; } } ``` ### Batch Validation ```typescript import { Component, signal } from '@angular/core'; import { isEan } from '@isa/utils/ean-validation'; interface ValidationResult { ean: string; valid: boolean; } @Component({ selector: 'app-batch-validator', template: `

Batch EAN Validator

@if (results().length > 0) {

Validation Results

Valid: {{ validCount() }} / {{ results().length }}

@for (result of results(); track result.ean) { }
EAN Status
{{ result.ean }} {{ result.valid ? '✓ Valid' : '✗ Invalid' }}
}
` }) export class BatchValidatorComponent { inputEans = signal(''); results = signal([]); validCount = signal(0); validate(): void { const eans = this.inputEans() .split('\n') .map(line => line.trim()) .filter(line => line.length > 0); const results: ValidationResult[] = eans.map(ean => ({ ean, valid: isEan(ean) })); this.results.set(results); this.validCount.set(results.filter(r => r.valid).length); } } ``` ### Dynamic Form with Multiple EANs ```typescript import { Component } from '@angular/core'; import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { eanValidator } from '@isa/utils/ean-validation'; @Component({ selector: 'app-multi-ean-form', standalone: true, imports: [ReactiveFormsModule], template: `

Product EANs

@for (control of eanControls.controls; track $index) {
@if (control.touched && control.errors?.['invalidEan']) { Invalid EAN }
}
` }) export class MultiEanFormComponent { form = new FormGroup({ eans: new FormArray([ new FormControl('', [eanValidator]) ]) }); get eanControls() { return this.form.get('eans') as FormArray; } addEan(): void { this.eanControls.push( new FormControl('', [eanValidator]) ); } removeEan(index: number): void { this.eanControls.removeAt(index); } onSubmit(): void { if (this.form.valid) { const eans = this.eanControls.value; console.log('Saving EANs:', eans); } } } ``` ### Type Guard Usage ```typescript import { Component } from '@angular/core'; import { isEan } from '@isa/utils/ean-validation'; interface Product { id: number; name: string; ean?: string; // Optional EAN } @Component({ selector: 'app-product-list', template: `
@for (product of products; track product.id) {

{{ product.name }}

@if (hasValidEan(product)) {

EAN: {{ product.ean }}

} @else {

No valid EAN

}
}
` }) export class ProductListComponent { products: Product[] = [ { id: 1, name: 'Product A', ean: '1234567890123' }, { id: 2, name: 'Product B', ean: '123' }, // Invalid { id: 3, name: 'Product C' }, // Missing ]; hasValidEan(product: Product): boolean { return isEan(product.ean); } } ``` ## EAN Format ### Standard Format **EAN-13 Structure:** ``` ┌──────────────────────────────────┐ │ 1 2 3 4 5 6 7 8 9 0 1 2 3 │ │ └──┬──┘ └───────┬────────┘ │ │ │ │ │ │ │ │ │ │ Prefix Manufacturer Product Check │ │ Code Code Digit │ └──────────────────────────────────┘ ``` ### Common Prefixes | Prefix | Region/Type | |--------|-------------| | 000-019 | GS1 US | | 020-029 | Restricted distribution | | 030-039 | GS1 US | | 040-049 | Restricted distribution | | 050-059 | GS1 US reserved | | 060-139 | GS1 US | | 200-299 | Restricted distribution | | 300-379 | GS1 France | | 380 | GS1 Bulgaria | | 383 | GS1 Slovenia | | 385 | GS1 Croatia | | 400-440 | GS1 Germany | | 450-459, 490-499 | GS1 Japan | | 460-469 | GS1 Russia | | 470 | GS1 Kyrgyzstan | | 471 | GS1 Taiwan | | 474 | GS1 Estonia | | 475 | GS1 Latvia | | 476 | GS1 Azerbaijan | | 477 | GS1 Lithuania | | 478 | GS1 Uzbekistan | | 479 | GS1 Sri Lanka | | 480 | GS1 Philippines | | 481 | GS1 Belarus | | 482 | GS1 Ukraine | | 484 | GS1 Moldova | | 485 | GS1 Armenia | | 486 | GS1 Georgia | | 487 | GS1 Kazakhstan | | 489 | GS1 Hong Kong | | 500-509 | GS1 UK | | 520-521 | GS1 Association Greece | | 528 | GS1 Lebanon | | 529 | GS1 Cyprus | | 530 | GS1 Albania | | 531 | GS1 Macedonia | | 535 | GS1 Malta | | 539 | GS1 Ireland | | 540-549 | GS1 Belgium & Luxembourg | | 560 | GS1 Portugal | | 569 | GS1 Iceland | | 570-579 | GS1 Denmark | | 590 | GS1 Poland | | 594 | GS1 Romania | | 599 | GS1 Hungary | | 600-601 | GS1 South Africa | | 603 | GS1 Ghana | | 604 | GS1 Senegal | | 608 | GS1 Bahrain | | 609 | GS1 Mauritius | | 611 | GS1 Morocco | | 613 | GS1 Algeria | | 615 | GS1 Nigeria | | 616 | GS1 Kenya | | 618 | GS1 Ivory Coast | | 619 | GS1 Tunisia | | 620 | GS1 Tanzania | | 621 | GS1 Syria | | 622 | GS1 Egypt | | 623 | GS1 Brunei | | 624 | GS1 Libya | | 625 | GS1 Jordan | | 626 | GS1 Iran | | 627 | GS1 Kuwait | | 628 | GS1 Saudi Arabia | | 629 | GS1 Emirates | | 640-649 | GS1 Finland | | 690-699 | GS1 China | | 700-709 | GS1 Norway | | 729 | GS1 Israel | | 730-739 | GS1 Sweden | | 740 | GS1 Guatemala | | 741 | GS1 El Salvador | | 742 | GS1 Honduras | | 743 | GS1 Nicaragua | | 744 | GS1 Costa Rica | | 745 | GS1 Panama | | 746 | GS1 Dominican Republic | | 750 | GS1 Mexico | | 754-755 | GS1 Canada | | 759 | GS1 Venezuela | | 760-769 | GS1 Switzerland | | 770-771 | GS1 Colombia | | 773 | GS1 Uruguay | | 775 | GS1 Peru | | 777 | GS1 Bolivia | | 778-779 | GS1 Argentina | | 780 | GS1 Chile | | 784 | GS1 Paraguay | | 786 | GS1 Ecuador | | 789-790 | GS1 Brazil | | 800-839 | GS1 Italy | | 840-849 | GS1 Spain | | 850 | GS1 Cuba | | 858 | GS1 Slovakia | | 859 | GS1 Czech | | 860 | GS1 Serbia | | 865 | GS1 Mongolia | | 867 | GS1 North Korea | | 868-869 | GS1 Turkey | | 870-879 | GS1 Netherlands | | 880 | GS1 South Korea | | 884 | GS1 Cambodia | | 885 | GS1 Thailand | | 888 | GS1 Singapore | | 890 | GS1 India | | 893 | GS1 Vietnam | | 896 | GS1 Pakistan | | 899 | GS1 Indonesia | | 900-919 | GS1 Austria | | 930-939 | GS1 Australia | | 940-949 | GS1 New Zealand | | 950 | GS1 Global Office | | 951 | GS1 Global Office (EPCglobal) | | 955 | GS1 Malaysia | | 958 | GS1 Macau | | 960-969 | GS1 Global Office (GTIN-8) | | 977 | Serial publications (ISSN) | | 978-979 | Bookland (ISBN) | | 980 | Refund receipts | | 981-984 | GS1 coupon identification | | 990-999 | GS1 coupon identification | **Note:** This library validates format only (13 digits), not check digit accuracy or prefix validity. ## Architecture Notes ### Design Patterns #### Validator Function Pattern The `eanValidator` follows Angular's `ValidatorFn` pattern: ```typescript export const eanValidator: ValidatorFn = ( control: AbstractControl ): ValidationErrors | null => { const value = control.value; if (value && !EAN_REGEX.test(value)) { return { invalidEan: true }; } return null; }; ``` **Benefits:** - Composable with other validators - Reusable across multiple form controls - Type-safe with TypeScript - Follows Angular conventions #### Pure Function Pattern The `isEan` function is a pure function with no side effects: ```typescript export const isEan = (value: string | undefined): boolean => { if (!value) { return false; } return EAN_REGEX.test(value); }; ``` **Benefits:** - Predictable - same input always produces same output - Testable - easy to unit test - Framework agnostic - can be used outside Angular - No dependencies - doesn't rely on external state ### Performance Considerations 1. **Regex Performance** - Simple regex pattern for O(n) validation 2. **No External Dependencies** - Minimal bundle size impact 3. **Null-Safe** - Early returns for undefined/null values 4. **Reusable Regex** - Single compiled regex constant ### Future Enhancements Potential improvements identified: 1. **Check Digit Validation** - Validate the EAN-13 check digit algorithm 2. **EAN-8 Support** - Support for 8-digit EAN format 3. **UPC Support** - Support for 12-digit UPC format 4. **Prefix Validation** - Validate GS1 country/region prefixes 5. **Format Conversion** - Convert between EAN-13, EAN-8, and UPC 6. **Barcode Generation** - Generate barcode images from EAN 7. **Async Validation** - Validate against product database ## Testing The library uses **Vitest** with **Angular Testing Utilities** for testing. ### Running Tests ```bash # Run tests for this library npx nx test ean-validation --skip-nx-cache # Run tests with coverage npx nx test ean-validation --code-coverage --skip-nx-cache # Run tests in watch mode npx nx test ean-validation --watch ``` ### Test Structure The library includes comprehensive unit tests covering: - **eanValidator Tests**: - Valid 13-digit EAN returns null - Empty value returns null (valid) - Invalid EAN returns error object - Null/undefined values handled correctly - **isEan Tests**: - Valid 13-digit EAN returns true - Invalid EAN returns false - Empty string returns false - Undefined returns false ### Example Test ```typescript import { describe, it, expect } from 'vitest'; import { FormControl } from '@angular/forms'; import { eanValidator, isEan } from './ean-validation'; describe('eanValidator', () => { it('should return null for valid EAN', () => { // Arrange const control = new FormControl('1234567890123'); // Act const result = eanValidator(control); // Assert expect(result).toBeNull(); }); it('should return null for empty value', () => { // Arrange const control = new FormControl(''); // Act const result = eanValidator(control); // Assert expect(result).toBeNull(); }); it('should return error for invalid EAN', () => { // Arrange const control = new FormControl('123'); // Act const result = eanValidator(control); // Assert expect(result).toEqual({ invalidEan: true }); }); }); describe('isEan', () => { it('should return true for valid EAN', () => { // Arrange const validEan = '1234567890123'; // Act const result = isEan(validEan); // Assert expect(result).toBe(true); }); it('should return false for undefined value', () => { // Arrange const undefinedValue = undefined; // Act const result = isEan(undefinedValue); // Assert expect(result).toBe(false); }); it('should return false for invalid EAN', () => { // Arrange const invalidEan = '123'; // Act const result = isEan(invalidEan); // Assert expect(result).toBe(false); }); }); ``` ## Dependencies ### Required Libraries - `@angular/core` - For TypeScript types - `@angular/forms` - For `ValidatorFn`, `AbstractControl`, `ValidationErrors` types ### Path Alias Import from: `@isa/utils/ean-validation` ### Exports - `eanValidator` - Angular Forms validator function - `isEan` - Standalone validation utility function - `EAN_REGEX` - Regular expression constant (internal use) ## Best Practices ### 1. Use with Validators.required Combine with `Validators.required` for required fields: ```typescript // Good - Required + EAN validation const eanControl = new FormControl('', [ Validators.required, eanValidator ]); // Avoid - Only EAN validation (allows empty) const eanControl = new FormControl('', [eanValidator]); ``` ### 2. Provide Clear Error Messages Show user-friendly error messages: ```typescript // Good - Clear, actionable error message @if (ean.errors?.['invalidEan']) {

Please enter a valid 13-digit EAN (e.g., 1234567890123)

} // Avoid - Generic or unclear message @if (ean.errors?.['invalidEan']) {

Invalid

} ``` ### 3. Use isEan for Type Guards Use `isEan()` for runtime type checking: ```typescript // Good - Type-safe validation function processEan(code: string | undefined): void { if (isEan(code)) { // TypeScript knows code is string here console.log('Processing:', code.toUpperCase()); } } // Avoid - Unsafe type assumption function processEan(code: string): void { // Might throw if code is invalid console.log('Processing:', code.toUpperCase()); } ``` ### 4. Add Maxlength Attribute Prevent users from entering more than 13 digits: ```typescript // Good - Limit input length // Avoid - No length restriction ``` ### 5. Handle Validation in Components Keep validation logic in components, not templates: ```typescript // Good - Logic in component canSubmit(): boolean { return this.form.valid && isEan(this.form.value.ean); } // Avoid - Complex logic in template @if (form.valid && form.value.ean && /^[0-9]{13}$/.test(form.value.ean)) { ... } ``` ## Limitations ### What This Library Does NOT Do 1. **Check Digit Validation** - Does not validate the EAN-13 checksum digit 2. **Database Validation** - Does not verify if EAN exists in product database 3. **Prefix Validation** - Does not validate GS1 country/region prefixes 4. **Format Conversion** - Does not convert between EAN/UPC formats 5. **Barcode Generation** - Does not generate barcode images ### When to Use Additional Validation For production systems, consider: - **Check digit validation** for data integrity - **Database lookup** for product existence - **Prefix validation** for regional compliance - **Duplicate detection** in batch processing ## License Internal ISA Frontend library - not for external distribution.