Библиотека Choices.js предоставляет развитый API, благодаря которому компонент выбора можно интегрировать практически с любыми внешними системами: UI-фреймворками, серверными API, библиотеками валидации, менеджерами состояния, CMS, инструментами аналитики и собственными плагинами.
Интеграция строится вокруг нескольких ключевых механизмов:
Базовая схема подключения:
import Choices from 'choices.js';
const element = document.querySelector('#categories');
const choices = new Choices(element, {
searchEnabled: true,
removeItemButton: true
});
После создания экземпляра компонент становится полноценным
интерактивным слоем поверх обычного <select> или
<input>.
Одной из наиболее распространённых задач является получение списка вариантов с сервера.
Пример загрузки данных через Fetch API:
const sel ect = document.querySelector('#users');
const choices = new Choices(select, {
searchEnabled: true,
shouldSort: false
});
async function loadUsers() {
const response = await fetch('/api/users');
const users = await response.json();
choices.setChoices(
users,
'id',
'name',
true
);
}
loadUsers();
setChoicesМетод принимает:
choices.setChoices(data, valueKey, labelKey, replaceChoices);
| Аргумент | Назначение |
|---|---|
data |
массив объектов |
valueKey |
поле значения |
labelKey |
отображаемый текст |
replaceChoices |
очистка старых элементов |
Choices.js поддерживает реализацию серверного поиска.
const choices = new Choices('#products', {
searchEnabled: true,
searchChoices: false
});
const input = choices.input.element;
input.addEventListener('input', async (event) => {
const query = event.target.value;
const response = await fetch(`/api/products?q=${query}`);
const products = await response.json();
await choices.clearChoices();
choices.setChoices(products, 'id', 'title', true);
});
Важно учитывать:
Без debounce приложение начинает отправлять запрос на каждый ввод символа.
function debounce(callback, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}
const searchHandler = debounce(async (value) => {
const response = await fetch(`/api/search?q=${value}`);
const data = await response.json();
choices.clearChoices();
choices.setChoices(data, 'id', 'name', true);
}, 300);
choices.input.element.addEventListener('input', (e) => {
searchHandler(e.target.value);
});
Choices.js напрямую изменяет DOM, поэтому интеграция с React требует контроля жизненного цикла.
import { useEffect, useRef } fr om 'react';
import Choices from 'choices.js';
function UserSelect() {
const selectRef = useRef(null);
const choicesRef = useRef(null);
useEffect(() => {
choicesRef.current = new Choices(selectRef.current, {
removeItemButton: true
});
return () => {
choicesRef.current.destroy();
};
}, []);
return (
<sel ect ref={selectRef}>
<option value="1">Admin</option>
<option value="2">Editor</option>
</select>
);
}
React использует однонаправленный поток данных, тогда как Choices.js управляет DOM самостоятельно.
Для синхронизации необходимо использовать события:
useEffect(() => {
const instance = new Choices(selectRef.current);
const handler = (event) => {
setValue(event.detail.value);
};
selectRef.current.addEventListener('change', handler);
return () => {
selectRef.current.removeEventListener('change', handler);
instance.destroy();
};
}, []);
useEffect(() => {
if (!choicesRef.current) return;
choicesRef.current.removeActiveItems();
choicesRef.current.setChoiceByValue(value);
}, [value]);
Такой подход позволяет синхронизировать внутреннее состояние библиотеки с React State.
export default {
mounted() {
this.choices = new Choices(this.$refs.select, {
searchEnabled: true
});
},
beforeUnmount() {
this.choices.destroy();
}
};
mounted() {
this.choices = new Choices(this.$refs.select);
this.$refs.select.addEventListener('change', (event) => {
this.modelValue = event.detail.value;
});
}
В Angular чаще всего создают собственную директиву.
import {
Directive,
ElementRef,
OnInit,
OnDestroy
} fr om '@angular/core';
import Choices from 'choices.js';
@Directive({
selector: '[appChoices]'
})
export class ChoicesDirective implements OnInit, OnDestroy {
private choices;
constructor(private el: ElementRef) {}
ngOnInit() {
this.choices = new Choices(this.el.nativeElement);
}
ngOnDestroy() {
this.choices.destroy();
}
}
sel ect.addEventListener('change', (event) => {
store.dispatch({
type: 'SET_CATEGORY',
payload: event.detail.value
});
});
store.subscribe(() => {
const state = store.getState();
choices.removeActiveItems();
choices.setChoiceByValue(state.category);
});
<Field name="country">
{({ field, form }) => (
<select
ref={(ref) => {
if (!ref) return;
const instance = new Choices(ref);
ref.addEventListener('change', (event) => {
form.setFieldValue(
field.name,
event.detail.value
);
});
}}
>
<option value="kz">Kazakhstan</option>
<option value="us">USA</option>
</select>
)}
</Field>
const schema = yup.object({
tags: yup.array()
.min(1)
.required()
});
Choices.js хорошо подходит для массивов тегов, поэтому интеграция с Yup используется особенно часто.
$(document).ready(function() {
const choices = new Choices('#status');
});
$.ajax({
url: '/api/statuses',
success(data) {
choices.setChoices(data, 'id', 'title', true);
}
});
Choices.js не зависит от Bootstrap, но легко адаптируется под него.
const choices = new Choices('#roles', {
classNames: {
containerOuter: 'choices form-control',
containerInner: 'choices__inner'
}
});
Модальные окна часто создают проблемы с фокусом.
const modal = document.getElementById('userModal');
modal.addEventListener('shown.bs.modal', () => {
choices.showDropdown();
});
const choices = new Choices('#tags', {
classNames: {
containerOuter:
'choices border rounded-lg p-2',
item:
'choices__item bg-blue-500 text-white'
}
});
<div
x-data
x-init="
new Choices($refs.select, {
removeItemButton: true
});
"
>
<select x-ref="select"></select>
</div>
<select id="users">
@foreach($users as $user)
<option value="{{ $user->id }}">
{{ $user->name }}
</option>
@endforeach
</select>
new Choices('#users');
async function loadUsers() {
const response = await fetch('/api/users');
const users = await response.json();
choices.setChoices(users, 'id', 'name', true);
}
<select id="categories">
{% for category in categories %}
<option value="{{ category.id }}">
{{ category.title }}
</option>
{% endfor %}
</select>
wp_enqueue_script(
'choices-js',
'https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js',
[],
null,
true
);
document.addEventListener('DOMContentLoaded', () => {
new Choices('#categories');
});
document.addEventListener('DOMContentLoaded', () => {
new Choices('.js-select');
});
import Choices fr om 'choices.js';
let choices: Choices;
choices = new Choices('#users');
interface User {
id: number;
name: string;
}
const users: User[] = await response.json();
choices.setChoices(users, 'id', 'name', true);
const { data } = await apolloClient.query({
query: GET_USERS
});
choices.setChoices(
data.users,
'id',
'name',
true
);
socket.onmess age = (event) => {
const data = JSON.parse(event.data);
choices.setChoices(data, 'id', 'name', true);
};
async function loadCachedUsers() {
const users = await db.users.toArray();
choices.setChoices(users, 'id', 'name', true);
}
sel ect.addEventListener('change', (event) => {
localStorage.setItem(
'selected_role',
event.detail.value
);
});
const saved = localStorage.getItem('selected_role');
if (saved) {
choices.setChoiceByValue(saved);
}
select.addEventListener('change', (event) => {
analytics.track('role_changed', {
value: event.detail.value
});
});
select.addEventListener('addItem', (event) => {
gtag('event', 'select_item', {
item: event.detail.value
});
});
form.addEventListener('submit', (event) => {
const value = choices.getValue(true);
if (!value.length) {
event.preventDefault();
}
});
choices.containerOuter.element.classList.add('is-invalid');
new Sortable(
choices.choiceList.element,
{
animation: 150
}
);
При больших объёмах данных стандартный рендер может создавать проблемы производительности.
Choices.js зависит от DOM API:
window
document
HTMLElement
Поэтому библиотека должна инициализироваться только на клиенте.
if (typeof window !== 'undefined') {
new Choices('#users');
}
onMounted(() => {
new Choices('#countries');
});
import dynamic fr om 'next/dynamic';
const ChoicesComponent = dynamic(
() => import('./ChoicesComponent'),
{
ssr: false
}
);
Choices.js генерирует множество пользовательских событий.
| Событие | Описание |
|---|---|
addItem |
добавление элемента |
removeItem |
удаление |
change |
изменение значения |
search |
поиск |
showDropdown |
открытие списка |
hideDropdown |
закрытие |
select.addEventListener('addItem', (event) => {
console.log(event.detail);
});
const choices = new Choices('#users', {
callbackOnCreateTemplates(template) {
return {
item(classNames, data) {
return template(`
<div class="${classNames.item}">
<strong>${data.label}</strong>
</div>
`);
}
};
}
});
if (!user.isAdmin) {
choices.disable();
}
if (permissions.includes('edit_categories')) {
choices.enable();
}
new Choices('#countries', {
loadingText: 'Загрузка...',
noResultsText: 'Ничего не найдено',
noChoicesText: 'Нет вариантов'
});
В архитектуре микрофронтендов важно:
class UserSelect extends HTMLElement {
connectedCallback() {
const select = this.shadowRoot.querySelector('select');
this.choices = new Choices(select);
}
disconnectedCallback() {
this.choices.destroy();
}
}
const filtered = data.filter(item => {
return permissions.includes(item.permission);
});
choices.setChoices(filtered, 'id', 'name', true);
document.addEventListener('DOMContentLoaded', () => {
new Choices('#projects');
});
Choices.js хорошо работает внутри Electron благодаря полной поддержке DOM API.
При использовании на мобильных устройствах важно учитывать:
Во время сборки обычно проверяют:
test('choices initializes', () => {
document.body.innerHTML =
'<select id="users"></select>';
const choices = new Choices('#users');
expect(choices).toBeDefined();
});
cy.get('.choices').click();
cy.get('.choices__item')
.contains('Admin')
.click();
select.addEventListener('change', (event) => {
logger.info('Selection changed', {
value: event.detail.value
});
});
select.addEventListener('change', async () => {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify({
value: choices.getValue(true)
})
});
});
countrySelect.addEventListener('change', async (e) => {
const country = e.detail.value;
const response = await fetch(
`/api/cities?country=${country}`
);
const cities = await response.json();
cityChoices.clearChoices();
cityChoices.setChoices(
cities,
'id',
'name',
true
);
});
export class ChoicesAdapter {
constructor(selector, options = {}) {
this.instance = new Choices(selector, options);
}
setData(data) {
this.instance.setChoices(
data,
'id',
'name',
true
);
}
destroy() {
this.instance.destroy();
}
}
Подобный слой абстракции особенно полезен в крупных корпоративных приложениях, где Choices.js используется сразу в нескольких подсистемах.