Работа с несколькими экземплярами селекторов в интерфейсе часто требует согласованного поведения между ними. Choices.js предоставляет механизм управления состоянием через API и события, но не навязывает встроенную синхронизацию. Координация нескольких селектов реализуется на уровне бизнес-логики приложения, где каждый экземпляр рассматривается как независимый контролируемый компонент с возможностью внешнего обновления состояния.
Каждый селект Choices.js инкапсулирует собственное состояние: выбранные элементы, доступные опции, фильтры поиска и внутренний кэш. При синхронизации нескольких экземпляров вводится общий слой управления состоянием, который выполняет роль посредника между ними.
Типовая модель включает:
Ключевым моментом становится разделение источника истины: либо один селект является главным, либо состояние выносится в отдельную структуру.
Choices.js генерирует события при изменении выбора. Основное событие
— change, а также более детализированные события вроде
добавления или удаления элемента.
Использование событий позволяет отслеживать изменения и распространять их на другие экземпляры.
const selectA = new Choices('#selectA');
const selectB = new Choices('#selectB');
function syncFromA(event) {
const value = event.detail.value;
selectB.setChoiceByValue(value);
}
function syncFromB(event) {
const value = event.detail.value;
selectA.setChoiceByValue(value);
}
document.querySelector('#selectA').addEventListener('change', syncFromA);
document.querySelector('#selectB').addEventListener('change', syncFromB);
Такой подход создаёт двунаправленную связь, однако требует контроля циклических обновлений.
При зеркальной синхронизации возникает риск зацикливания: изменение одного селекта вызывает обновление второго, которое снова триггерит событие первого.
Для предотвращения используется флаг блокировки:
let isSyncing = false;
const selectA = new Choices('#selectA');
const selectB = new Choices('#selectB');
function handleA(event) {
if (isSyncing) return;
isSyncing = true;
selectB.setChoiceByValue(event.detail.value);
isSyncing = false;
}
function handleB(event) {
if (isSyncing) return;
isSyncing = true;
selectA.setChoiceByValue(event.detail.value);
isSyncing = false;
}
Флаг выполняет роль простейшего mutex-механизма, блокируя повторную реакцию на программные изменения.
Более стабильная архитектура строится на принципе единственного источника изменений. Один селект становится primary, остальные — зависимыми.
const primary = new Choices('#primary');
const secondary = new Choices('#secondary');
const tertiary = new Choices('#tertiary');
function propagate(event) {
const value = event.detail.value;
secondary.setChoiceByValue(value);
tertiary.setChoiceByValue(value);
}
primary.passedElement.element.addEventListener('change', propagate);
В такой модели отсутствует обратная связь, что исключает циклические обновления и упрощает отладку.
При использовании multi-select поведение усложняется: значения представляют массив, а не одиночное значение. В этом случае синхронизация должна учитывать полный набор выбранных элементов.
function syncMulti(source, targets) {
const values = source.getValue(true);
targets.forEach(t => {
t.removeActiveItems();
t.setChoiceByValue(values);
});
}
Метод getValue(true) возвращает массив значений, что
позволяет полностью перезаписывать состояние зависимых селектов.
Choices.js предоставляет методы для прямого изменения состояния без пользовательского взаимодействия:
setChoiceByValuesetValueremoveActiveItemsclearStoreКомбинация этих методов используется для полной репликации состояния между экземплярами.
function applyState(target, values) {
target.removeActiveItems();
target.setChoiceByValue(values);
}
При сложной синхронизации предпочтительнее пересобирать состояние целиком, а не пытаться дифференцировать изменения.
При увеличении количества связанных селектов локальные обработчики событий становятся недостаточными. Вводится централизованное состояние.
const store = {
value: []
};
const instances = [
new Choices('#a'),
new Choices('#b'),
new Choices('#c')
];
function updateStore(values) {
store.value = values;
instances.forEach(instance => {
instance.removeActiveItems();
instance.setChoiceByValue(values);
});
}
Теперь любой селект обновляет store, а store распространяет изменения обратно.
instances.forEach(instance => {
instance.passedElement.element.addEventListener('change', (e) => {
updateStore(e.detail.value);
});
});
Такая модель приближена к реактивной архитектуре.
При загрузке опций из внешнего источника (API) синхронизация усложняется из-за задержек и возможной несогласованности состояния.
async function loadOptions(instance, url) {
const response = await fetch(url);
const data = await response.json();
instance.setChoices(data, 'value', 'label', true);
}
После загрузки данных необходимо повторно применить текущее состояние store:
function resyncAll(instances, values) {
instances.forEach(instance => {
instance.removeActiveItems();
instance.setChoiceByValue(values);
});
}
В некоторых случаях значения между селектами не совпадают напрямую. Требуется маппинг:
const mapAtoB = {
'1': 'a',
'2': 'b',
'3': 'c'
};
function transform(values) {
return values.map(v => mapAtoB[v]).filter(Boolean);
}
Использование трансформеров позволяет связывать селекты с различной структурой данных без изменения внутренней логики Choices.js.
При большом количестве связанных экземпляров частые обновления приводят к лишним перерендериваниям DOM. Оптимизация строится на пакетной обработке изменений.
let pending = false;
let queuedValue = null;
function scheduleUpdate(values) {
queuedValue = values;
if (pending) return;
pending = true;
requestAnimationFrame(() => {
instances.forEach(i => {
i.removeActiveItems();
i.setChoiceByValue(queuedValue);
});
pending = false;
});
}
Использование requestAnimationFrame позволяет
сгруппировать изменения в один цикл отрисовки.
Choices.js часто используется вместе с глобальными state-менеджерами. В этом случае экземпляры становятся подписчиками состояния.
store.subscribe((values) => {
instances.forEach(i => {
i.removeActiveItems();
i.setChoiceByValue(values);
});
});
Любое изменение централизованного состояния автоматически отражается в UI, исключая необходимость прямого взаимодействия между экземплярами.
При одновременном изменении нескольких селектов возможны конфликты. Решение основывается на стратегии разрешения:
Пример стратегии последнего изменения:
let lastTimestamp = 0;
function update(values, timestamp) {
if (timestamp < lastTimestamp) return;
lastTimestamp = timestamp;
instances.forEach(i => {
i.removeActiveItems();
i.setChoiceByValue(values);
});
}
Для упрощения поддержки логика синхронизации выносится в отдельный модуль, который управляет всеми экземплярами.
class ChoicesSync {
constructor(instances) {
this.instances = instances;
this.lock = false;
}
propagate(source, values) {
if (this.lock) return;
this.lock = true;
this.instances.forEach(instance => {
if (instance !== source) {
instance.removeActiveItems();
instance.setChoiceByValue(values);
}
});
this.lock = false;
}
}
Такой подход унифицирует поведение и уменьшает дублирование кода в обработчиках событий.
В сложных формах значения селектов могут пересекаться только частично. В этом случае применяется фильтрация:
function intersect(valuesA, valuesB) {
return valuesA.filter(v => valuesB.includes(v));
}
Далее пересечение применяется к каждому экземпляру перед обновлением состояния.
При построении сложных интерфейсов Choices.js часто интегрируется с реактивными системами. В таких системах синхронизация становится побочным эффектом изменения состояния, а не отдельной логикой.
Состояние рассматривается как функция отображения:
UI = f(state)
Каждый экземпляр Choices.js выступает как проекция одного и того же состояния, что исключает необходимость ручного управления связями между ними.