Работа с Angular

Интеграция библиотеки AutoNumeric в Angular требует понимания особенностей жизненного цикла компонентов, механизма привязки данных, работы с формами и взаимодействия с DOM. Поскольку AutoNumeric напрямую управляет HTML-элементом, Angular и библиотека могут конфликтовать при обновлении значения поля. Для корректной интеграции требуется правильно организовать двустороннюю синхронизацию.

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

Установка через npm:

npm install autonumeric

Импорт в компоненте:

import AutoNumeric from 'autonumeric';

Базовая интеграция в компонент Angular

HTML-шаблон

<input #priceInput type="text">

Компонент

import {
    AfterViewInit,
    Component,
    ElementRef,
    ViewChild
} from '@angular/core';

import AutoNumeric from 'autonumeric';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements AfterViewInit {

    @ViewChild('priceInput')
    priceInput!: ElementRef;

    autoNumeric!: AutoNumeric;

    ngAfterViewInit(): void {
        this.autoNumeric = new AutoNumeric(
            this.priceInput.nativeElement,
            {
                digitGroupSeparator: ' ',
                decimalCharacter: ',',
                decimalPlaces: 2,
                currencySymbol: '₸ '
            }
        );
    }
}

Почему используется ngAfterViewInit

Экземпляр AutoNumeric требует существования реального DOM-элемента. На этапе constructor или ngOnInit ссылка ViewChild ещё недоступна.

Правильный жизненный цикл:

ngAfterViewInit()

Именно в этот момент Angular завершает рендеринг шаблона.


Получение значения из AutoNumeric

Форматированное значение

const formatted = this.autoNumeric.getFormatted();

Результат:

₸ 12 500,00

Числовое значение

const numeric = this.autoNumeric.getNumber();

Результат:

12500

Строковое числовое значение

const raw = this.autoNumeric.getNumericString();

Результат:

12500.00

Установка значения

Простая установка

this.autoNumeric.set(45000);

Установка строки

this.autoNumeric.set('12500.75');

Очистка поля

this.autoNumeric.clear();

Работа с Angular Forms

Наиболее важная часть интеграции — совместимость с Angular Forms.

Существует два основных подхода:

  1. Template-driven forms
  2. Reactive forms

Template-Driven Forms

Использование ngModel

HTML

<input
    #amountInput
    type="text"
    [(ngModel)]="amount">

Проблема

AutoNumeric изменяет значение DOM напрямую, а Angular ожидает контроль над полем через ngModel.

В результате возможны:

  • циклические обновления;
  • потеря форматирования;
  • неправильное отображение значения;
  • рассинхронизация модели.

Правильная синхронизация

Компонент

import {
    AfterViewInit,
    Component,
    ElementRef,
    ViewChild
} from '@angular/core';

import AutoNumeric from 'autonumeric';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements AfterViewInit {

    @ViewChild('amountInput')
    amountInput!: ElementRef;

    amount = 0;

    anElement!: AutoNumeric;

    ngAfterViewInit(): void {

        this.anElement = new AutoNumeric(
            this.amountInput.nativeElement,
            {
                decimalPlaces: 2
            }
        );

        this.amountInput.nativeElement.addEventListener(
            'autoNumeric:rawValueModified',
            () => {
                this.amount = this.anElement.getNumber();
            }
        );
    }
}

Reactive Forms

Reactive Forms значительно лучше подходят для интеграции с AutoNumeric.


Подключение FormControl

Компонент

import {
    AfterViewInit,
    Component,
    ElementRef,
    ViewChild
} from '@angular/core';

import {
    FormControl,
    FormGroup
} from '@angular/forms';

import AutoNumeric from 'autonumeric';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements AfterViewInit {

    @ViewChild('salaryInput')
    salaryInput!: ElementRef;

    form = new FormGroup({
        salary: new FormControl(0)
    });

    an!: AutoNumeric;

    ngAfterViewInit(): void {

        this.an = new AutoNumeric(
            this.salaryInput.nativeElement,
            {
                currencySymbol: '$ '
            }
        );

        this.salaryInput.nativeElement.addEventListener(
            'autoNumeric:rawValueModified',
            () => {

                const value = this.an.getNumber();

                this.form.patchValue(
                    {
                        salary: value
                    },
                    {
                        emitEvent: false
                    }
                );
            }
        );
    }
}

Почему используется emitEvent: false

Без этого параметра возможно появление бесконечного цикла:

  1. AutoNumeric меняет значение
  2. Angular обновляет FormControl
  3. FormControl обновляет input
  4. AutoNumeric снова реагирует

Параметр предотвращает повторную генерацию событий.


Создание Angular Directive

Наиболее правильный способ интеграции AutoNumeric — собственная директива.

Такой подход:

  • переиспользуем;
  • изолирует логику;
  • совместим с Angular Forms;
  • поддерживает ControlValueAccessor.

Базовая директива

import {
    Directive,
    ElementRef,
    AfterViewInit
} from '@angular/core';

import AutoNumeric from 'autonumeric';

@Directive({
    selector: '[appAutoNumeric]'
})
export class AutoNumericDirective implements AfterViewInit {

    private anElement!: AutoNumeric;

    constructor(private el: ElementRef) {}

    ngAfterViewInit(): void {

        this.anElement = new AutoNumeric(
            this.el.nativeElement,
            {
                digitGroupSeparator: ' ',
                decimalCharacter: ','
            }
        );
    }
}

Использование директивы

<input type="text" appAutoNumeric>

ControlValueAccessor

Для полной совместимости с Angular Forms требуется реализация интерфейса ControlValueAccessor.


Полная директива

import {
    Directive,
    ElementRef,
    forwardRef,
    AfterViewInit
} from '@angular/core';

import {
    ControlValueAccessor,
    NG_VALUE_ACCESSOR
} from '@angular/forms';

import AutoNumeric from 'autonumeric';

@Directive({
    selector: '[appAutoNumeric]',
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AutoNumericDirective),
            multi: true
        }
    ]
})
export class AutoNumericDirective
implements ControlValueAccessor, AfterViewInit {

    private autoNumeric!: AutoNumeric;

    private onChange: any = () => {};
    private onTouched: any = () => {};

    constructor(private el: ElementRef) {}

    ngAfterViewInit(): void {

        this.autoNumeric = new AutoNumeric(
            this.el.nativeElement,
            {
                currencySymbol: '$ ',
                decimalPlaces: 2
            }
        );

        this.el.nativeElement.addEventListener(
            'autoNumeric:rawValueModified',
            () => {

                this.onChange(
                    this.autoNumeric.getNumber()
                );
            }
        );

        this.el.nativeElement.addEventListener(
            'blur',
            () => {
                this.onTouched();
            }
        );
    }

    writeValue(value: any): void {

        if (value !== null && value !== undefined) {
            this.autoNumeric.set(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;
    }
}

Использование директивы с FormControl

<input
    type="text"
    formControlName="price"
    appAutoNumeric>

Передача настроек через @Input

Жёстко заданные настройки неудобны. Директива должна поддерживать конфигурацию.


Директива

import {
    Directive,
    ElementRef,
    Input,
    AfterViewInit
} from '@angular/core';

import AutoNumeric from 'autonumeric';

@Directive({
    selector: '[appAutoNumeric]'
})
export class AutoNumericDirective
implements AfterViewInit {

    @Input()
    options: any = {};

    an!: AutoNumeric;

    constructor(private el: ElementRef) {}

    ngAfterViewInit(): void {

        this.an = new AutoNumeric(
            this.el.nativeElement,
            this.options
        );
    }
}

Использование

<input
    type="text"
    appAutoNumeric
    [options]="{
        currencySymbol: '€ ',
        decimalPlaces: 2
    }">

Динамическое обновление настроек

Angular может изменять входные параметры после инициализации.


Реализация OnChanges

import {
    Directive,
    ElementRef,
    Input,
    OnChanges,
    SimpleChanges
} from '@angular/core';

import AutoNumeric from 'autonumeric';

@Directive({
    selector: '[appAutoNumeric]'
})
export class AutoNumericDirective
implements OnChanges {

    @Input()
    options: any = {};

    an!: AutoNumeric;

    constructor(private el: ElementRef) {

        this.an = new AutoNumeric(
            this.el.nativeElement,
            this.options
        );
    }

    ngOnChanges(changes: SimpleChanges): void {

        if (changes['options']) {

            this.an.update(
                changes['options'].currentValue
            );
        }
    }
}

Работа с событиями

AutoNumeric генерирует собственные DOM-события.


Основные события

Событие Описание
autoNumeric:initialized Инициализация
autoNumeric:rawValueModified Изменение числового значения
autoNumeric:formatted Форматирование
autoNumeric:minExceeded Значение меньше минимума
autoNumeric:maxExceeded Значение больше максимума

Подписка на события

this.el.nativeElement.addEventListener(
    'autoNumeric:maxExceeded',
    (event: Event) => {

        console.log('Максимум превышен');
    }
);

Удаление экземпляра

Angular уничтожает компоненты при смене маршрутов и условий *ngIf.

Если не удалить AutoNumeric вручную, возможны:

  • утечки памяти;
  • висящие события;
  • некорректные ссылки на DOM.

Использование OnDestroy

import {
    Directive,
    OnDestroy
} from '@angular/core';

export class AutoNumericDirective
implements OnDestroy {

    an!: AutoNumeric;

    ngOnDestroy(): void {

        if (this.an) {
            this.an.remove();
        }
    }
}

Использование в Angular Material

AutoNumeric совместим с Angular Material, однако имеются особенности.


Пример

<mat-form-field appearance="outline">

    <mat-label>Сумма</mat-label>

    <input
        matInput
        appAutoNumeric
        [options]="{
            currencySymbol: '₽ ',
            decimalPlaces: 2
        }">

</mat-form-field>

Проблемы Angular Material

Конфликт placeholder

AutoNumeric может изменять содержимое поля, что влияет на анимацию mat-label.

Решение:

emptyInputBehavior: 'null'

Некорректное определение пустого значения

Material ожидает null, а AutoNumeric может возвращать пустую строку.

Рекомендуемая настройка:

emptyInputBehavior: null

Использование с ngx-datatable и таблицами

При динамическом создании строк экземпляры AutoNumeric необходимо создавать отдельно для каждого input.


Пример с *ngFor

<tr *ngFor="let item of items">

    <td>
        <input
            type="text"
            appAutoNumeric>
    </td>

</tr>

Особенности Change Detection

AutoNumeric изменяет DOM напрямую, обходя Angular.

Иногда Angular не замечает изменения.


Использование ChangeDetectorRef

import {
    ChangeDetectorRef
} from '@angular/core';

constructor(
    private cdr: ChangeDetectorRef
) {}

Принудительное обновление

this.cdr.detectChanges();

Работа с OnPush

При стратегии ChangeDetectionStrategy.OnPush Angular реагирует только на:

  • изменение ссылок;
  • события;
  • async pipe.

AutoNumeric работает вне Angular Zone.


Использование NgZone

import {
    NgZone
} from '@angular/core';

constructor(
    private zone: NgZone
) {}

Возврат в Angular Zone

this.zone.run(() => {

    this.control.setValue(
        this.an.getNumber()
    );
});

SSR и Angular Universal

AutoNumeric использует window и DOM API.

На сервере Angular Universal DOM отсутствует.


Проверка платформы

import {
    isPlatformBrowser
} from '@angular/common';

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

Защита от SSR

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

if (isPlatformBrowser(this.platformId)) {

    this.an = new AutoNumeric(
        this.el.nativeElement
    );
}

Lazy Loading и AutoNumeric

При ленивой загрузке модулей директиву необходимо экспортировать.


SharedModule

@NgModule({
    declarations: [
        AutoNumericDirective
    ],
    exports: [
        AutoNumericDirective
    ]
})
export class SharedModule {}

Типизация настроек

AutoNumeric предоставляет типы для TypeScript.


Использование типов

import AutoNumeric, {
    Options
} from 'autonumeric';

Типизированные настройки

options: Partial<Options> = {
    currencySymbol: '$ ',
    decimalPlaces: 2
};

Асинхронная загрузка значений

Частая ситуация — получение данных с сервера после инициализации.


Пример

this.http.get('/api/product')
.subscribe((data: any) => {

    this.an.set(data.price);
});

Ошибки синхронизации

Распространённая ошибка:

this.control.setValue(5000);

Angular обновляет модель, но AutoNumeric не знает об изменении.


Правильный вариант

this.an.set(5000);

или внутри writeValue.


Работа с валютами

Тенге

{
    currencySymbol: '₸ ',
    digitGroupSeparator: ' ',
    decimalCharacter: ','
}

Доллары

{
    currencySymbol: '$ ',
    decimalCharacter: '.',
    digitGroupSeparator: ','
}

Евро

{
    currencySymbol: '€ ',
    decimalCharacter: ',',
    digitGroupSeparator: '.'
}

Работа с процентами

{
    suffixText: ' %',
    decimalPlaces: 2
}

Ограничение диапазона

{
    minimumValue: '0',
    maximumValue: '1000000'
}

Запрет отрицательных значений

{
    minimumValue: '0'
}

Обработка ошибок

Проверка экземпляра

if (this.an) {
    this.an.set(100);
}

Защита от отсутствующего элемента

if (this.el?.nativeElement) {

    this.an = new AutoNumeric(
        this.el.nativeElement
    );
}

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

При большом количестве полей рекомендуется:

  • использовать директивы;
  • удалять экземпляры при уничтожении;
  • избегать лишних подписок;
  • отключать ненужные события;
  • использовать OnPush;
  • минимизировать вызовы detectChanges.

Архитектурный подход

Наиболее стабильная схема интеграции:

  1. Angular FormControl хранит числовое значение.
  2. AutoNumeric отвечает только за форматирование.
  3. ControlValueAccessor синхронизирует данные.
  4. DOM напрямую не изменяется вне директивы.
  5. Все настройки передаются через @Input.

Такая архитектура обеспечивает:

  • совместимость с Angular Forms;
  • стабильную двустороннюю синхронизацию;
  • отсутствие циклических обновлений;
  • корректную работу Change Detection;
  • повторное использование компонента в приложении любой сложности.