Angular + Inputmask интеграция строится вокруг идеи управления пользовательским вводом на уровне DOM-инпута через маску, при этом сохраняя совместимость с реактивной моделью форм и системой контроля состояния Angular Forms. Основная сложность заключается в согласовании императивной модели Inputmask с декларативной архитектурой Angular Forms API.
Inputmask работает напрямую с DOM-элементом input,
перехватывая ввод, вставку, удаление и изменение значения. Angular,
напротив, управляет значением через FormControl или
ngModel, синхронизируя состояние через механизмы change
detection.
Ключевая задача интеграции:
npm install inputmask
Дополнительно в Angular-проектах часто требуется типизация:
npm install --save-dev @types/inputmask
Простейший способ подключения — создание директивы-обёртки.
import { Directive, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';
import Inputmask from 'inputmask';
@Directive({
selector: '[appInputMask]'
})
export class InputMaskDirective implements OnInit, OnDestroy {
@Input('appInputMask') mask: any;
private imaskInstance: any;
constructor(private el: ElementRef<HTMLInputElement>) {}
ngOnInit(): void {
this.imaskInstance = new Inputmask(this.mask);
this.imaskInstance.mask(this.el.nativeElement);
}
ngOnDestroy(): void {
if (this.imaskInstance) {
this.imaskInstance.remove();
}
}
}
Использование:
<input type="text" [appInputMask]="'99-99-9999'" />
Основная проблема Reactive Forms — Inputmask изменяет DOM, минуя
FormControl.setValue.
Корректная интеграция требует синхронизации через события Inputmask.
this.imaskInstance = new Inputmask(this.mask);
this.imaskInstance.opts.oncompl ete = () => {
const value = this.el.nativeElement.value;
this.control.setValue(value, { emitEvent: false });
};
Более корректный вариант — слушать input:
this.el.nativeElement.addEventListener('input', () => {
const value = this.el.nativeElement.value;
this.control.setValue(value, { emitEvent: false });
});
Однако это может приводить к рассинхронизации при программных обновлениях, поэтому предпочтительнее использовать ControlValueAccessor.
Angular-совместимая интеграция строится через
ControlValueAccessor.
import {
Directive, ElementRef, forwardRef, HostListener
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import Inputmask from 'inputmask';
@Directive({
selector: '[appMaskedInput]',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MaskedInputDirective),
multi: true
}]
})
export class MaskedInputDirective implements ControlValueAccessor {
private onCha nge = (value: any) => {};
private onTouc hed = () => {};
private imask: any;
constructor(private el: ElementRef<HTMLInputElement>) {}
writeValue(value: any): void {
this.el.nativeElement.value = value ?? '';
}
registerOnChange(fn: any): void {
this.onCha nge = fn;
}
registerOnTouched(fn: any): void {
this.onTouc hed = fn;
}
setDisabledState(isDisabled: boolean): void {
this.el.nativeElement.disabled = isDisabled;
}
@HostListener('input')
handleInput(): void {
const value = this.el.nativeElement.value;
this.onChange(value);
}
@HostListener('blur')
handleBlur(): void {
this.onTouched();
}
ngOnInit(): void {
this.imask = new Inputmask('99-99-9999');
this.imask.mask(this.el.nativeElement);
}
}
Inputmask поддерживает различие:
В Angular важно хранить именно raw value в модели.
Пример извлечения:
const masked = this.el.nativeElement.value;
const raw = this.imaskInstance?.unmaskedvalue?.();
В реальных приложениях маска часто зависит от данных:
@Input() set mask(value: string) {
if (this.imask) {
this.imask.remove();
}
this.imask = new Inputmask(value);
this.imask.mask(this.el.nativeElement);
}
Пример сценариев:
Типовой паттерн:
Inputmask({
mask: '+7 (999) 999-99-99',
showMaskOnHover: false,
showMaskOnFocus: true
});
Особенность интеграции: при потере фокуса Angular может триггерить
valueChanges, поэтому важно контролировать emit:
this.control.setValue(rawValue, { emitEvent: false });
Валидация должна разделять:
Пример:
Validators.pattern(/^\d{10}$/)
При этом Inputmask может допускать промежуточные состояния, поэтому:
Браузерные autofill сценарии нарушают поток событий.
Решение:
@HostListener('paste', ['$event'])
onPaste(e: ClipboardEvent) {
setTimeout(() => {
const value = this.el.nativeElement.value;
this.onChange(value);
});
}
setTimeout используется для ожидания применения
маски.
Inputmask работает вне Angular zone.
Оптимизация:
NgZonesetValue вызововthis.ngZone.runOutsideAngular(() => {
this.imask.mask(this.el.nativeElement);
});
При серверном рендеринге DOM отсутствует, поэтому:
isPlatformBrowserif (isPlatformBrowser(this.platformId)) {
this.imask.mask(this.el.nativeElement);
}
this.form.patchValue({
phone: '+79991234567'
});
Проблема: маска может не примениться автоматически.
Решение:
setTimeout(() => {
this.imask.setValue(value);
});
При form.reset() Inputmask сохраняет старое значение
DOM.
Необходимо вручную очищать:
this.imask.remove();
this.el.nativeElement.value = '';
this.imask.mask(this.el.nativeElement);
ngModel и ReactiveFormsngAfterViewInitngOnDestroyControlValueAccessor