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 шаблонных биндингов без обёртки приводит к рассинхронизации состояния.
При использовании 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.
Более правильный архитектурный подход — создание кастомного 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]
});
Такой подход требует осторожности, поскольку повторная инициализация может приводить к потере состояния ввода.
Angular использует механизм зональности (zone.js), а
Cleave.js изменяет DOM напрямую, минуя 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>();
При использовании 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 });
}
}
Часто встречающиеся проблемы:
destroy() в ngOnDestroyДля стабильной интеграции с Angular:
ControlValueAccessor как основной
интерфейсАрхитектура интеграции строится вокруг принципа: Angular управляет состоянием, Cleave.js управляет отображением ввода, а связующий слой обеспечивает строгую синхронизацию без дублирования источников истины.