При использовании нескольких экземпляров Slim Select поверх связанных
<select>-элементов возникает задача поддержания
согласованного состояния между ними. Состояние выбора может
дублироваться, трансформироваться или частично зависеть от другого
инстанса. Основной принцип реализации синхронизации заключается в
наличии единого источника данных и управляемом распространении изменений
между экземплярами без возникновения циклических обновлений.
Внутренне Slim Select оперирует массивом выбранных значений, а также набором доступных опций. Любое изменение выбора инициирует событие обновления, которое используется как триггер для синхронизации других компонентов.
Типовая схема синхронизации строится вокруг централизованного состояния:
Состояние обычно представлено структурой:
const sharedState = {
country: null,
state: null,
city: null
};
Каждый экземпляр Slim Select получает привязку к этому состоянию через callback изменения.
Наиболее простой сценарий — зеркалирование значений между двумя селектами.
const selectA = new SlimSelect({
select: '#selectA',
events: {
afterChange: (info) => {
syncFromA(info);
}
}
});
const selectB = new SlimSelect({
select: '#selectB',
events: {
afterChange: (info) => {
syncFromB(info);
}
}
});
Механизм синхронизации:
let lockA = false;
let lockB = false;
function syncFromA(info) {
if (lockA) return;
lockB = true;
selectB.setSelected(info.map(i => i.value));
lockB = false;
}
function syncFromB(info) {
if (lockB) return;
lockA = true;
selectA.setSelected(info.map(i => i.value));
lockA = false;
}
Использование флагов блокировки предотвращает зацикливание обновлений, возникающее при взаимной подписке.
При усложнении логики применяется централизованный стор, который выступает единственным источником правды.
const store = {
value: null,
subscribers: new Set(),
setValue(val) {
this.value = val;
this.notify();
},
notify() {
this.subscribers.forEach(fn => fn(this.value));
},
subscribe(fn) {
this.subscribers.add(fn);
}
};
Подключение Slim Select к стору:
const selectA = new SlimSelect({
select: '#a',
events: {
afterChange: (info) => {
store.setValue(info.map(i => i.value));
}
}
});
const selectB = new SlimSelect({
select: '#b',
events: {
afterChange: (info) => {
store.setValue(info.map(i => i.value));
}
}
});
Подписка экземпляров на обновления:
let internalUpdate = false;
store.subscribe((value) => {
internalUpdate = true;
selectA.setSelected(value);
selectB.setSelected(value);
internalUpdate = false;
});
Исключение повторной эмиссии событий достигается через флаг
internalUpdate.
Часто используется иерархическая модель: страна → регион → город. В этом случае изменения одного уровня влияют на доступные значения следующего.
const countrySelect = new SlimSelect({
select: '#country',
events: {
afterChange: (info) => updateStates(info[0]?.value)
}
});
const stateSelect = new SlimSelect({
select: '#state',
events: {
afterChange: (info) => updateCities(info[0]?.value)
}
});
Обновление зависимых списков:
function updateStates(country) {
const states = getStatesByCountry(country);
stateSelect.setData(states);
stateSelect.setSelected(null);
citySelect.setData([]);
citySelect.setSelected(null);
}
function updateCities(state) {
const cities = getCitiesByState(state);
citySelect.setData(cities);
citySelect.setSelected(null);
}
Здесь ключевым моментом выступает сброс нижестоящих уровней при изменении вышестоящего значения.
Иногда несколько экземпляров отражают одно и то же состояние, но с разной фильтрацией или отображением.
Пример: один селект показывает все значения, второй — только избранные.
const allSelect = new SlimSelect({
select: '#all',
events: {
afterChange: (info) => updateFavorites(info)
}
});
const favSelect = new SlimSelect({
select: '#fav',
events: {
afterChange: (info) => updateAll(info)
}
});
Фильтрация состояния:
let state = {
all: [],
fav: []
};
function updateFavorites(info) {
state.fav = info.map(i => i.value);
favSelect.setSelected(state.fav);
}
function updateAll(info) {
state.all = info.map(i => i.value);
allSelect.setSelected(state.all);
}
При такой модели требуется аккуратное разделение ответственности, чтобы исключить конфликт источников данных.
При взаимной синхронизации наиболее частой проблемой становится
бесконечный цикл вызовов afterChange. Используются
следующие стратегии:
1. Флаги блокировки
let isSyncing = false;
function handleChange(source, target, values) {
if (isSyncing) return;
isSyncing = true;
target.setSelected(values);
isSyncing = false;
}
2. Сравнение состояния перед обновлением
function arraysEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
function safeUpdate(instance, values) {
const current = instance.getSelected().map(i => i.value);
if (!arraysEqual(current, values)) {
instance.setSelected(values);
}
}
3. Разделение событий пользователя и программных изменений
let programmatic = false;
selectA = new SlimSelect({
select: '#a',
events: {
afterChange: (info) => {
if (programmatic) return;
sync(info);
}
}
});
function sync(info) {
programmatic = true;
selectB.setSelected(info.map(i => i.value));
programmatic = false;
}
При загрузке данных из API возникает дополнительная сложность: экземпляры могут обновляться не одновременно.
async function loadOptions() {
const res = await fetch('/api/options');
const data = await res.json();
selectA.setData(data);
selectB.setData(data);
synchronizeInitialValue();
}
Согласование начального состояния:
function synchronizeInitialValue() {
const initial = sharedState.value;
if (initial) {
selectA.setSelected(initial);
selectB.setSelected(initial);
}
}
Особое внимание требуется уделять моменту, когда события ещё не подписаны, но данные уже применяются.
При большом количестве селектов используется модель регистрации:
const registry = [];
function register(instance) {
registry.push(instance);
}
Обновление всех экземпляров:
function broadcast(value, origin) {
registry.forEach(instance => {
if (instance !== origin) {
instance.setSelected(value);
}
});
}
Привязка:
const selectA = new SlimSelect({
select: '#a',
events: {
afterChange: (info) => broadcast(info.map(i => i.value), selectA)
}
});
register(selectA);
В некоторых случаях наборы опций различаются, и требуется пересечение значений.
function intersectValues(sourceValues, targetInstance) {
const available = targetInstance.data.map(i => i.value);
return sourceValues.filter(v => available.includes(v));
}
Применение:
function syncPartial(values, target) {
const valid = intersectValues(values, target);
target.setSelected(valid);
}
При сложных связях важно учитывать последовательность обновления экземпляров:
Пример очередности:
function updateChain(value) {
updateA(value);
requestAnimationFrame(() => {
updateB(value);
setTimeout(() => {
updateC(value);
}, 0);
});
}
Такой подход предотвращает блокировку UI и обеспечивает предсказуемость состояния.
Стабильная синхронизация нескольких Slim Select достигается комбинацией следующих принципов: