Интеграция 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"
]
В 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-элемент должен быть уже
создан.
При использовании реактивных форм 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();
}
}
Синхронизация осуществляется в двух направлениях:
Для полной интеграции с 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
});
Особенности мультивыбора:
Игнорирование очистки приводит к утечкам памяти, особенно при частой маршрутизации.
ngOnDestroy(): void {
if (this.choices) {
this.choices.destroy();
}
}
Destroy:
При работе с большими массивами данных ключевым фактором становится минимизация DOM операций.
Рекомендации:
shouldSort: falsesetChoicesChoices.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 архитектуре:
Такой подход сохраняет реактивность Angular и минимизирует побочные эффекты сторонней библиотеки.