Choices.js проектируется как библиотека, отделяющая ядро управления состоянием выбора от конкретных источников данных и UI-окружений. Центральная идея — возможность подключения внешних слоёв через адаптеры и обёртки, которые преобразуют входные данные, события и взаимодействие DOM в унифицированный формат, понятный ядру.
Внутри системы выделяются три ключевых слоя:
Такое разделение позволяет использовать Choices.js в разных окружениях без изменения внутренней логики.
Адаптеры отвечают за нормализацию входных данных и событий. Основная задача — сделать так, чтобы ядро работало с единым интерфейсом, независимо от того, откуда приходят данные:
Адаптер выступает промежуточным преобразователем:
Внешний формат → унифицированный формат Choices.js
Ключевой принцип — отсутствие прямых зависимостей ядра от источника данных.
Наиболее распространённый тип — адаптер данных. Он отвечает за загрузку и преобразование опций.
Типовой интерфейс включает:
fetch() — получение данныхtransform() — приведение к внутреннему форматуcache() — кэширование результатовfilter() — предварительная фильтрацияПример логики трансформации:
function transform(items) {
return items.map(item => ({
value: item.id,
label: item.name,
selected: false,
disabled: false
}));
}
Choices.js ожидает строгую структуру:
value — уникальный идентификаторlabel — отображаемый текстselected — состояние выбораdisabled — доступность опцииАдаптер гарантирует соответствие этому контракту.
Одной из ключевых возможностей является поддержка асинхронных данных. Для этого используются адаптеры, реализующие стратегию отложенной загрузки.
Типовая схема:
Пример абстракции:
class AsyncAdapter {
constructor(fetcher) {
this.fetcher = fetcher;
this.cache = new Map();
}
async search(query) {
if (this.cache.has(query)) {
return this.cache.get(query);
}
const result = await this.fetcher(query);
const transformed = result.map(item => ({
value: item.id,
label: item.title
}));
this.cache.set(query, transformed);
return transformed;
}
}
Особенность такого подхода — отделение логики получения данных от логики отображения.
Choices.js часто применяется как замена стандартным
<select> и <input> элементам. Для
этого используется DOM-wrapper, который:
Основные задачи обёртки:
input, change,
keydownСхема взаимодействия:
DOM → Wrapper → Core → Wrapper → DOM
Обёртка не содержит бизнес-логики, она только транслирует события.
Интеграция с React требует создания контролируемого компонента, который связывает жизненный цикл React с внутренним состоянием Choices.js.
Ключевые элементы:
useEffectuseRefПример логики:
function ChoicesSelect({ options, value, onChange }) {
const ref = useRef(null);
const instance = useRef(null);
useEffect(() => {
instance.current = new Choices(ref.current, {
choices: options
});
instance.current.passedElement.element.addEventListener(
'change',
(e) => onChange(e.detail.value)
);
return () => instance.current.destroy();
}, []);
useEffect(() => {
if (instance.current) {
instance.current.setChoices(options, 'value', 'label', true);
}
}, [options]);
useEffect(() => {
if (instance.current) {
instance.current.setChoiceByValue(value);
}
}, [value]);
return <select ref={ref} />;
}
Здесь обёртка решает проблему синхронизации двух моделей состояния: React state и Choices state.
Во Vue адаптация строится вокруг реактивных данных и lifecycle hooks.
Основная проблема — предотвращение циклических обновлений между Vue и Choices.js.
Стратегия:
mountedbeforeUnmountПример логики:
export default {
props: ['options', 'value'],
mounted() {
this.instance = new Choices(this.$refs.select);
this.syncOptions();
this.syncValue();
},
watch: {
options() {
this.syncOptions();
},
value() {
this.syncValue();
}
},
methods: {
syncOptions() {
this.instance.setChoices(this.options, 'value', 'label', true);
},
syncValue() {
this.instance.setChoiceByValue(this.value);
}
}
};
В Angular подход отличается строгой типизацией и DI-контейнером.
Часто используется комбинация:
ControlValueAccessor для формыСхема:
Angular Form → ControlValueAccessor → Choices Instance → DOM
Контракт ControlValueAccessor позволяет интегрировать
Choices.js в reactive forms.
Ключевые методы:
writeValueregisterOnChangeregisterOnTouchedФасад (Facade) используется для упрощения работы с несколькими адаптерами одновременно.
Он скрывает:
Фасад предоставляет единый API:
setOptions()setValue()getValue()search()Пример структуры:
class ChoicesFacade {
constructor(instance, adapter) {
this.instance = instance;
this.adapter = adapter;
}
async search(query) {
const results = await this.adapter.search(query);
this.instance.setChoices(results, 'value', 'label', true);
}
}
В реальных системах данные редко приходят в формате, совместимом с Choices.js. Поэтому адаптеры часто выполняют сложные преобразования:
Пример трансформации:
function normalize(data) {
return data.items.map(item => ({
value: item.meta.uuid,
label: `${item.profile.firstName} ${item.profile.lastName}`
}));
}
Адаптер становится слоем бизнес-логики, но не должен влиять на UI-логику.
Для асинхронных источников критично снижение количества запросов. В адаптерах используется кэширование:
Стратегии:
Пример TTL-логики:
class Cache {
constructor(ttl = 30000) {
this.ttl = ttl;
this.store = new Map();
}
set(key, value) {
this.store.set(key, {
value,
time: Date.now()
});
}
get(key) {
const entry = this.store.get(key);
if (!entry) return null;
if (Date.now() - entry.time > this.ttl) {
this.store.delete(key);
return null;
}
return entry.value;
}
}
Адаптеры часто подписываются на события ядра:
addItemremoveItemsearchhighlightИ наоборот — транслируют внешние события внутрь системы.
Это создаёт двусторонний поток данных:
UI → Adapter → Core → Adapter → UI
Основная сложность — предотвращение рекурсивных обновлений.
Choices.js допускает создание полностью кастомных адаптеров, если стандартные механизмы недостаточны.
Кастомный адаптер может:
Пример комбинированного адаптера:
class MultiSourceAdapter {
constructor(sources) {
this.sources = sources;
}
async search(query) {
const results = await Promise.all(
this.sources.map(s => s.search(query))
);
return results.flat();
}
}
Несмотря на гибкость, существуют ограничения:
Адаптерная архитектура требует дисциплины: любое отклонение от унифицированного интерфейса приводит к расхождению состояния между слоями.