Работа в Angular

Интеграция Choices.js в приложения на Angular требует понимания жизненного цикла компонентов, управления DOM через ViewChild и корректной синхронизации сторонних DOM-манипуляций с реактивной моделью Angular.

Choices.js представляет собой библиотеку для замены стандартных <select> и <input> элементов на расширенные компоненты с поддержкой поиска, мультивыбора, тегов, асинхронной подгрузки данных и кастомного рендера опций. Angular, в свою очередь, управляет DOM через собственный механизм шаблонов, поэтому прямое вмешательство Choices.js требует аккуратной инициализации и уничтожения экземпляров.


Базовая установка осуществляется через пакетный менеджер:

npm install choices.js

После установки библиотека подключается в компонент Angular:

import Choices from 'choices.js';
import 'choices.js/public/assets/styles/choices.min.css';

Стили подключаются глобально или на уровне angular.json:

"styles": [
  "node_modules/choices.js/public/assets/styles/choices.min.css"
]

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

В Angular прямой доступ к DOM осуществляется через ViewChild, что позволяет инициализировать Choices после рендера элемента.

<sel ect #countrySelect>
  <option value="kz">Kazakhstan</option>
  <option value="ru">Russia</option>
  <option value="de">Germany</option>
</select>
import { AfterViewInit, Component, ElementRef, ViewChild, OnDestroy } fr om '@angular/core';
import Choices from 'choices.js';

@Component({
  selector: 'app-country-select',
  templateUrl: './country-select.component.html'
})
export class CountrySelectComponent implements AfterViewInit, OnDestroy {
  @ViewChild('countrySelect') countrySelect!: ElementRef<HTMLSelectElement>;

  private choices!: Choices;

  ngAfterViewInit(): void {
    this.choices = new Choices(this.countrySelect.nativeElement, {
      searchEnabled: true,
      shouldSort: false,
      itemSelectText: ''
    });
  }

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

Ключевой момент заключается в использовании AfterViewInit, поскольку DOM-элемент должен быть уже создан.


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

При использовании реактивных форм Angular требуется синхронизация состояния формы и Choices.js.

<sel ect #citySelect [formControl]="cityControl">
  <option *ngFor="let city of cities" [value]="city.id">
    {{ city.name }}
  </option>
</select>
import { Component, AfterViewInit, ViewChild, ElementRef, OnDestroy } fr om '@angular/core';
import { FormControl } from '@angular/forms';
import Choices from 'choices.js';

@Component({
  selector: 'app-city-select',
  templateUrl: './city-select.component.html'
})
export class CitySelectComponent implements AfterViewInit, OnDestroy {
  @ViewChild('citySelect') citySelect!: ElementRef<HTMLSelectElement>;

  cityControl = new FormControl(null);

  cities = [
    { id: '1', name: 'Almaty' },
    { id: '2', name: 'Astana' }
  ];

  private choices!: Choices;

  ngAfterViewInit(): void {
    this.choices = new Choices(this.citySelect.nativeElement, {
      searchEnabled: true,
      shouldSort: false
    });

    this.cityControl.valueChanges.subscribe(value => {
      this.choices.setChoiceByValue(value);
    });

    this.citySelect.nativeElement.addEventListener('change', (event: Event) => {
      const target = event.target as HTMLSelectElement;
      this.cityControl.setValue(target.value, { emitEvent: false });
    });
  }

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

Синхронизация осуществляется в двух направлениях:

  • изменения формы → обновление Choices
  • изменения Choices → обновление FormControl

Реализация ControlValueAccessor

Для полной интеграции с Angular Forms предпочтительным подходом становится реализация ControlValueAccessor.

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

@Component({
  selector: 'app-choices-select',
  template: `
    <select #select>
      <option *ngFor="let option of options" [value]="option.value">
        {{ option.label }}
      </option>
    </select>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ChoicesSelectComponent),
      multi: true
    }
  ]
})
export class ChoicesSelectComponent
  implements ControlValueAccessor, AfterViewInit, OnDestroy {

  @ViewChild('select') select!: ElementRef<HTMLSelectElement>;

  options = [
    { value: 'a', label: 'Option A' },
    { value: 'b', label: 'Option B' }
  ];

  private choices!: Choices;

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

  ngAfterViewInit(): void {
    this.choices = new Choices(this.select.nativeElement, {
      searchEnabled: true
    });

    this.select.nativeElement.addEventListener('change', () => {
      this.onChange(this.select.nativeElement.value);
    });
  }

  writeValue(value: any): void {
    if (this.choices) {
      this.choices.setChoiceByValue(value);
    }
  }

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

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

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

Такой подход превращает компонент в полноценный form-control, совместимый с formControlName.


Динамическое обновление списка опций

Choices.js не всегда автоматически отслеживает изменения DOM, поэтому при обновлении массива данных требуется ручное обновление.

updateOptions(newOptions: Array<{ value: string; label: string }>) {
  const select = this.select.nativeElement;

  select.innerHTML = '';

  newOptions.forEach(opt => {
    const option = document.createElement('option');
    option.value = opt.value;
    option.text = opt.label;
    select.appendChild(option);
  });

  this.choices.setChoices(newOptions, 'value', 'label', true);
}

Флаг true очищает текущие значения перед загрузкой новых.


Асинхронная загрузка данных

Частый сценарий — подгрузка данных из API при вводе текста.

ngAfterViewInit(): void {
  this.choices = new Choices(this.select.nativeElement, {
    searchEnabled: true,
    shouldSort: false
  });

  this.choices.passedElement.element.addEventListener(
    'search',
    async (event: any) => {
      const query = event.detail.value;

      const response = await fetch(`/api/cities?q=${query}`);
      const data = await response.json();

      this.choices.setChoices(
        data.map((c: any) => ({
          value: c.id,
          label: c.name
        })),
        'value',
        'label',
        true
      );
    }
  );
}

Асинхронная модель позволяет использовать Choices как поисковый компонент с серверной фильтрацией.


Работа с мультивыбором

Для мультивыбора используется стандартный HTML атрибут multiple.

<select #tagsSelect multiple>
  <option *ngFor="let tag of tags" [value]="tag.id">
    {{ tag.name }}
  </option>
</select>
this.choices = new Choices(this.tagsSelect.nativeElement, {
  removeItemButton: true,
  maxItemCount: 5,
  searchEnabled: true
});

Особенности мультивыбора:

  • значения возвращаются массивом
  • управление тегами осуществляется через API Choices
  • удаление элементов требует пересинхронизации с Angular формой

Очистка и уничтожение экземпляра

Игнорирование очистки приводит к утечкам памяти, особенно при частой маршрутизации.

ngOnDestroy(): void {
  if (this.choices) {
    this.choices.destroy();
  }
}

Destroy:

  • удаляет обработчики событий
  • очищает DOM-обертки
  • освобождает внутренние ссылки

Производительность и большие списки

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

Рекомендации:

  • использовать shouldSort: false
  • применять серверную фильтрацию
  • избегать частого setChoices
  • кешировать результаты поиска

Стилизация и кастомизация

Choices.js использует набор CSS классов:

  • .choices
  • .choices__inner
  • .choices__list--dropdown
  • .choices__item

Переопределение стилей в Angular:

.choices__inner {
  border-radius: 8px;
  min-height: 44px;
}

.choices__item--selectable {
  font-size: 14px;
}

При использовании Angular View Encapsulation может потребоваться ::ng-deep или глобальные стили.


Типичные проблемы интеграции

1. Несоответствие DOM и Angular состояния Возникает при изменении массива без обновления Choices.

2. Повторная инициализация Создание нескольких экземпляров приводит к дублированию обработчиков.

3. Потеря синхронизации формы Требует ручного связывания через события или ControlValueAccessor.

4. SSR (Server-Side Rendering) Choices.js обращается к window и document, поэтому инициализация должна выполняться только на клиенте:

if (typeof window !== 'undefined') {
  this.choices = new Choices(...);
}

Архитектурные паттерны интеграции

Наиболее устойчивый подход в Angular архитектуре:

  • обёртка над Choices.js как UI-компонент
  • использование ControlValueAccessor
  • изоляция логики инициализации внутри компонента
  • запрет прямого доступа к Choices извне компонента

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