# Input

- Entry point: `@koobiq/components/input`
- NgModule: `KbqInputModule`
- Page: https://koobiq.io/en/components/input/overview

## Overview

Input provides text, numeric, and password fields with formatting, validation, masking, and other data-entry behavior.

### With default parameters

**Example: Input** (`input-overview`)

```ts input-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';

@Component({
    selector: 'input-overview-example',
    imports: [
        KbqInputModule,
        KbqIconModule,
        FormsModule
    ],
    template: `
        <kbq-form-field>
            <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix [autoColor]="true"></i>

            <input kbqInput placeholder="Placeholder" [(ngModel)]="value" />

            <kbq-cleaner />
        </kbq-form-field>

        <br />
        <br />
        <br />

        <kbq-form-field noBorders>
            <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix [autoColor]="true"></i>

            <input kbqInput placeholder="Placeholder" [(ngModel)]="value" />

            <kbq-cleaner />
        </kbq-form-field>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputOverviewExample {
    value = '';
}
```

### Numeric input field

This is a special field for entering numbers only. You can type digits here, and the system will automatically format them in a user-friendly way (for example, adding thousands separators).

In cases where the thousands separator is a thin space, the input field includes logic to replace them when pasting from the clipboard, using the `kbqNormalizeWhitespace` directive. This behavior is not enabled by default — it is expected to be turned on manually when needed.

Number formatting depends on your chosen locale. This means the same number may look different depending on your regional settings.

**Example: Input number** (`input-number-overview`)

```ts input-number-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqFormsModule, KbqNormalizeWhitespace } from '@koobiq/components/core';
import { KbqInputModule } from '@koobiq/components/input';

@Component({
    selector: 'input-number-overview-example',
    imports: [
        KbqInputModule,
        FormsModule,
        KbqFormsModule,
        KbqNormalizeWhitespace
    ],
    template: `
        <div class="kbq-text-big">
            <div class="kbq-form-horizontal">
                <div class="kbq-form__row">
                    <label class="kbq-form__label flex-40" for="number-with-bounds">
                        С разделением групп разрядов, с мин. и макс. ограничением
                    </label>
                    <kbq-form-field class="kbq-form__control flex-60">
                        <input
                            id="number-with-bounds"
                            kbqNumberInput
                            kbqNormalizeWhitespace
                            [max]="12000"
                            [min]="-12000"
                            [(ngModel)]="value"
                        />
                        <kbq-stepper />

                        <kbq-hint>От −12 000 до 12 000</kbq-hint>
                    </kbq-form-field>
                </div>

                <div class="kbq-form__row">
                    <label class="kbq-form__label flex-40" for="number-with-separator">
                        С разделителем групп разрядов
                    </label>
                    <kbq-form-field class="kbq-form__control flex-60">
                        <input id="number-with-separator" kbqNumberInput kbqNormalizeWhitespace [(ngModel)]="value" />
                        <kbq-stepper />
                    </kbq-form-field>
                </div>

                <div class="kbq-form__row">
                    <label class="kbq-form__label flex-40" for="number-without-separator">
                        Без разделителя групп разрядов
                    </label>
                    <kbq-form-field class="kbq-form__control flex-60">
                        <input
                            id="number-without-separator"
                            kbqNumberInput
                            kbqNormalizeWhitespace
                            [withThousandSeparator]="false"
                            [(ngModel)]="value"
                        />
                        <kbq-stepper />
                    </kbq-form-field>
                </div>

                <div class="kbq-form__row">
                    <label class="kbq-form__label flex-40" for="number-integer">Целочисленное значение</label>
                    <kbq-form-field class="kbq-form__control flex-60">
                        <input
                            id="number-integer"
                            kbqNumberInput
                            kbqNormalizeWhitespace
                            [integer]="true"
                            [(ngModel)]="integerValue"
                        />
                        <kbq-stepper />
                    </kbq-form-field>
                </div>
            </div>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputNumberOverviewExample {
    value = '';
    integerValue = '';
}
```

#### Numeric input API

`kbqNumberInput` accepts the bounds and the step it applies, plus the formatting switches:

- `min` / `max` — the range the value is clamped to when stepping. Default `-Infinity` / `Infinity`. Both accept a static attribute (`min="3"`) and a binding (`[min]="3"`); the value is coerced to a number either way.
- `step` — the amount added by `ArrowUp`/`ArrowDown` and by `kbq-stepper`. Default `1`.
- `bigStep` — the amount added when `Shift` is held. Default `10`.
- `integer` — rejects the fraction separator on typing and truncates a pasted fraction. Default `false`.
- `withThousandSeparator` — groups the integer part with the locale's separator. Default `true`.
- `startFormattingFrom` — grouping only kicks in from this power of ten, so `3` groups `1234` and `4` does not. Defaults to the active locale's own value (`4` for `ru-RU`), which is why a four-digit number stays un-grouped there.

The directive renders into a `type="text"` field. Do not add `type="number"`: the browser's value sanitizer rejects the formatted value and blanks the field, so the type is reset to `text` with a console warning.

`min` and `max` also drive the `KbqMinValidator` / `KbqMaxValidator` directives shipped in `KbqInputModule`, which install `Validators.min` / `Validators.max` on any `formControl`, `formControlName` or `ngModel` control carrying the corresponding attribute, and mirror the bound value into the native `min` / `max` attribute.

`min`, `max`, `step` and `bigStep` are signals, so reading one programmatically is a call — `numberInput.max()`. They are read-only: an `input()` has no `.set()`, so a value that changes at runtime is bound rather than assigned. `valueChange` and `disabledChange` are RxJS `Subject`s; subscribing is unchanged, emitting is `.next()`.

#### Number formatting and locale

Separators come from the active locale, through the `input.number` section of its configuration. Override them for a subtree with `kbqInputLocaleConfigurationProvider({ number: { fractionSeparator: '.' } })`; only the keys you pass are replaced, the rest keep following the locale. `KBQ_INPUT_DEFAULT_LOCALE_CONFIGURATION` holds the `ru-RU` defaults.

### Monospace input

`kbqInputMonospace` switches a `kbqInput` to the monospace type level — useful for identifiers, hashes and keys.

### Password input

**Example: Input password** (`input-password-overview`)

```ts input-password-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractControl, FormControl, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
import { PasswordValidators } from '@koobiq/components/core';
import { KbqInputModule } from '@koobiq/components/input';

/**
 * Allows printable ASCII only: latin letters, digits, spaces and special characters. An empty password fails too,
 * as it fails the other rules.
 */
const latinAndSpecialSymbols = ({ value }: AbstractControl): ValidationErrors | null =>
    typeof value === 'string' && !/^[\x20-\x7E]+$/.test(value) ? { latinAndSpecialSymbols: true } : null;

@Component({
    selector: 'input-password-overview-example',
    imports: [
        KbqInputModule,
        ReactiveFormsModule
    ],
    template: `
        <kbq-form-field style="width: 250px">
            <input kbqInputPassword [formControl]="control" />

            <kbq-password-toggle [kbqTooltipHidden]="'Показать пароль'" [kbqTooltipNotHidden]="'Скрыть пароль'" />

            <kbq-reactive-password-hint [hasError]="control.hasError('minLength') || control.hasError('maxLength')">
                От 8 до 15 символов
            </kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="control.hasError('minUppercase')">
                Заглавная латинская буква
            </kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="control.hasError('minLowercase')">
                Строчная латинская буква
            </kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="control.hasError('minNumber')">Цифра</kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="control.hasError('latinAndSpecialSymbols')">
                Только латинские буквы, цифры, пробелы и спецсимволы
            </kbq-reactive-password-hint>
        </kbq-form-field>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputPasswordOverviewExample {
    readonly control = new FormControl('', [
        PasswordValidators.minLength(8),
        PasswordValidators.maxLength(15),
        PasswordValidators.minUppercase(1),
        PasswordValidators.minLowercase(1),
        PasswordValidators.minNumber(1),
        latinAndSpecialSymbols
    ]);
}
```

### Using input masks

Input masks control the format and validate data in real time.

The example uses the [Maskito](https://maskito.dev/) library:

```bash
npm install @maskito/core @maskito/angular @maskito/kit
```

**Example: Input with mask** (`input-with-mask`)

```ts input-with-mask-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
    AbstractControl,
    FormControl,
    ReactiveFormsModule,
    ValidationErrors,
    ValidatorFn,
    Validators
} from '@angular/forms';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { MaskitoDirective } from '@maskito/angular';
import { MaskitoOptions, MaskitoPlugin, maskitoUpdateElement } from '@maskito/core';
import { maskitoNumberOptionsGenerator, maskitoWithPlaceholder } from '@maskito/kit';

@Component({
    selector: 'input-with-mask-example',
    imports: [
        ReactiveFormsModule,
        MaskitoDirective,
        KbqInputModule,
        KbqIconModule
    ],
    template: `
        <kbq-form-field>
            <kbq-label>IP address</kbq-label>
            <input kbqInput placeholder="192.168.0.1" [maskito]="ipAddressMask" [formControl]="ipAddressControl" />
            <kbq-hint>Format: 192.168.0.1</kbq-hint>
            <kbq-error>Invalid IP address</kbq-error>
            <kbq-cleaner />
        </kbq-form-field>

        <kbq-form-field>
            <kbq-label>MAC address</kbq-label>
            <input
                kbqInput
                placeholder="AA:BB:CC:DD:EE:FF"
                [maskito]="macAddressMask"
                [formControl]="macAddressControl"
            />
            <kbq-hint>Format: AA:BB:CC:DD:EE:FF</kbq-hint>
            <kbq-error>Invalid MAC address</kbq-error>
            <kbq-cleaner />
        </kbq-form-field>

        <kbq-form-field>
            <kbq-label>IPv6 address</kbq-label>
            <input
                kbqInput
                placeholder="2001:0db8:85a3:0000:0000:8a2e:0370:7334"
                [maskito]="ipv6AddressMask"
                [formControl]="ipv6AddressControl"
            />
            <kbq-hint>Format: 2001:0db8:85a3:0000:0000:8a2e:0370:7334</kbq-hint>
            <kbq-error>Invalid IPv6 address</kbq-error>
            <kbq-cleaner />
        </kbq-form-field>

        <kbq-form-field>
            <kbq-label>Port</kbq-label>
            <input kbqInput placeholder="8080" [maskito]="portMask" [formControl]="portControl" />
            <kbq-hint>Range: 0-65535</kbq-hint>
            <kbq-hint>Use ↑/↓ to increase or decrease the value</kbq-hint>
            <kbq-cleaner />
        </kbq-form-field>

        <kbq-form-field>
            <kbq-label>License key</kbq-label>
            <input kbqInput placeholder="XXXX-XXXX-XXXX-XXXX" [maskito]="licenseMask" [formControl]="licenseControl" />
            <kbq-error>Invalid license key</kbq-error>
            <kbq-cleaner />
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            gap: var(--kbq-size-l);
            align-items: center;
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputWithMaskExample {
    readonly ipAddressControl = new FormControl('192.168.0.1', [this.ipAddressValidator()]);
    readonly macAddressControl = new FormControl('AA:BB:CC:DD:EE:FF', [
        Validators.pattern(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/)
    ]);
    readonly ipv6AddressControl = new FormControl('2001:0db8:85a3:0000:0000:8a2e:0370:7334', [
        Validators.pattern(/^([0-9A-Fa-f]{4}:){7}[0-9A-Fa-f]{4}$/)
    ]);
    readonly portControl = new FormControl('8080');
    readonly licenseControl = new FormControl('ABCD-1234-EFGH-5678', [
        Validators.pattern(/^[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}$/)
    ]);

    readonly ipAddressMask = this.makeIPAddressMaskOptions();
    readonly macAddressMask = this.makeMACAddressMaskOptions();
    readonly ipv6AddressMask = this.makeIPv6AddressMaskOptions();
    readonly portMask = this.makePortMaskOptions();
    readonly licenseMask = this.makeLicenseMaskOptions();

    private makeIPAddressMaskOptions(): MaskitoOptions {
        return {
            mask: ({ value }) => {
                const octets = value.split('.');

                return octets.flatMap((octet, index) => {
                    const digitCount = Math.max(1, Math.min(3, octet.length));
                    const digits = Array.from<RegExp>({ length: digitCount }).fill(/\d/);

                    return index < 3 ? [...digits, '.'] : digits;
                });
            }
        };
    }

    private makeMACAddressMaskOptions(): MaskitoOptions {
        const hexChar = /[0-9A-Fa-f]/;

        return {
            mask: [
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar
            ],
            postprocessors: [
                ({ value, selection }) => ({ value: value.toUpperCase(), selection })
            ]
        };
    }

    private makeIPv6AddressMaskOptions(): MaskitoOptions {
        const hexChar = /[0-9A-Fa-f]/;

        return {
            mask: [
                hexChar,
                hexChar,
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                hexChar,
                hexChar,
                ':',
                hexChar,
                hexChar,
                hexChar,
                hexChar
            ],
            postprocessors: [
                ({ value, selection }) => ({ value: value.toLowerCase(), selection })
            ]
        };
    }

    private ipAddressValidator(): ValidatorFn {
        return (control: AbstractControl): ValidationErrors | null => {
            const value = control.value;

            if (!value) return null;

            const octets = value.split('.');

            if (octets.length !== 4) return { invalidIp: true };

            const isValid = octets.every((octet: string) => {
                const num = parseInt(octet, 10);

                return !isNaN(num) && num >= 0 && num <= 255 && octet === String(num);
            });

            return isValid ? null : { invalidIp: true };
        };
    }

    private makePortMaskOptions(): MaskitoOptions {
        const min = 0;
        const max = 65535;
        const options = maskitoNumberOptionsGenerator({
            min,
            max,
            maximumFractionDigits: 0,
            thousandSeparator: ''
        });

        const stepPlugin: MaskitoPlugin = (element) => {
            const handler = (event: KeyboardEvent) => {
                if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
                    return;
                }

                event.preventDefault();

                const step = event.key === 'ArrowUp' ? 1 : -1;
                const currentValue = parseInt(element.value, 10) || 0;
                const newValue = Math.min(max, Math.max(min, currentValue + step));

                maskitoUpdateElement(element, String(newValue));
            };

            element.addEventListener('keydown', handler);

            return () => element.removeEventListener('keydown', handler);
        };

        return {
            ...options,
            plugins: [...options.plugins, stepPlugin]
        };
    }

    private makeLicenseMaskOptions(): MaskitoOptions {
        const alphanumeric = /[A-Za-z0-9]/;

        return {
            ...maskitoWithPlaceholder('____-____-____-____', true),
            mask: [
                alphanumeric,
                alphanumeric,
                alphanumeric,
                alphanumeric,
                '-',
                alphanumeric,
                alphanumeric,
                alphanumeric,
                alphanumeric,
                '-',
                alphanumeric,
                alphanumeric,
                alphanumeric,
                alphanumeric,
                '-',
                alphanumeric,
                alphanumeric,
                alphanumeric,
                alphanumeric
            ]
        };
    }
}
```

## API

### Directives

#### KbqInput (directive)

```ts
@Directive({
    selector: 'input[kbqInput],input[kbqNumberInput]',
    exportAs: 'kbqInput'
})
class KbqInput implements KbqFormFieldControl<any>, CanUpdateErrorState {
    readonly errorStateMatcher = input<ErrorStateMatcher>();
    type = input<string | undefined>();
    ngControl: NgControl | null;
    numberInput: KbqNumberInput | null;
    parentForm: NgForm | null;
    parentFormGroup: FormGroupDirective | null;
    defaultErrorStateMatcher: ErrorStateMatcher;
    readonly errorState: Signal<boolean>;
    updateErrorState(): void;
    focus(): void;
    onBlur(): void;
    focusChanged(isFocused: boolean): void;
}
```

- `[errorStateMatcher]`: `ErrorStateMatcher` — An object used to control when error messages are shown.
- `[type]`: `string | undefined` — Input type of the element.
- `errorState`: `Signal<boolean>` — Whether the component is in an error state.
- `focus()`: `void` — Focuses the input.
- `focusChanged()`: `void` — Callback for the cases where the focused state of the input changes.

#### KbqInputMono (directive)

```ts
@Directive({
    selector: 'input[kbqInputMonospace]',
    exportAs: 'kbqInputMonospace, KbqInputMonospace'
})
class KbqInputMono {}
```

#### KbqInputPassword (directive)

```ts
@Directive({
    selector: 'input[kbqInputPassword]',
    exportAs: 'kbqInputPassword'
})
class KbqInputPassword implements KbqFormFieldControl<any>, CanUpdateErrorState {
    readonly errorStateMatcher = input<ErrorStateMatcher>();
    ngControl: NgControl | null;
    parentForm: NgForm | null;
    parentFormGroup: FormGroupDirective | null;
    defaultErrorStateMatcher: ErrorStateMatcher;
    readonly errorState: Signal<boolean>;
    readonly elementType: Signal<"text" | "password">;
    updateErrorState(): void;
    toggleType(): void;
    focus(): void;
    onBlur(): void;
    focusChanged(isFocused: boolean): void;
}
```

- `[errorStateMatcher]`: `ErrorStateMatcher` — An object used to control when error messages are shown.
- `errorState`: `Signal<boolean>` — Whether the component is in an error state.
- `elementType`: `Signal<"text" | "password">` — Type of the native input: `password` while the password is hidden, `text` while it is shown.
- `toggleType()`: `void` — Toggles the native input type between password and text.
- `focus()`: `void` — Focuses the input.
- `focusChanged()`: `void` — Callback for the cases where the focused state of the input changes.

#### KbqMaxValidator (directive)

A directive which installs the `KbqMaxValidator` for any `formControlName`,
`formControl`, or control with `ngModel` that also has a `max` attribute.

```ts
@Directive({
    selector: '[max][formControlName],[max][formControl],[max][ngModel]'
})
class KbqMaxValidator implements Validator {
    readonly max = input<number>();
    validate(c: AbstractControl): ValidationErrors | null;
    registerOnValidatorChange(fn: () => void): void;
}
```

- `[max]`: `number` — Upper bound installed as `Validators.max`. A non-numeric bound reads back as `NaN`.

#### KbqMinValidator (directive)

A directive which installs the `KbqMinValidator` for any `formControlName`,
`formControl`, or control with `ngModel` that also has a `min` attribute.

```ts
@Directive({
    selector: '[min][formControlName],[min][formControl],[min][ngModel]'
})
class KbqMinValidator implements Validator {
    readonly min = input<number>();
    validate(c: AbstractControl): ValidationErrors | null;
    registerOnValidatorChange(fn: () => void): void;
}
```

- `[min]`: `number` — Lower bound installed as `Validators.min`. A non-numeric bound reads back as `NaN`.

#### KbqNumberInput (directive)

```ts
@Directive({
    selector: 'input[kbqNumberInput]',
    exportAs: 'kbqNumberInput, kbqNumericalInput'
})
class KbqNumberInput implements ControlValueAccessor {
    readonly integer = input<boolean>(false);
    readonly bigStep = input<number>(BIG_STEP);
    readonly step = input<number>(SMALL_STEP);
    readonly min = input<number>(-Infinity);
    readonly max = input<number>(Infinity);
    readonly withThousandSeparator = input<boolean>(true);
    readonly startFormattingFrom = input<number>();
    value = input<number | null | undefined>();
    disabled = input<boolean | undefined>();
    readonly valueChange: Subject<number | null>;
    readonly disabledChange: Subject<boolean>;
    readonly stateChanges: Subject<void>;
    focused: boolean;
    get nativeElement(): HTMLInputElement;
    get viewValue(): string;
    get valueAsNumber(): number | null;
    onContainerClick(): void;
    focus(): void;
    focusChanged(isFocused: boolean): void;
    onKeyDown(event: KeyboardEvent): void;
    onInput(event: InputEvent): void;
    onPaste(event: ClipboardEvent): void;
    stepUp(step: number): void;
    stepDown(step: number): void;
}
```

- `[integer]`: `boolean` — Allows input and pasting of integers only.
- `[bigStep]`: `number` — Step applied when `Shift` is held.
- `[step]`: `number` — Step applied by the arrow keys and by `kbq-stepper`.
- `[min]`: `number` — Lower bound the value is clamped to when stepping.
- `[max]`: `number` — Upper bound the value is clamped to when stepping.
- `[startFormattingFrom]`: `number` — Adds the thousand separator only from this power of ten. For example, `4` keeps `1234`
  un-grouped and groups `12345`. Defaults to the active locale's own value.
- `[value]`: `number | null | undefined` — The numeric value of the field, bound with `[value]` or written by a form control.
- `[disabled]`: `boolean | undefined` — Whether the field is disabled, bound with `[disabled]` or set by a form control.
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.
- `valueChange`: `Subject<number | null>` — Emits when the value changes (either due to user input or programmatic change).
- `disabledChange`: `Subject<boolean>` — Emits when the disabled state has changed
- `stateChanges`: `Subject<void>` — Emits whenever the focused or disabled state of the directive changes.
- `valueAsNumber`: `number | null` — Locale-aware numeric read of the field: parsed straight from `nativeElement.value`, so `"1 234,5"`
  reads back as `1234.5` where the native `valueAsNumber` of a `type="text"` input reports `NaN`.
  Computed on every read rather than cached off the committed model, so it never lags behind a
  keystroke the way `value` can while `onInput`'s reformat is still pending in its `setTimeout(0)` —
  safe to read from a consumer's own `(input)` handler. An in-progress `"-"` reads as `null`, same as
  an empty field, rather than the last committed number.

### Constants

#### KBQ_INPUT_DEFAULT_LOCALE_CONFIGURATION (const)

Default configuration of `KbqNumberInput`: the whole `input` locale section, of which the number input
reads `number`.

```ts
const KBQ_INPUT_DEFAULT_LOCALE_CONFIGURATION: KbqInputLocaleConfiguration;
```

#### KBQ_INPUT_LOCALE_CONFIGURATION (const)

Injection token for providing the default configuration of `KbqNumberInput`.

```ts
const KBQ_INPUT_LOCALE_CONFIGURATION: InjectionToken<KbqInputLocaleConfiguration>;
```

#### KBQ_INPUT_NUMBER_DEFAULT_CONFIGURATION (const)

Deprecated: Use `KBQ_INPUT_DEFAULT_LOCALE_CONFIGURATION`, which carries the whole `input`
locale section rather than just its `number` slice. Will be removed in 22.0.0.

```ts
const KBQ_INPUT_NUMBER_DEFAULT_CONFIGURATION: { groupSeparator: string[]; fractionSeparator: string; startFormattingFrom: number; viewGroupSeparator: string; };
```

#### KBQ_INPUT_VALUE_ACCESSOR (const)

```ts
const KBQ_INPUT_VALUE_ACCESSOR: InjectionToken<{ value: any }>;
```

#### KBQ_MAX_VALIDATOR (const)

```ts
const KBQ_MAX_VALIDATOR: Provider;
```

#### KBQ_MIN_VALIDATOR (const)

```ts
const KBQ_MIN_VALIDATOR: Provider;
```

#### KBQ_NUMBER_INPUT_VALUE_ACCESSOR (const)

```ts
const KBQ_NUMBER_INPUT_VALUE_ACCESSOR: any;
```

#### kbqInputLocaleConfigurationProvider (const)

Utility provider for `KBQ_INPUT_LOCALE_CONFIGURATION`. Only the values you pass are overridden; the rest
keep following the active locale.

```ts
const kbqInputLocaleConfigurationProvider: (
    configuration: KbqDeepPartial<KbqInputLocaleConfiguration>
) => Provider;
```

## Examples

### Password change form

**Example: Validation: cross-field** (`validation-cross-field-password`)

```ts validation-cross-field-password-example.ts
import { ChangeDetectionStrategy, Component, viewChildren } from '@angular/core';
import {
    AbstractControl,
    FormControl,
    FormGroup,
    FormGroupDirective,
    ReactiveFormsModule,
    ValidationErrors,
    Validators,
    type ValidatorFn
} from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import {
    kbqErrorStateMatcherProvider,
    KbqFormsModule,
    PasswordValidators,
    ShowOnCrossFieldErrorStateMatcher,
    ShowRequiredOnSubmitErrorStateMatcher
} from '@koobiq/components/core';
import { KbqFormField } from '@koobiq/components/form-field';
import { KbqInputModule } from '@koobiq/components/input';

/**
 * Payload every cross-field validator below publishes, so that a single `ErrorStateMatcher` can tell which
 * controls a group-level error belongs to. The names are relative to the group holding the error.
 */
type ExampleCrossFieldError = { controls: string[] };

const isExampleCrossFieldError = (value: unknown): value is ExampleCrossFieldError =>
    typeof value === 'object' && value !== null && 'controls' in value && Array.isArray(value.controls);

/**
 * Compares two control values for equality — both rules below only ever ask "same or not", never "which is
 * greater". The default (`===`) is enough for the strings here; an ordering comparator, needed for the date
 * pair in `validation-cross-field-dates-example.ts`, is a different job with a different default.
 */
type ExampleCompare = (a: unknown, b: unknown) => boolean;

const exampleValuesEqual: ExampleCompare = (a, b) => a === b;

// A control that is missing or still empty is not a cross-field problem: `Validators.required` owns that
// case, and reporting a mismatch against an empty field only gets in the user's way.
const isExampleValueEmpty = (value: unknown): boolean => value === null || value === undefined || value === '';

/** All the listed controls must hold the same value. */
const exampleMatchAll =
    (controls: string[], compare: ExampleCompare = exampleValuesEqual): ValidatorFn =>
    (group: AbstractControl): ValidationErrors | null => {
        const values = controls.map((name) => group.get(name)?.value);

        if (values.some(isExampleValueEmpty)) {
            return null;
        }

        const matches = values.every((value) => compare(value, values[0]));

        return matches ? null : { matchAll: { controls } satisfies ExampleCrossFieldError };
    };

/** All the listed controls must hold different values. */
const exampleDistinct =
    (controls: string[], compare: ExampleCompare = exampleValuesEqual): ValidatorFn =>
    (group: AbstractControl): ValidationErrors | null => {
        const values = controls.map((name) => group.get(name)?.value);

        if (values.some(isExampleValueEmpty)) {
            return null;
        }

        const distinct = values.every((value, index) =>
            values.every((other, otherIndex) => index === otherIndex || !compare(value, other))
        );

        return distinct ? null : { distinct: { controls } satisfies ExampleCrossFieldError };
    };

/**
 * The library matcher does the display half: it shows a group-level error on the controls that error concerns,
 * once every one of them is touched. All it needs is a way to map an error to its controls — here the
 * validators publish that list themselves, so the scope is a single lookup.
 *
 * `ShowRequiredOnSubmitErrorStateMatcher` as the second argument keeps the validation guide's rule for
 * `required` — an empty field shouldn't turn red on blur — without affecting the cross-field errors above,
 * which still reveal once every control they name has been touched, or on submit.
 */
const exampleCrossFieldMatcher = new ShowOnCrossFieldErrorStateMatcher(
    (_key, value) => (isExampleCrossFieldError(value) ? value.controls : null),
    new ShowRequiredOnSubmitErrorStateMatcher()
);

@Component({
    selector: 'validation-cross-field-password-example',
    imports: [ReactiveFormsModule, KbqInputModule, KbqButtonModule, KbqFormsModule],
    template: `
        <form #formDirective="ngForm" class="kbq-form-vertical" [formGroup]="form" (ngSubmit)="onSubmit(formDirective)">
            <div class="kbq-form__fieldset">
                <div class="kbq-form__row">
                    <kbq-form-field>
                        <kbq-label>Current password</kbq-label>
                        <input formControlName="currentPassword" kbqInputPassword />
                        <kbq-password-toggle />

                        @if (form.controls.currentPassword.hasError('required')) {
                            <kbq-error>Required</kbq-error>
                        }
                    </kbq-form-field>
                </div>

                <div class="kbq-form__row">
                    <kbq-form-field>
                        <kbq-label>New password</kbq-label>
                        <input formControlName="newPassword" kbqInputPassword />
                        <kbq-password-toggle />

                        @if (form.controls.newPassword.hasError('required')) {
                            <kbq-error>Required</kbq-error>
                        } @else if (form.controls.newPassword.hasError('minLength')) {
                            <kbq-error>Minimum 8 characters</kbq-error>
                        } @else if (form.hasError('distinct')) {
                            <kbq-error>The new password must differ from the current one</kbq-error>
                        }
                    </kbq-form-field>
                </div>

                <div class="kbq-form__row">
                    <kbq-form-field>
                        <kbq-label>Confirm new password</kbq-label>
                        <input formControlName="confirmPassword" kbqInputPassword />
                        <kbq-password-toggle />

                        @if (form.controls.confirmPassword.hasError('required')) {
                            <kbq-error>Required</kbq-error>
                        } @else if (form.hasError('matchAll')) {
                            <kbq-error>Passwords do not match</kbq-error>
                        }
                    </kbq-form-field>
                </div>

                <div class="kbq-form__row">
                    <button color="contrast" kbq-button type="submit">Change password</button>
                </div>
            </div>
        </form>
    `,
    styles: `
        form {
            width: 320px;
        }
    `,
    providers: [
        kbqErrorStateMatcherProvider(exampleCrossFieldMatcher)
    ],
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-align-center-center layout-row'
    }
})
export class ValidationCrossFieldPasswordExample {
    protected readonly form = new FormGroup(
        {
            currentPassword: new FormControl('', Validators.required),
            newPassword: new FormControl('', [Validators.required, PasswordValidators.minLength(8)]),
            confirmPassword: new FormControl('', Validators.required)
        },
        {
            // Both rules live on the group and both are served by the same matcher. Their control sets differ
            // and overlap on `newPassword`, so the highlight follows the error payload rather than lighting
            // up the whole group.
            validators: [
                exampleDistinct(['currentPassword', 'newPassword']),
                exampleMatchAll(['newPassword', 'confirmPassword'])
            ]
        }
    );

    private readonly formFieldList = viewChildren(KbqFormField);

    protected onSubmit(formDirective: FormGroupDirective): void {
        if (this.form.invalid) {
            this.focusFirstInvalidControl();

            return;
        }

        // `FormGroupDirective.resetForm()`, not `this.form.reset()`: the latter only resets the model, not
        // the directive's own `submitted` flag, which would otherwise stay `true` forever and make every
        // cross-field error reveal immediately on the next attempt instead of waiting for both fields touched.
        formDirective.resetForm();
    }

    private focusFirstInvalidControl(): void {
        // The matcher re-evaluates `errorState` on the next change detection, triggered by the submit event
        // itself — deferred a tick so `formFieldList()` reports each field's post-submit invalid state.
        setTimeout(() => {
            const invalidControl = this.formFieldList().find((control) => control.invalid);

            invalidControl?.focus();
        });
    }
}
```

### Password hints in a template-driven form

`PasswordValidators` are functions, so a template-driven form attaches them to `ngModel` through a validator directive. The hints read the errors from the `ngModel` reference.

**Example: Input password with template-driven forms** (`input-password-template-driven`)

```ts input-password-template-driven-example.ts
import { ChangeDetectionStrategy, Component, Directive, signal } from '@angular/core';
import { AbstractControl, FormsModule, NG_VALIDATORS, ValidationErrors, Validator, Validators } from '@angular/forms';
import { PasswordValidators } from '@koobiq/components/core';
import { KbqInputModule } from '@koobiq/components/input';

/** Template-driven forms take validators from directives, so this one hands `PasswordValidators` to `ngModel`. */
@Directive({
    selector: '[examplePasswordRules]',
    providers: [{ provide: NG_VALIDATORS, useExisting: ExamplePasswordRules, multi: true }]
})
class ExamplePasswordRules implements Validator {
    private readonly rules = Validators.compose([
        PasswordValidators.minLength(8),
        PasswordValidators.maxLength(15),
        PasswordValidators.minUppercase(1),
        PasswordValidators.minLowercase(1),
        PasswordValidators.minNumber(1),
        PasswordValidators.minSpecial(1)
    ]);

    validate(control: AbstractControl): ValidationErrors | null {
        return this.rules?.(control) ?? null;
    }
}

@Component({
    selector: 'input-password-template-driven-example',
    imports: [
        KbqInputModule,
        FormsModule,
        ExamplePasswordRules
    ],
    template: `
        <kbq-form-field style="width: 250px">
            <kbq-label>Password</kbq-label>

            <input #passwordModel="ngModel" examplePasswordRules kbqInputPassword [(ngModel)]="password" />

            <kbq-password-toggle />

            <kbq-reactive-password-hint
                [hasError]="passwordModel.hasError('minLength') || passwordModel.hasError('maxLength')"
            >
                8 to 15 characters
            </kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="passwordModel.hasError('minUppercase')">
                An uppercase letter
            </kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="passwordModel.hasError('minLowercase')">
                A lowercase letter
            </kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="passwordModel.hasError('minNumber')">
                A digit
            </kbq-reactive-password-hint>

            <kbq-reactive-password-hint [hasError]="passwordModel.hasError('minSpecial')">
                A special character
            </kbq-reactive-password-hint>
        </kbq-form-field>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class InputPasswordTemplateDrivenExample {
    protected readonly password = signal('');
}
```
