Синхронизация между селектами

Синхронизация нескольких экземпляров Tom Select строится вокруг управления состоянием значений, событий изменения и предотвращения циклических обновлений. В типичных интерфейсах встречаются сценарии, где один селект влияет на другой: каскадные фильтры, дублирующиеся поля, мастер-детальные формы, распределённый выбор одного и того же набора данных.

Основной механизм взаимодействия базируется на API методов setValue, addItem, clear, а также на событиях change, item_add, item_remove. При проектировании синхронизации ключевым аспектом становится контроль за повторными вызовами обработчиков.


Базовая модель связки экземпляров

Минимальная схема синхронизации двух селектов строится на одностороннем обновлении:

const selectA = new TomSelect('#select-a');
const selectB = new TomSelect('#select-b');

selectA.on('change', (value) => {
  selectB.setValue(value);
});

В таком варианте любое изменение первого селекта полностью перезаписывает состояние второго. Однако подобная реализация приводит к рекурсивным вызовам, если оба селекта связаны взаимно.


Защита от циклических обновлений

При двусторонней синхронизации возникает проблема бесконечного цикла событий. Для её устранения используется флаг блокировки состояния:

let isSyncing = false;

const selectA = new TomSelect('#select-a');
const selectB = new TomSelect('#select-b');

selectA.on('change', (value) => {
  if (isSyncing) return;

  isSyncing = true;
  selectB.setValue(value);
  isSyncing = false;
});

selectB.on('change', (value) => {
  if (isSyncing) return;

  isSyncing = true;
  selectA.setValue(value);
  isSyncing = false;
});

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


Синхронизация без генерации событий

Tom Select позволяет выполнять обновления без вызова событий, что особенно полезно при сложных связках:

selectA.on('change', (value) => {
  selectB.setValue(value, true);
});

Второй аргумент silent подавляет события, предотвращая каскадные вызовы обработчиков.

Аналогичный подход используется при массовых обновлениях:

selectB.setValue(['a', 'b', 'c'], true);

Это снижает нагрузку на DOM и исключает лишние пересчёты состояния.


Каскадная синхронизация (master-detail)

Часто встречается структура, где один селект определяет доступные значения другого. Это характерно для фильтров по категориям:

const category = new TomSelect('#category');
const product = new TomSelect('#product');

const productsByCategory = {
  fruits: [
    { value: 'apple', text: 'Apple' },
    { value: 'banana', text: 'Banana' }
  ],
  vegetables: [
    { value: 'carrot', text: 'Carrot' },
    { value: 'cabbage', text: 'Cabbage' }
  ]
};

category.on('change', (value) => {
  const options = productsByCategory[value] || [];

  product.clear(true);
  product.clearOptions();

  options.forEach(opt => product.addOption(opt));

  product.refreshOptions(false);
});

В данной модели второй селект полностью пересобирается при изменении первого. Использование clear(true) предотвращает лишние события, а refreshOptions(false) обновляет интерфейс без всплеска перерисовок.


Двусторонняя синхронизация с нормализацией данных

При работе с двумя идентичными селектами требуется приведение значений к единому формату. Особенно это актуально при множественном выборе:

const selectA = new TomSelect('#a');
const selectB = new TomSelect('#b');

const normalize = (value) => {
  if (Array.isArray(value)) return value.sort();
  return value;
};

let lock = false;

function sync(source, target) {
  source.on('change', (value) => {
    if (lock) return;

    lock = true;
    target.setValue(normalize(value), true);
    lock = false;
  });
}

sync(selectA, selectB);
sync(selectB, selectA);

Сортировка массива позволяет избежать ложных изменений при одинаковых наборах данных в разном порядке.


Синхронизация с задержкой (debounce)

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

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const selectA = new TomSelect('#a');
const selectB = new TomSelect('#b');

const sync = debounce((value) => {
  selectB.setValue(value, true);
}, 300);

selectA.on('change', sync);

Такая модель снижает количество обновлений и предотвращает перегрузку интерфейса при быстром вводе.


Синхронизация с асинхронной загрузкой данных

При использовании load или динамических источников данных синхронизация усложняется из-за задержек ответа:

const selectA = new TomSelect('#a', {
  load(query, callback) {
    fetch(`/api/items?q=${query}`)
      .then(res => res.json())
      .then(data => callback(data));
  }
});

const selectB = new TomSelect('#b');

selectA.on('change', (value) => {
  fetch(`/api/detail?id=${value}`)
    .then(res => res.json())
    .then(data => {
      selectB.clearOptions();
      data.items.forEach(item => selectB.addOption(item));
      selectB.refreshOptions(false);
    });
});

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

let requestId = 0;

selectA.on('change', async (value) => {
  const current = ++requestId;

  const res = await fetch(`/api/detail?id=${value}`);
  const data = await res.json();

  if (current !== requestId) return;

  selectB.clearOptions();
  data.items.forEach(item => selectB.addOption(item));
  selectB.refreshOptions(false);
});

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

При наличии более двух связанных экземпляров применяется централизованный стор состояния:

const state = {
  value: null
};

const selects = [
  new TomSelect('#a'),
  new TomSelect('#b'),
  new TomSelect('#c')
];

let updating = false;

selects.forEach(select => {
  select.on('change', (value) => {
    if (updating) return;

    state.value = value;
    updating = true;

    selects.forEach(s => {
      if (s !== select) {
        s.setValue(value, true);
      }
    });

    updating = false;
  });
});

Такой подход упрощает масштабирование логики синхронизации на произвольное количество компонентов.


Синхронизация с частичным совпадением значений

В сценариях множественного выбора часто требуется пересечение или объединение значений:

const selectA = new TomSelect('#a');
const selectB = new TomSelect('#b');

selectA.on('change', (values) => {
  const filtered = values.filter(v => v.startsWith('a'));
  selectB.setValue(filtered, true);
});

Аналогично можно реализовать пересечение двух множеств:

function intersection(a, b) {
  return a.filter(x => b.includes(x));
}

Изоляция состояния и предотвращение побочных эффектов

При сложных интерфейсах важно разделять пользовательские события и программные обновления. Tom Select не предоставляет встроенного режима “store-only update”, поэтому используется паттерн временного отключения обработчиков:

let disabled = false;

selectA.on('change', (value) => {
  if (disabled) return;
  disabled = true;

  selectB.setValue(value, true);

  disabled = false;
});

Дополнительно применяется разделение источников данных: пользовательские действия обрабатываются отдельно от системных обновлений.


Оптимизация синхронизации

При большом количестве связанных экземпляров критично уменьшать количество DOM-операций. Основные приёмы включают:

  • использование setValue(..., true) для подавления событий
  • пакетное обновление через временное накопление изменений
  • отказ от промежуточных clear при полной перезаписи состояния
  • минимизация вызовов refreshOptions

Поведение при удалении элементов

Удаление элементов в одном селекте должно корректно отражаться в других:

selectA.on('item_remove', (value) => {
  const current = selectB.getValue();
  selectB.setValue(current.filter(v => v !== value), true);
});

Такая модель предотвращает рассинхронизацию при ручном удалении значений пользователем.


Итоговая модель взаимодействия

Синхронизация нескольких Tom Select экземпляров строится как комбинация событийной модели и управляемого состояния. Ключевыми элементами являются блокировка повторных вызовов, использование silent-обновлений, нормализация данных и контроль асинхронных потоков. При увеличении количества связанных компонентов центральное управление состоянием становится предпочтительной архитектурой, минимизирующей сложность взаимных зависимостей.