Мастер-детальные отношения в Tom Select представляют собой модель зависимости одного выпадающего списка от значения другого, где выбор в родительском поле определяет содержимое дочернего. Такой подход используется для построения каскадных форм, фильтров, многоуровневых справочников и интерфейсов, где данные логически связаны и не должны существовать изолированно.
Мастер-детальная структура строится вокруг двух типов селектов:
Ключевая особенность заключается в том, что detail не имеет собственного фиксированного набора опций. Его состояние формируется динамически на основе выбора master.
В контексте Tom Select это достигается через программное управление
опциями (options), очистку значений (clear()),
перезагрузку (clearOptions()) и асинхронное обновление
данных.
Простейшая схема включает два экземпляра Tom Select:
const master = new TomSelect("#country");
const detail = new TomSelect("#city", {
create: false,
placeholder: "Выберите город"
});
Далее добавляется обработчик изменения master:
master.on("change", (value) => {
detail.clear();
detail.clearOptions();
const cities = getCitiesByCountry(value);
detail.addOptions(
cities.map(city => ({
value: city.id,
text: city.name
}))
);
detail.refreshOptions(false);
});
Функция getCitiesByCountry выступает источником
бизнес-логики и может быть как локальной, так и серверной.
В реальных системах данные редко хранятся локально. Чаще используется API-запрос:
master.on("change", async (countryId) => {
detail.clear();
detail.clearOptions();
const response = await fetch(`/api/cities?country=${countryId}`);
const cities = await response.json();
detail.addOptions(
cities.map(c => ({
value: c.id,
text: c.name
}))
);
detail.refreshOptions(false);
});
Здесь критично учитывать состояние загрузки, чтобы избежать неконсистентного UI.
Смена значения master требует полного сброса dependent-селекта:
master.on("change", async (countryId) => {
detail.disable();
detail.clear();
detail.clearOptions();
const cities = await fetchCities(countryId);
detail.addOptions(cities);
detail.refreshOptions(false);
detail.enable();
});
Такой подход предотвращает выбор устаревших данных.
Мастер-детальная модель не ограничивается двумя уровнями. Возможна цепочка:
country.on("change", async (countryId) => {
region.clear();
city.clear();
const regions = await fetch(`/api/regions?country=${countryId}`).then(r => r.json());
region.addOptions(regions);
region.refreshOptions(false);
});
region.on("change", async (regionId) => {
city.clear();
const cities = await fetch(`/api/cities?region=${regionId}`).then(r => r.json());
city.addOptions(cities);
city.refreshOptions(false);
});
Каждый уровень является одновременно master для следующего и detail для предыдущего.
Особая сложность возникает при предзаполненных формах (edit mode). Необходимо восстановить всю цепочку зависимостей:
async function initForm(data) {
await master.setValue(data.country);
const regions = await fetchRegions(data.country);
region.addOptions(regions);
await region.setValue(data.region);
const cities = await fetchCities(data.region);
city.addOptions(cities);
await city.setValue(data.city);
}
Ключевой принцип — последовательная инициализация сверху вниз.
Проблема частых изменений master решается через debounce:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
master.on("change", debounce(async (value) => {
detail.clear();
const data = await fetchData(value);
detail.addOptions(data);
detail.refreshOptions(false);
}, 300));
Это снижает нагрузку на API при быстрых переключениях.
При повторных выборах одного и того же master-значения можно избежать повторных запросов:
const cache = new Map();
async function getCities(countryId) {
if (cache.has(countryId)) {
return cache.get(countryId);
}
const data = await fetch(`/api/cities?country=${countryId}`)
.then(r => r.json());
cache.set(countryId, data);
return data;
}
При сбое загрузки важно не оставлять detail в неконсистентном состоянии:
master.on("change", async (value) => {
try {
detail.disable();
detail.clearOptions();
const data = await fetchData(value);
detail.addOptions(data);
detail.refreshOptions(false);
} catch (e) {
detail.clear();
detail.addOption({ value: "", text: "Ошибка загрузки" });
} finally {
detail.enable();
}
});
Мастер-детальная модель часто используется не только для UI, но и для формирования серверных фильтров:
form.addEventListener("submit", (e) => {
e.preventDefault();
const params = new URLSearchParams({
country: master.getValue(),
city: detail.getValue()
});
fetch(`/api/search?${params}`);
});
Зависимости могут быть не линейными, а условными:
master.on("change", (value) => {
if (value === "remote") {
detail.disable();
} else {
detail.enable();
}
});
Detail может зависеть от нескольких master-селектов:
function updateCities() {
const country = countrySelect.getValue();
const region = regionSelect.getValue();
fetch(`/api/cities?country=${country}®ion=${region}`)
.then(r => r.json())
.then(data => {
citySelect.clearOptions();
citySelect.addOptions(data);
citySelect.refreshOptions(false);
});
}
countrySelect.on("change", updateCities);
regionSelect.on("change", updateCities);
Для улучшения пользовательского опыта применяются состояния загрузки:
detail.disable();
detail.setValue("");
detail.settings.placeholder = "Загрузка...";
detail.refreshOptions(false);
После загрузки placeholder возвращается к нормальному состоянию.
refreshOptions(false)Эти ошибки приводят к рассинхронизации UI и данных.
Корректная модель состояния строится вокруг трех операций:
function resetSelect(select) {
select.clear();
select.clearOptions();
}
async function loadSelect(select, data) {
select.addOptions(data);
select.refreshOptions(false);
}
async function bindSelect(select, value) {
await select.setValue(value);
}
Комбинация этих операций формирует устойчивую каскадную систему зависимостей между селектами.