Choices.js предоставляет систему шаблонов, позволяющую полностью переопределять отображение элементов выпадающего списка (dropdown), выбранных значений и групп. Механизм основан на наборе функций, возвращающих HTML-строки, которые библиотека вставляет в DOM без дополнительной логики рендера. Основная цель — дать полный контроль над визуальной структурой и поведением элементов без необходимости форка или модификации ядра.
Внутренне Choices.js разделяет визуализацию на несколько независимых слоёв:
Каждый слой формируется через функцию-шаблон, возвращающую
HTML-строку. Эти функции передаются через параметр
callbackOnCreateTemplates или через объект
templates (в зависимости от версии библиотеки).
Основной объект шаблонов задаётся при инициализации:
const instance = new Choices('#select', {
templates: {
item: (classNames, data) => {},
choice: (classNames, data) => {},
group: (classNames, data) => {},
loading: (classNames) => {},
noResults: (classNames) => {},
noChoices: (classNames) => {}
}
});
Каждая функция получает:
classNames — объект с CSS-классами библиотекиdata — объект текущего элемента (если применимо)choice определяет, как отображается элемент внутри
выпадающего списка.
Структура данных data:
id — идентификаторvalue — значениеlabel — текстselected — выбран ли элементdisabled — недоступен лиcustomProperties — дополнительные данныеПример базового переопределения:
const instance = new Choices('#select', {
templates: {
choice: (classNames, data) => {
return `
<div class="${classNames.item} ${classNames.itemChoice}"
data-choice
data-id="${data.id}"
data-value="${data.value}"
${data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'}>
${data.label}
</div>
`;
}
}
});
data-choicedata-id и
data-valuedata-choice-selectable определяет
кликабельностьaria-disabledChoices.js использует эти атрибуты для управления логикой выбора, поэтому их отсутствие ломает поведение компонента.
item управляет отображением выбранных значений внутри
инпута.
templates: {
item: (classNames, data) => {
return `
<div class="${classNames.item} ${data.highlighted ? classNames.highlightedState : classNames.itemSelectable}"
data-item
data-id="${data.id}"
data-value="${data.value}">
<span class="item-label">${data.label}</span>
<button type="button"
class="item-remove"
data-button="remove-item">
×
</button>
</div>
`;
}
}
data-item обязателен для обработки удаленияdata-button="remove-item" связывает кнопку с внутренним
обработчикомhighlighted используется при навигации клавиатуройГруппы используются при grouped options, когда данные
структурированы:
{
label: 'Fruits',
id: 1,
disabled: false,
choices: [...]
}
Переопределение:
templates: {
group: (classNames, data) => {
return `
<div class="${classNames.group}">
<div class="${classNames.groupHeading}">
${data.label}
</div>
<div class="${classNames.groupChoices}">
${data.children}
</div>
</div>
`;
}
}
data.children уже содержит сгенерированные
choice-элементыОтображается при отсутствии совпадений:
templates: {
noResults: (classNames) => {
return `
<div class="${classNames.noResults}">
Ничего не найдено
</div>
`;
}
}
Используется, когда список пуст:
noChoices: (classNames) => {
return `
<div class="${classNames.noChoices}">
Данные отсутствуют
</div>
`;
}
Состояние загрузки при async-режиме:
loading: (classNames) => {
return `
<div class="${classNames.item} ${classNames.loading}">
Загрузка...
</div>
`;
}
customProperties позволяет расширять данные элементов
без изменения API:
{
value: 'paris',
label: 'Paris',
customProperties: {
country: 'France',
population: 2148000
}
}
Использование в шаблоне:
choice: (classNames, data) => {
return `
<div class="${classNames.item}" data-choice data-id="${data.id}">
<div>${data.label}</div>
<small>${data.customProperties.country}</small>
</div>
`;
}
Choices.js не выполняет автоматическую очистку HTML внутри шаблонов. Любые вставки:
data.labeldata.valuecustomPropertiesмогут содержать потенциально опасный HTML.
Типовая стратегия защиты:
const escapeHtml = (str) => {
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
};
Использование:
label: escapeHtml(data.label)
Шаблоны часто комбинируются с состояниями:
activehighlightedselecteddisabledПример комбинированной логики:
choice: (classNames, data) => {
const classes = [
classNames.item,
classNames.itemChoice,
data.selected ? classNames.selectedState : '',
data.disabled ? classNames.disabledState : ''
].join(' ');
return `
<div class="${classes}"
data-choice
data-id="${data.id}"
data-value="${data.value}">
${data.label}
</div>
`;
}
Для сложных интерфейсов шаблоны выносятся в фабрики:
const createChoiceTemplate = (iconMap) => {
return (classNames, data) => {
return `
<div class="${classNames.item}" data-choice data-id="${data.id}">
<span class="icon">${iconMap[data.value] || ''}</span>
<span>${data.label}</span>
</div>
`;
};
};
Использование:
templates: {
choice: createChoiceTemplate({
apple: '?',
banana: '?'
})
}
При загрузке данных через API шаблоны часто комбинируются с
состоянием loading и noResults.
Пример сценария:
new Choices('#select', {
searchEnabled: true,
shouldSort: false,
templates: {
loading: (classNames) => `
<div class="${classNames.loading}">
Загрузка данных...
</div>
`,
noResults: (classNames) => `
<div class="${classNames.noResults}">
Совпадения отсутствуют
</div>
`
}
});
Использование сложных шаблонов влияет на:
Критические факторы:
Оптимизированный подход:
Механизм шаблонов не поддерживает:
Каждое обновление списка приводит к полной перегенерации HTML-строк для отображаемой части dropdown, что требует аккуратного проектирования сложных UI-расширений.