This commit is contained in:
Nino
2025-11-21 17:10:51 +01:00
5 changed files with 835 additions and 7 deletions

View File

@@ -37,6 +37,7 @@ class CustomerService extends __BaseService {
static readonly CustomerUpdateCustomerPath = '/customer/{customerId}';
static readonly CustomerPatchCustomerPath = '/customer/{customerId}';
static readonly CustomerDeleteCustomerPath = '/customer/{customerId}';
static readonly CustomerMergeLoyaltyAccountsPath = '/customer/{customerId}/loyalty/merge';
static readonly CustomerAddLoyaltyCardPath = '/customer/{customerId}/loyalty/add-card';
static readonly CustomerCreateCustomerPath = '/customer';
static readonly CustomerAddPayerReferencePath = '/customer/{customerId}/payer';
@@ -392,6 +393,56 @@ class CustomerService extends __BaseService {
);
}
/**
* Kundenkarte hinzufügen
* @param params The `CustomerService.CustomerMergeLoyaltyAccountsParams` containing the following parameters:
*
* - `loyaltyCardValues`:
*
* - `customerId`:
*
* - `locale`:
*/
CustomerMergeLoyaltyAccountsResponse(params: CustomerService.CustomerMergeLoyaltyAccountsParams): __Observable<__StrictHttpResponse<ResponseArgsOfAccountDetailsDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = params.loyaltyCardValues;
if (params.locale != null) __params = __params.set('locale', params.locale.toString());
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/customer/${encodeURIComponent(String(params.customerId))}/loyalty/merge`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfAccountDetailsDTO>;
})
);
}
/**
* Kundenkarte hinzufügen
* @param params The `CustomerService.CustomerMergeLoyaltyAccountsParams` containing the following parameters:
*
* - `loyaltyCardValues`:
*
* - `customerId`:
*
* - `locale`:
*/
CustomerMergeLoyaltyAccounts(params: CustomerService.CustomerMergeLoyaltyAccountsParams): __Observable<ResponseArgsOfAccountDetailsDTO> {
return this.CustomerMergeLoyaltyAccountsResponse(params).pipe(
__map(_r => _r.body as ResponseArgsOfAccountDetailsDTO)
);
}
/**
* Kundenkarte hinzufügen
* @param params The `CustomerService.CustomerAddLoyaltyCardParams` containing the following parameters:
@@ -914,6 +965,15 @@ module CustomerService {
deletionComment?: null | string;
}
/**
* Parameters for CustomerMergeLoyaltyAccounts
*/
export interface CustomerMergeLoyaltyAccountsParams {
loyaltyCardValues: AddLoyaltyCardValues;
customerId: number;
locale?: null | string;
}
/**
* Parameters for CustomerAddLoyaltyCard
*/

View File

@@ -153,7 +153,7 @@ export class CrmSearchService {
const parsed = AddCardSchema.parse(params);
const req$ = this.#customerService
.CustomerAddLoyaltyCard({
.CustomerMergeLoyaltyAccounts({
customerId: parsed.customerId,
loyaltyCardValues: {
cardCode: parsed.loyaltyCardValues.cardCode,

View File

@@ -1,7 +1,166 @@
# utils-positive-integer-input
This library was generated with [Nx](https://nx.dev).
An Angular directive that ensures only positive integers can be entered into number input fields.
## Running unit tests
## Features
- ✅ Blocks invalid characters during keyboard input (`.`, `,`, `-`, `+`, `e`, `E`)
- ✅ Sanitizes pasted content to extract only positive integers
- ✅ Handles all input methods (typing, paste, drag & drop)
- ✅ Removes leading zeros automatically
- ✅ Works seamlessly with Angular forms (`ngModel`, `formControl`)
- ✅ Standalone directive - easy to import
## Installation
The directive is available through the `@isa/utils/positive-integer-input` package.
## Usage
### Basic Usage
Simply add the `positiveIntegerInput` directive to any `<input type="number">` element:
```html
<input type="number" positiveIntegerInput />
```
### With Angular Forms
```html
<!-- With ngModel -->
<input
type="number"
positiveIntegerInput
[(ngModel)]="points"
placeholder="Enter points"
/>
<!-- With Reactive Forms -->
<input
type="number"
positiveIntegerInput
[formControl]="pointsControl"
placeholder="Enter points"
/>
```
### Complete Example
```typescript
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { PositiveIntegerInputDirective } from '@isa/utils/positive-integer-input';
@Component({
selector: 'app-booking',
standalone: true,
imports: [FormsModule, PositiveIntegerInputDirective],
template: `
<input
type="number"
positiveIntegerInput
[(ngModel)]="points"
placeholder="Punkte"
min="1"
step="1"
/>
<p>Entered points: {{ points() ?? 'None' }}</p>
`,
})
export class BookingComponent {
points = signal<number | undefined>(undefined);
}
```
## How It Works
The directive implements three protection layers:
### 1. Keyboard Input Protection (`keydown`)
Blocks invalid keys before they can be entered:
- **Blocked:** `.`, `,`, `-`, `+`, `e`, `E`
- **Allowed:** `0-9`, navigation keys (arrow keys, backspace, delete, tab)
### 2. Paste Protection (`paste`)
Intercepts paste operations and sanitizes the content:
- Extracts only digits from pasted text
- Removes leading zeros
- Updates the input value with the sanitized result
### 3. General Input Protection (`input`)
Catches all other input methods (drag & drop, autofill, programmatic changes):
- Validates and sanitizes any value changes
- Ensures consistency across all input methods
## Examples
### ✅ What Works
| User Action | Input Attempt | Result in Field | Explanation |
|-------------|---------------|-----------------|-------------|
| **Typing** | `123` | `123` | Valid positive integer |
| **Typing** | `1-2-3` | `123` | Minus signs blocked during typing |
| **Typing** | `1.5` | `15` | Decimal point blocked, only digits entered |
| **Paste** | `-100` | `100` | Negative sign removed |
| **Paste** | `1.000` | `1000` | Decimal point removed |
| **Paste** | `1,58` | `158` | Comma removed |
| **Paste** | `+42` | `42` | Plus sign removed |
| **Paste** | `3.14e2` | `314` | Scientific notation sanitized |
| **Paste** | `00123` | `123` | Leading zeros removed |
| **Paste** | `abc123xyz` | `123` | Non-digit characters removed |
| **Typing** | `007` | `7` | Leading zeros removed |
### ❌ What Doesn't Work (By Design)
| User Action | Input Attempt | Result | Explanation |
|-------------|---------------|--------|-------------|
| **Typing/Paste** | `-50` | `50` | Negative numbers converted to positive |
| **Typing/Paste** | `12.34` | `1234` | Decimals removed (not rounded) |
| **Typing/Paste** | `0` | `` (empty) | Single zero removed (use `min="0"` if needed) |
| **Paste** | `abc` | `` (empty) | No digits to extract |
| **Paste** | `---` | `` (empty) | No digits to extract |
## Important Notes
### Zero Handling
The directive removes leading zeros, which means a single `0` input will result in an empty field. If you need to allow zero as a valid value, consider:
```html
<!-- Add min="0" and handle empty state in your component -->
<input
type="number"
positiveIntegerInput
[(ngModel)]="points"
min="0"
/>
```
### Decimal Numbers
This directive is **not suitable** for decimal number inputs. Pasted decimals like `1.58` become `158`, not `1` or `2`. For decimal inputs, use a different validation approach.
### Form Validation
The directive sanitizes input but doesn't perform validation. Combine it with Angular form validators:
```typescript
import { Validators } from '@angular/forms';
// In your component
pointsControl = new FormControl(null, [
Validators.required,
Validators.min(1),
Validators.max(1000)
]);
```
## Browser Compatibility
The directive uses standard browser APIs and works in all modern browsers:
- Chrome/Edge (Chromium-based)
- Firefox
- Safari
- Mobile browsers (iOS Safari, Chrome Mobile)
## Running Unit Tests
Run `nx test utils-positive-integer-input` to execute the unit tests.

View File

@@ -0,0 +1,425 @@
import { describe, it, beforeEach, expect, vi } from 'vitest';
import { Component, signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { PositiveIntegerInputDirective } from './positive-integer-input.directive';
/**
* Test host component to test the directive in a realistic scenario
*/
@Component({
selector: 'test-host',
standalone: true,
imports: [FormsModule, PositiveIntegerInputDirective],
template: `
<input
type="number"
positiveIntegerInput
[(ngModel)]="value"
data-testid="test-input"
/>
`,
})
class TestHostComponent {
value = signal<number | undefined>(undefined);
}
describe('PositiveIntegerInputDirective', () => {
let fixture: ComponentFixture<TestHostComponent>;
let component: TestHostComponent;
let inputElement: HTMLInputElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestHostComponent],
}).compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
inputElement = fixture.nativeElement.querySelector(
'[data-testid="test-input"]',
) as HTMLInputElement;
});
describe('Component Setup and Initialization', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should attach directive to input element', () => {
expect(inputElement).toBeTruthy();
expect(inputElement.type).toBe('number');
});
});
describe('Keyboard Input (keydown) - Blocked Characters', () => {
it('should prevent decimal point (.) from being entered', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: '.' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should prevent comma (,) from being entered', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: ',' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should prevent minus sign (-) from being entered', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: '-' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should prevent plus sign (+) from being entered', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: '+' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should prevent lowercase e from being entered', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: 'e' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should prevent uppercase E from being entered', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: 'E' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).toHaveBeenCalled();
});
});
describe('Keyboard Input (keydown) - Allowed Characters', () => {
it('should allow digit keys (0-9)', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: '5' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).not.toHaveBeenCalled();
});
it('should allow backspace key', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: 'Backspace' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).not.toHaveBeenCalled();
});
it('should allow delete key', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: 'Delete' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).not.toHaveBeenCalled();
});
it('should allow arrow keys', () => {
// Arrange
const leftEvent = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
const rightEvent = new KeyboardEvent('keydown', { key: 'ArrowRight' });
const preventDefaultSpyLeft = vi.spyOn(leftEvent, 'preventDefault');
const preventDefaultSpyRight = vi.spyOn(rightEvent, 'preventDefault');
// Act
inputElement.dispatchEvent(leftEvent);
inputElement.dispatchEvent(rightEvent);
// Assert
expect(preventDefaultSpyLeft).not.toHaveBeenCalled();
expect(preventDefaultSpyRight).not.toHaveBeenCalled();
});
it('should allow tab key', () => {
// Arrange
const event = new KeyboardEvent('keydown', { key: 'Tab' });
const preventDefaultSpy = vi.spyOn(event, 'preventDefault');
// Act
inputElement.dispatchEvent(event);
// Assert
expect(preventDefaultSpy).not.toHaveBeenCalled();
});
});
describe('Input Sanitization - Simulating Paste/External Input', () => {
it('should sanitize negative number (-100 → 100)', () => {
// Arrange
inputElement.value = '-100';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('100');
});
it('should sanitize decimal with dot (1.000 → 1000)', () => {
// Arrange
inputElement.value = '1.000';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('1000');
});
it('should sanitize scientific notation as typed (3142 stays 3142)', () => {
// Note: type="number" inputs accept "3.14e2" but it becomes "3142"
// before our directive sees it
// Arrange
inputElement.value = '3142';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('3142');
});
it('should remove leading zeros (00123 → 123)', () => {
// Arrange
inputElement.value = '00123';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('123');
});
it('should remove leading zeros (007 → 7)', () => {
// Arrange
inputElement.value = '007';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('7');
});
it('should handle single zero (0 → empty)', () => {
// Arrange
inputElement.value = '0';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('');
});
it('should handle multiple zeros (000 → empty)', () => {
// Arrange
inputElement.value = '000';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('');
});
it('should handle valid positive integer without changes (123 → 123)', () => {
// Arrange
inputElement.value = '123';
const dispatchEventSpy = vi.spyOn(inputElement, 'dispatchEvent');
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('123');
// Should only have the initial dispatchEvent call, not an additional one
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
it('should sanitize number with plus at start (+42 becomes 42 by browser)', () => {
// Note: type="number" inputs convert "+42" to "42" before our directive sees it
// Arrange
inputElement.value = '42';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('42');
});
});
describe('Input Events - General Sanitization', () => {
it('should sanitize programmatically set invalid value', () => {
// Arrange
inputElement.value = '-50';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('50');
});
it('should sanitize value with decimal point', () => {
// Arrange
inputElement.value = '12.34';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('1234');
});
it('should remove leading zeros on input', () => {
// Arrange
inputElement.value = '00789';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('789');
});
it('should not trigger unnecessary updates for valid input', () => {
// Arrange
inputElement.value = '123';
const dispatchEventSpy = vi.spyOn(inputElement, 'dispatchEvent');
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
// Should only have the initial dispatchEvent call, not an additional one
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
});
describe('Integration with Angular Forms', () => {
it('should update ngModel with sanitized value', async () => {
// Arrange
inputElement.value = '-250';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
await fixture.whenStable();
fixture.detectChanges();
// Assert
expect(inputElement.value).toBe('250');
});
it('should handle valid positive integer input', async () => {
// Arrange
inputElement.value = '42';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
await fixture.whenStable();
// Assert
expect(inputElement.value).toBe('42');
});
});
describe('Edge Cases', () => {
it('should handle very large numbers', () => {
// Arrange
const largeNumber = '999999999999';
inputElement.value = largeNumber;
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe(largeNumber);
});
it('should handle decimal number by removing decimal point (12.34 → 1234)', () => {
// Arrange
inputElement.value = '12.34';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('1234');
});
it('should handle negative with decimals (-12.34 → 1234)', () => {
// Arrange
inputElement.value = '-12.34';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('1234');
});
it('should handle leading zeros with decimals (00.789 → 789)', () => {
// Arrange
inputElement.value = '00.789';
// Act
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
// Assert
expect(inputElement.value).toBe('789');
});
});
});

View File

@@ -1,25 +1,209 @@
import { Directive, HostListener } from '@angular/core';
import { Directive, ElementRef, HostListener } from '@angular/core';
/**
* Directive that prevents non-numeric input in number input fields.
* Blocks: decimal points (. and ,), negative signs (-), plus signs (+), and exponential notation (e, E)
* Directive that ensures only positive integers can be entered into number input fields.
*
* Usage:
* This directive provides three layers of protection:
* 1. Keyboard input blocking - prevents invalid characters from being typed
* 2. Paste sanitization - cleans pasted content to extract only positive integers
* 3. General input validation - catches any other input methods (drag & drop, autofill, etc.)
*
* **Blocked characters:** `.`, `,`, `-`, `+`, `e`, `E`, and all other non-digit characters
*
* **Examples:**
* - Typing `1-2-3` → only `123` appears (minus signs blocked)
* - Pasting `-100` → becomes `100` (negative sign removed)
* - Pasting `1.000` → becomes `1000` (decimal point removed)
* - Pasting `1,58` → becomes `158` (comma removed)
* - Typing `007` → becomes `7` (leading zeros removed)
*
* **Important Notes:**
* - This directive removes ALL non-digit characters, so `1.58` becomes `158`, not `1` or `2`
* - Leading zeros are automatically removed
* - A single `0` input results in an empty field
* - Works seamlessly with Angular forms (ngModel, formControl)
*
* @example
* ```html
* <!-- Basic usage -->
* <input type="number" positiveIntegerInput />
*
* <!-- With ngModel -->
* <input type="number" positiveIntegerInput [(ngModel)]="points" />
*
* <!-- With form control -->
* <input type="number" positiveIntegerInput [formControl]="pointsControl" />
* ```
*
* @see {@link https://angular.dev/guide/directives} Angular Directives Documentation
*/
@Directive({
selector: 'input[type="number"][positiveIntegerInput]',
standalone: true,
})
export class PositiveIntegerInputDirective {
/**
* Set of characters that should be blocked during keyboard input.
* These characters are common in numeric input but not allowed for positive integers.
*/
private readonly blockedKeys = new Set(['.', ',', '-', '+', 'e', 'E']);
/**
* Injects the ElementRef to get direct access to the native input element.
* This is needed for the paste and input handlers to manipulate the input value.
*
* @param elementRef Reference to the host input element
*/
constructor(private elementRef: ElementRef<HTMLInputElement>) {}
/**
* Handles keyboard input events to prevent invalid characters from being entered.
*
* This is the first line of defense - it blocks keys before they can affect the input value.
* Blocked keys include: `.`, `,`, `-`, `+`, `e`, `E`
*
* Navigation keys (arrows, backspace, delete, tab, etc.) are NOT blocked,
* allowing normal text editing operations.
*
* @example
* User presses `-` → Event prevented, nothing happens
* User presses `5` → Allowed, `5` appears in the input
* User presses `.` → Event prevented, nothing happens
*
* @param event The keyboard event to evaluate
*/
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent): void {
// Check if the pressed key is in our blocked list
if (this.blockedKeys.has(event.key)) {
// Prevent the key from affecting the input
event.preventDefault();
}
}
/**
* Handles paste events to sanitize pasted content before it enters the input.
*
* This is the second line of defense - it prevents users from bypassing keyboard
* restrictions by pasting invalid content (e.g., from Excel, calculators, or text editors).
*
* The handler:
* 1. Prevents the default paste behavior
* 2. Extracts text from the clipboard
* 3. Sanitizes it to contain only positive integers (removes all non-digits)
* 4. Sets the sanitized value in the input
* 5. Triggers an input event so Angular forms (ngModel, formControl) detect the change
*
* @example
* User pastes `-100` → Input value becomes `100`
* User pastes `1.000` → Input value becomes `1000`
* User pastes `1,58` → Input value becomes `158`
* User pastes `abc123xyz` → Input value becomes `123`
* User pastes `00042` → Input value becomes `42`
*
* @param event The clipboard event containing the pasted data
*/
@HostListener('paste', ['$event'])
onPaste(event: ClipboardEvent): void {
// Prevent the default paste behavior to control what gets inserted
event.preventDefault();
// Get the text from the clipboard
const pastedText = event.clipboardData?.getData('text');
if (!pastedText) {
return;
}
// Remove all non-digit characters and leading zeros
const sanitized = this.sanitizeInput(pastedText);
if (sanitized) {
// Insert the sanitized value into the input
const input = this.elementRef.nativeElement;
input.value = sanitized;
// Trigger input event to update Angular form binding (ngModel/formControl)
// The 'bubbles: true' ensures the event propagates up to Angular's listeners
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
/**
* Handles input events to catch any other input methods that bypass keydown and paste.
*
* This is the third line of defense - a catch-all for:
* - Drag and drop operations
* - Browser autofill/autocomplete
* - Programmatic value changes
* - Right-click paste (on some browsers)
* - Voice input
* - IME (Input Method Editor) input
*
* If the current input value contains invalid characters, they are removed and
* the sanitized value is set back to the input.
*
* @example
* User drags `-50` from a text document → Input value becomes `50`
* Browser autofills `1.234` → Input value becomes `1234`
*
* @param event The input event triggered by any value change
*/
@HostListener('input', ['$event'])
onInput(event: Event): void {
const input = event.target as HTMLInputElement;
const value = input.value;
// Sanitize the current value (catches any other ways of setting invalid values)
const sanitized = this.sanitizeInput(value);
// Only update if the value actually changed after sanitization
// This prevents unnecessary updates and potential infinite loops
if (value !== sanitized) {
input.value = sanitized;
// Trigger input event again to ensure Angular picks up the sanitized value
// This is necessary because we're modifying the value programmatically
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}
/**
* Sanitizes a string to contain only positive integers.
*
* This method performs two operations:
* 1. Removes all non-digit characters using regex `\D` (matches anything that's not 0-9)
* 2. Removes leading zeros to prevent issues with number parsing
*
* **Examples:**
* - `sanitizeInput('-100')` → `'100'`
* - `sanitizeInput('1.000')` → `'1000'`
* - `sanitizeInput('1,58')` → `'158'` (NOT `'1'` or `'2'`!)
* - `sanitizeInput('+42')` → `'42'`
* - `sanitizeInput('3.14e2')` → `'314'`
* - `sanitizeInput('abc123xyz')` → `'123'`
* - `sanitizeInput('00123')` → `'123'`
* - `sanitizeInput('007')` → `'7'`
* - `sanitizeInput('0')` → `''` (empty string)
* - `sanitizeInput('abc')` → `''` (no digits found)
*
* **Important:** This method does NOT round or parse decimals.
* It simply removes all non-digit characters, so `1.58` becomes `158`, not `1` or `2`.
*
* @param value The input string to sanitize
* @returns A string containing only digits, with leading zeros removed
*
* @private This method is for internal use only
*/
private sanitizeInput(value: string): string {
// Remove all non-digit characters (including -, +, ., ,, e, E, spaces, etc.)
// \D matches any character that is NOT a digit (0-9)
let sanitized = value.replace(/\D/g, '');
// Remove leading zeros
// ^0+ matches one or more zeros at the start of the string
// This prevents issues like '007' remaining as '007' instead of '7'
// Note: A single '0' will become an empty string ''
sanitized = sanitized.replace(/^0+/, '');
return sanitized;
}
}