Angular директива

Архитектура интеграции кастомных UI-компонентов в Angular строится вокруг директив и адаптеров формы. Основная задача директивы для Tom Select — связать imperative API библиотеки с реактивной моделью Angular Forms, обеспечив двустороннюю синхронизацию состояния, корректное управление жизненным циклом и минимизацию побочных эффектов при изменениях данных.

Базовая модель интеграции

Tom Select работает поверх стандартного <select> или <input> и преобразует его в управляемый компонент с поиском, тегами, кастомными рендерами и асинхронной подгрузкой. В Angular это требует:

  • синхронизации value ↔︎ model
  • реакции на изменения formControl
  • управления DOM-инициализацией вне зоны Angular
  • корректного уничтожения инстанса при destroy компонента

Директива выступает как прослойка между Angular Forms API и внутренним состоянием Tom Select.

Контракт ControlValueAccessor

Ключевой механизм интеграции — реализация интерфейса ControlValueAccessor. Он определяет контракт:

  • запись значения из модели в UI
  • уведомление формы об изменениях
  • обработка touch-событий
  • отключение компонента

Типовая структура:

  • writeValue(value: any) — обновление UI
  • registerOnChange(fn) — регистрация callback изменения значения
  • registerOnTouched(fn) — регистрация touch-события
  • setDisabledState(isDisabled) — управление disabled состоянием

Именно через этот слой Angular абстрагирует DOM-логику.

Инициализация Tom Select внутри директивы

Инициализация выполняется после появления элемента в DOM, обычно в ngAfterViewInit.

Основная схема:

  1. Получение ссылки на native <select>
  2. Создание инстанса Tom Select
  3. Подписка на события изменения
  4. Привязка обработчиков к Angular forms

Логика:

  • инициализация только один раз
  • защита от повторного создания
  • хранение ссылки на инстанс

При создании учитываются опции:

  • create
  • options
  • valueField
  • labelField
  • searchField
  • plugins

Пример структуры директивы

@Directive({
  selector: '[appTomSelect]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => TomSelectDirective),
      multi: true
    }
  ]
})
export class TomSelectDirective implements ControlValueAccessor, AfterViewInit, OnDestroy {
  private ts: any;
  private onChange: (value: any) => void = () => {};
  private onTouched: () => void = () => {};

  constructor(private el: ElementRef) {}

  ngAfterViewInit(): void {
    this.initTomSelect();
  }

  private initTomSelect(): void {
    this.ts = new TomSelect(this.el.nativeElement, {
      onChange: (value: any) => this.onChange(value),
      onBlur: () => this.onTouched()
    });
  }

  writeValue(value: any): void {
    if (this.ts) {
      this.ts.setValue(value, true);
    }
  }

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

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

  setDisabledState(isDisabled: boolean): void {
    if (this.ts) {
      isDisabled ? this.ts.disable() : this.ts.enable();
    }
  }

  ngOnDestroy(): void {
    this.ts?.destroy();
  }
}

Синхронизация состояния

Основная сложность заключается в предотвращении циклических обновлений:

  • изменение формы → обновление Tom Select
  • изменение Tom Select → обновление формы

Чтобы избежать зацикливания:

  • используется флаг подавления событий
  • применяется параметр silent update
  • обновление модели отделяется от UI-инициализации

Пример логики:

  • writeValue обновляет UI без emit событий
  • onChange вызывает Angular callback только при пользовательском вводе

Работа с reactive forms

В связке с FormControl директива становится прозрачным адаптером:

  • установка значения через setValue
  • сброс через reset
  • валидация через Angular validators

Особенности:

  • массив значений поддерживается нативно
  • multi-select требует синхронизации массива строк/объектов
  • асинхронные значения требуют обновления options перед setValue

Динамические options

Tom Select поддерживает динамическое добавление и удаление опций. В Angular директиве это реализуется через @Input():

  • обновление списка options
  • пересборка internal index
  • предотвращение потери selected values

Стратегия обновления:

  • сравнение ссылок массива
  • частичное обновление через addOption
  • полная пересборка через clearOptions

Асинхронный поиск

Tom Select часто используется с remote data source. В директиве это реализуется через:

  • debounce пользовательского ввода
  • Observable поток (RxJS)
  • cancel предыдущих запросов

Алгоритм:

  1. пользователь вводит текст
  2. Tom Select вызывает hook load
  3. Angular сервис выполняет HTTP запрос
  4. результаты подаются в addOption

Важно контролировать:

  • race conditions
  • устаревшие ответы
  • очистку при destroy

Управление жизненным циклом

Жизненный цикл директивы критичен:

  • ngAfterViewInit — создание инстанса
  • ngOnChanges — обновление options
  • ngOnDestroy — уничтожение и освобождение памяти

Особое внимание уделяется:

  • утечкам event listeners
  • сохранению ссылок на DOM
  • корректному destroy Tom Select instance

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

Интеграция с Angular требует контроля зон:

  • инициализация вне Angular zone
  • минимизация trigger change detection
  • использование runOutsideAngular

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

  • debounce ввода
  • memoization options
  • избегание лишних writeValue вызовов

Поддержка кастомного рендера

Tom Select позволяет кастомизировать отображение:

  • опций
  • выбранных элементов
  • dropdown header/footer

В Angular это реализуется через:

  • шаблонные строки
  • или render callback функции
  • либо интеграцию с Angular TemplateRef (через manual rendering)

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

Типичные проблемы:

  • null value при инициализации
  • несоответствие типов (string vs object)
  • повторная инициализация при *ngIf
  • потеря состояния при пересоздании компонента

Стратегии:

  • нормализация входных данных
  • строгая типизация моделей
  • guard на существование инстанса

Расширенные сценарии

Директива может расширяться до полноценного адаптера:

  • поддержка virtual scroll
  • интеграция с infinite loading
  • синхронизация с store (NgRx)
  • поддержка grouped options

В сложных сценариях Tom Select используется как view-layer компонент, а Angular управляет исключительно состоянием данных, оставляя DOM полностью библиотеке.