Интеграция с Angular

Cleave.js представляет собой библиотеку для форматирования пользовательского ввода в реальном времени: номера карт, телефоны, даты, числовые значения. При работе с Angular интеграция требует учёта жизненного цикла компонентов, реактивных форм и механизма change detection.

Установка выполняется через npm:

npm install cleave.js

В Angular-проекте библиотека обычно импортируется непосредственно в компонент или сервис:

import Cleave from 'cleave.js';
import 'cleave.js/dist/addons/cleave-phone.us';

При необходимости локализации телефонных масок подключаются дополнительные модули cleave-phone.*.

Базовая инициализация внутри компонента

Наиболее распространённый подход — инициализация в ngAfterViewInit, когда DOM-элемент уже доступен:

import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import Cleave from 'cleave.js';

@Component({
  selector: 'app-phone-input',
  template: `<input #phoneInput type="text" />`
})
export class PhoneInputComponent implements AfterViewInit, OnDestroy {
  @ViewChild('phoneInput') phoneInput!: ElementRef;

  private cleave!: Cleave;

  ngAfterViewInit(): void {
    this.cleave = new Cleave(this.phoneInput.nativeElement, {
      phone: true,
      phoneRegionCode: 'US'
    });
  }

  ngOnDestroy(): void {
    this.cleave.destroy();
  }
}

Ключевой момент заключается в том, что библиотека напрямую манипулирует DOM-элементом, поэтому использование Angular шаблонных биндингов без обёртки приводит к рассинхронизации состояния.

Интеграция с Reactive Forms

При использовании FormControl необходимо синхронизировать значение между Angular и Cleave.js вручную:

import { Component, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import Cleave from 'cleave.js';

@Component({
  selector: 'app-card-input',
  template: `<input #cardInput [formControl]="cardControl" />`
})
export class CardInputComponent implements AfterViewInit, OnDestroy {
  @ViewChild('cardInput') cardInput!: ElementRef;

  cardControl = new FormControl('');

  private cleave!: Cleave;

  ngAfterViewInit(): void {
    this.cleave = new Cleave(this.cardInput.nativeElement, {
      creditCard: true,
      onCreditCardTypeChanged: (type: string) => {
        this.cardControl.setValue(this.cleave.getRawValue(), { emitEvent: false });
      }
    });

    this.cardControl.valueChanges.subscribe(value => {
      if (value !== this.cleave.getRawValue()) {
        this.cleave.setRawValue(value || '');
      }
    });
  }

  ngOnDestroy(): void {
    this.cleave.destroy();
  }
}

Основная сложность заключается в предотвращении циклических обновлений между Angular и библиотекой. Для этого используется emitEvent: false.

Реализация ControlValueAccessor

Более правильный архитектурный подход — создание кастомного form control через ControlValueAccessor. Это позволяет полностью скрыть логику Cleave.js внутри компонента.

import {
  Component,
  forwardRef,
  ElementRef,
  ViewChild,
  AfterViewInit,
  OnDestroy
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import Cleave from 'cleave.js';

@Component({
  selector: 'app-cleave-input',
  template: `<input #input type="text" (input)="onInput()" />`,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CleaveInputComponent),
      multi: true
    }
  ]
})
export class CleaveInputComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
  @ViewChild('input') input!: ElementRef;

  private cleave!: Cleave;

  private onChange: (value: string) => void = () => {};
  private onTouched: () => void = () => {};

  ngAfterViewInit(): void {
    this.cleave = new Cleave(this.input.nativeElement, {
      numeral: true,
      numeralThousandsGroupStyle: 'thousand'
    });
  }

  writeValue(value: string): void {
    if (this.cleave) {
      this.cleave.setRawValue(value || '');
    }
  }

  registerOnChange(fn: any): void {
    this.onCha nge = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouc hed = fn;
  }

  onInput(): void {
    const raw = this.cleave.getRawValue();
    this.onChange(raw);
  }

  ngOnDestroy(): void {
    this.cleave.destroy();
  }
}

Такой подход полностью совместим с реактивными и шаблонными формами Angular и устраняет необходимость ручной синхронизации.

Форматирование чисел и валют

Cleave.js поддерживает числовые маски:

this.cleave = new Cleave(this.input.nativeElement, {
  numeral: true,
  numeralDecimalMark: '.',
  delimiter: ',',
  numeralDecimalScale: 2
});

В Angular важно учитывать локализацию: форматирование может конфликтовать с региональными настройками приложения.

Работа с датами

this.cleave = new Cleave(this.input.nativeElement, {
  date: true,
  datePattern: ['Y', 'm', 'd']
});

При интеграции с Angular Forms часто требуется преобразование в ISO-формат перед отправкой на сервер, поскольку библиотека оперирует строковым представлением.

Маскирование телефонов

this.cleave = new Cleave(this.input.nativeElement, {
  phone: true,
  phoneRegionCode: 'RU'
});

Подключение региональных модулей обязательно:

import 'cleave.js/dist/addons/cleave-phone.ru';

Управление динамическими настройками

В Angular часто возникает необходимость менять маску в зависимости от состояния формы:

this.cleave.destroy();

this.cleave = new Cleave(this.input.nativeElement, {
  numericOnly: true,
  blocks: [4, 4, 4, 4]
});

Такой подход требует осторожности, поскольку повторная инициализация может приводить к потере состояния ввода.

Особенности работы с Change Detection

Angular использует механизм зональности (zone.js), а Cleave.js изменяет DOM напрямую, минуя Angular.

Это приводит к нескольким важным эффектам:

  • Angular не всегда фиксирует изменения значения
  • ngModel может не обновляться автоматически
  • требуется ручной вызов событий или синхронизация через ControlValueAccessor

Для оптимизации часто используется стратегия:

ChangeDetectionStrategy.OnPush

и ручной контроль обновлений.

Обработка событий библиотеки

Некоторые сценарии требуют реакции на изменение типа карты:

this.cleave = new Cleave(this.input.nativeElement, {
  creditCard: true,
  onCreditCardTypeChanged: (type: string) => {
    this.onChangeType(type);
  }
});

В Angular это обычно проксируется через @Output:

@Output() cardTypeChange = new EventEmitter<string>();

SSR и Angular Universal

При использовании Angular Universal важно учитывать отсутствие DOM на сервере. Инициализация Cleave.js должна выполняться только в браузере:

import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

ngAfterViewInit(): void {
  if (isPlatformBrowser(this.platformId)) {
    this.cleave = new Cleave(this.input.nativeElement, { numeral: true });
  }
}

Типовые архитектурные ошибки

Часто встречающиеся проблемы:

  • двойная инициализация экземпляра Cleave при повторном рендере компонента
  • отсутствие destroy() в ngOnDestroy
  • попытка использовать двусторонний binding без синхронизации raw value
  • конфликт между форматированным и “сырым” значением
  • отсутствие защиты от race conditions при async обновлениях формы

Оптимизационные практики

Для стабильной интеграции с Angular:

  • использовать ControlValueAccessor как основной интерфейс
  • хранить только raw value в модели
  • минимизировать пересоздание экземпляра Cleave
  • избегать прямого доступа к DOM вне Angular lifecycle hooks
  • синхронизировать изменения через контролируемые события

Архитектура интеграции строится вокруг принципа: Angular управляет состоянием, Cleave.js управляет отображением ввода, а связующий слой обеспечивает строгую синхронизацию без дублирования источников истины.