Angular и Inputmask

Angular + Inputmask интеграция строится вокруг идеи управления пользовательским вводом на уровне DOM-инпута через маску, при этом сохраняя совместимость с реактивной моделью форм и системой контроля состояния Angular Forms. Основная сложность заключается в согласовании императивной модели Inputmask с декларативной архитектурой Angular Forms API.


Inputmask работает напрямую с DOM-элементом input, перехватывая ввод, вставку, удаление и изменение значения. Angular, напротив, управляет значением через FormControl или ngModel, синхронизируя состояние через механизмы change detection.

Ключевая задача интеграции:

  • Inputmask управляет отображаемым значением в input
  • Angular Forms управляют модельным значением
  • Оба слоя должны оставаться синхронизированными без циклических обновлений

Установка библиотеки

npm install inputmask

Дополнительно в Angular-проектах часто требуется типизация:

npm install --save-dev @types/inputmask

Базовая интеграция через directive wrapper

Простейший способ подключения — создание директивы-обёртки.

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

Основная проблема 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.


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

Разделение model value и view value

Inputmask поддерживает различие:

  • raw value — без форматирования
  • display value — с маской

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

Взаимодействие с валидаторами Angular

Валидация должна разделять:

  • структурную корректность (Inputmask)
  • бизнес-валидацию (Angular Validators)

Пример:

Validators.pattern(/^\d{10}$/)

При этом Inputmask может допускать промежуточные состояния, поэтому:

  • нельзя валидировать masked value напрямую
  • необходимо использовать unmasked value

Обработка вставки и автозаполнения

Браузерные autofill сценарии нарушают поток событий.

Решение:

@HostListener('paste', ['$event'])
onPaste(e: ClipboardEvent) {
  setTimeout(() => {
    const value = this.el.nativeElement.value;
    this.onChange(value);
  });
}

setTimeout используется для ожидания применения маски.


Производительность и change detection

Inputmask работает вне Angular zone.

Оптимизация:

  • инициализация вне NgZone
  • минимизация setValue вызовов
  • отключение лишних событий
this.ngZone.runOutsideAngular(() => {
  this.imask.mask(this.el.nativeElement);
});

SSR (Angular Universal)

При серверном рендеринге DOM отсутствует, поэтому:

  • Inputmask нельзя инициализировать на сервере
  • требуется проверка isPlatformBrowser
if (isPlatformBrowser(this.platformId)) {
  this.imask.mask(this.el.nativeElement);
}

Сложные сценарии синхронизации

Программное обновление формы

this.form.patchValue({
  phone: '+79991234567'
});

Проблема: маска может не примениться автоматически.

Решение:

setTimeout(() => {
  this.imask.setValue(value);
});

Reset формы

При form.reset() Inputmask сохраняет старое значение DOM.

Необходимо вручную очищать:

this.imask.remove();
this.el.nativeElement.value = '';
this.imask.mask(this.el.nativeElement);

Частые ошибки интеграции

  • двойная инициализация маски
  • отсутствие очистки при destroy
  • использование masked value как model value
  • конфликт ngModel и ReactiveForms
  • отсутствие синхронизации при programmatic update

Рекомендуемая структура production-директивы

  • initialization в ngAfterViewInit
  • cleanup в ngOnDestroy
  • поддержка ControlValueAccessor
  • поддержка динамических mask inputs
  • работа с raw/unmasked value
  • защита от SSR

Пример итоговой архитектуры

  • Angular FormControl хранит чистое значение
  • Inputmask отвечает только за UX ввода
  • директива выступает адаптером между слоями
  • все изменения проходят через единый поток ControlValueAccessor