Синхронизация нескольких селектов

Работа с несколькими экземплярами селекторов в интерфейсе часто требует согласованного поведения между ними. Choices.js предоставляет механизм управления состоянием через API и события, но не навязывает встроенную синхронизацию. Координация нескольких селектов реализуется на уровне бизнес-логики приложения, где каждый экземпляр рассматривается как независимый контролируемый компонент с возможностью внешнего обновления состояния.


Архитектура взаимодействия между экземплярами

Каждый селект Choices.js инкапсулирует собственное состояние: выбранные элементы, доступные опции, фильтры поиска и внутренний кэш. При синхронизации нескольких экземпляров вводится общий слой управления состоянием, который выполняет роль посредника между ними.

Типовая модель включает:

  • набор экземпляров Choices
  • общий источник состояния (store)
  • обработчики событий изменений
  • механизм программного обновления состояния

Ключевым моментом становится разделение источника истины: либо один селект является главным, либо состояние выносится в отдельную структуру.


Базовая синхронизация через события

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 предоставляет методы для прямого изменения состояния без пользовательского взаимодействия:

  • setChoiceByValue
  • setValue
  • removeActiveItems
  • clearStore

Комбинация этих методов используется для полной репликации состояния между экземплярами.

function applyState(target, values) {
  target.removeActiveItems();
  target.setChoiceByValue(values);
}

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


Общий store как центр синхронизации

При увеличении количества связанных селектов локальные обработчики событий становятся недостаточными. Вводится централизованное состояние.

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, исключая необходимость прямого взаимодействия между экземплярами.


Обработка конфликтов состояния

При одновременном изменении нескольких селектов возможны конфликты. Решение основывается на стратегии разрешения:

  • last write wins
  • priority-based update
  • merge strategy

Пример стратегии последнего изменения:

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 выступает как проекция одного и того же состояния, что исключает необходимость ручного управления связями между ними.