Синхронизация нескольких экземпляров 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 и исключает лишние пересчёты состояния.
Часто встречается структура, где один селект определяет доступные значения другого. Это характерно для фильтров по категориям:
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);
Сортировка массива позволяет избежать ложных изменений при одинаковых наборах данных в разном порядке.
При частых изменениях значений, например при поиске или асинхронной фильтрации, применяется задержка:
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-обновлений, нормализация данных и контроль асинхронных потоков. При увеличении количества связанных компонентов центральное управление состоянием становится предпочтительной архитектурой, минимизирующей сложность взаимных зависимостей.