Система шаблонов в Tom Select позволяет полностью переопределять
внешний вид выпадающего списка, выбранных элементов, заголовков групп,
состояния загрузки, пустых результатов и других частей интерфейса.
Механизм основан на объекте render, внутри которого
определяются функции генерации HTML.
Каждая функция получает данные элемента и функцию
escape, предназначенную для безопасного экранирования
HTML.
Базовый пример кастомизации:
<select id="users" multiple>
<option value="1">Alex</option>
<option value="2">John</option>
<option value="3">Maria</option>
</select>
new TomSelect('#users', {
render: {
option: function(data, escape) {
return `
<div class="user-option">
<strong>${escape(data.text)}</strong>
</div>
`;
},
item: function(data, escape) {
return `
<div class="selected-user">
${escape(data.text)}
</div>
`;
}
}
});
Функция option() отвечает за отображение элемента в
выпадающем списке, а item() — за отображение выбранного
значения внутри поля.
Объект render поддерживает множество шаблонов.
new TomSelect('#select', {
render: {
option: function(data, escape) {},
item: function(data, escape) {},
option_create: function(data, escape) {},
no_results: function(data, escape) {},
loading: function(data, escape) {},
not_loading: function(data, escape) {},
optgroup: function(data) {},
optgroup_header: function(data, escape) {}
}
});
Каждый шаблон отвечает за отдельную часть интерфейса.
| Шаблон | Назначение |
|---|---|
option |
Элемент выпадающего списка |
item |
Выбранный элемент |
option_create |
Кнопка создания нового значения |
no_results |
Сообщение при отсутствии результатов |
loading |
Индикатор загрузки |
not_loading |
Состояние после загрузки |
optgroup |
Контейнер группы |
optgroup_header |
Заголовок группы |
Наиболее часто переопределяемый шаблон.
new TomSelect('#products', {
options: [
{
value: 1,
title: 'MacBook Pro',
price: 2500
},
{
value: 2,
title: 'Dell XPS',
price: 1800
}
],
labelField: 'title',
valueField: 'value',
render: {
option: function(data, escape) {
return `
<div class="product-option">
<div class="title">
${escape(data.title)}
</div>
<div class="price">
$${escape(data.price)}
</div>
</div>
`;
}
}
});
Данные внутри data соответствуют объекту option.
Tom Select удобно использовать для карточек пользователей, товаров, стран и других объектов с визуальным контентом.
new TomSelect('#employees', {
options: [
{
value: 1,
name: 'Alex Morgan',
avatar: '/avatars/alex.jpg',
role: 'Designer'
},
{
value: 2,
name: 'Emma Stone',
avatar: '/avatars/emma.jpg',
role: 'Developer'
}
],
labelField: 'name',
valueField: 'value',
render: {
option: function(data, escape) {
return `
<div class="employee-option">
<img
src="${escape(data.avatar)}"
class="avatar"
>
<div class="info">
<div class="name">
${escape(data.name)}
</div>
<div class="role">
${escape(data.role)}
</div>
</div>
</div>
`;
}
}
});
Шаблон выбранного элемента может отличаться от шаблона option.
new TomSelect('#tags', {
render: {
item: function(data, escape) {
return `
<div class="tag-item">
<span class="tag-text">
${escape(data.text)}
</span>
<span class="tag-remove">
×
</span>
</div>
`;
}
}
});
При использовании множественного выбора шаблон item
особенно важен, поскольку именно он определяет внешний вид тегов.
Tom Select поддерживает сложные HTML-макеты.
render: {
option: function(data, escape) {
return `
<article class="card">
<header class="card-header">
${escape(data.title)}
</header>
<section class="card-body">
${escape(data.description)}
</section>
<footer class="card-footer">
${escape(data.category)}
</footer>
</article>
`;
}
}
Внутри шаблонов допустимо использовать:
Tom Select передаёт функцию escape, которую необходимо
использовать для защиты от XSS.
Небезопасный вариант:
return `
<div>${data.text}</div>
`;
Безопасный вариант:
return `
<div>${escape(data.text)}</div>
`;
Если данные загружаются с сервера, использование
escape() обязательно.
В option-объектах можно хранить произвольные поля.
options: [
{
value: 1,
text: 'Server 1',
ip: '192.168.0.10',
status: 'online'
}
]
Доступ внутри шаблона:
render: {
option: function(data, escape) {
return `
<div class="server-option">
<div>${escape(data.text)}</div>
<div>${escape(data.ip)}</div>
<div>${escape(data.status)}</div>
</div>
`;
}
}
Шаблоны позволяют изменять интерфейс в зависимости от данных.
render: {
option: function(data, escape) {
let statusClass = data.online
? 'online'
: 'offline';
return `
<div class="user ${statusClass}">
${escape(data.name)}
</div>
`;
}
}
render: {
option: function(data, escape) {
let premium = '';
if (data.premium) {
premium = `
<span class="premium-badge">
PRO
</span>
`;
}
return `
<div class="user-option">
${escape(data.name)}
${premium}
</div>
`;
}
}
Используется при включённой опции create.
new TomSelect('#skills', {
create: true,
render: {
option_create: function(data, escape) {
return `
<div class="create-option">
Создать:
<strong>${escape(data.input)}</strong>
</div>
`;
}
}
});
data.input содержит текст, введённый пользователем.
Сообщение при отсутствии результатов можно полностью изменить.
render: {
no_results: function(data, escape) {
return `
<div class="empty-results">
Ничего не найдено
</div>
`;
}
}
render: {
loading: function() {
return `
<div class="loading-spinner">
Загрузка...
</div>
`;
}
}
Tom Select поддерживает группировку элементов.
new TomSelect('#countries', {
optgroups: [
{
value: 'asia',
label: 'Asia'
},
{
value: 'europe',
label: 'Europe'
}
],
options: [
{
value: 'jp',
text: 'Japan',
continent: 'asia'
},
{
value: 'fr',
text: 'France',
continent: 'europe'
}
],
optgroupField: 'continent',
render: {
optgroup_header: function(data, escape) {
return `
<div class="group-title">
${escape(data.label)}
</div>
`;
}
}
});
render: {
option: function(data, escape) {
return `
<div
class="item"
data-id="${escape(data.id)}"
data-role="${escape(data.role)}"
>
${escape(data.name)}
</div>
`;
}
}
Подход полезен для интеграции со сторонними скриптами.
render: {
option: function(data, escape) {
let icon = `
<svg width="16" height="16">
<circle cx="8" cy="8" r="6"></circle>
</svg>
`;
return `
<div class="icon-option">
${icon}
${escape(data.text)}
</div>
`;
}
}
render: {
option: function(data, escape) {
return `
<div class="menu-item">
<i class="fa fa-user"></i>
${escape(data.text)}
</div>
`;
}
}
Внутри шаблонов допустимо размещать дополнительные элементы управления.
render: {
option: function(data, escape) {
return `
<div class="product">
<span>${escape(data.title)}</span>
<button
class="preview-btn"
type="button"
>
Preview
</button>
</div>
`;
}
}
Однако необходимо учитывать, что dropdown управляется самим Tom Select, поэтому некоторые события могут перехватываться библиотекой.
Часто шаблоны применяются при AJAX-загрузке данных.
new TomSelect('#repositories', {
valueField: 'id',
labelField: 'name',
searchField: 'name',
load: function(query, callback) {
fetch('/api/repos?q=' + query)
.then(response => response.json())
.then(json => {
callback(json.items);
})
.catch(() => {
callback();
});
},
render: {
option: function(data, escape) {
return `
<div class="repo">
<div class="repo-name">
${escape(data.name)}
</div>
<div class="repo-stars">
⭐ ${escape(data.stars)}
</div>
</div>
`;
}
}
});
Слишком тяжёлые HTML-шаблоны могут ухудшать производительность.
Основные причины:
Проблемный вариант:
option: function(data, escape) {
return `
<div>
<div>
<div>
<div>
<div>
${escape(data.text)}
</div>
</div>
</div>
</div>
</div>
`;
}
Оптимизированный вариант:
option: function(data, escape) {
return `
<div class="simple-option">
${escape(data.text)}
</div>
`;
}
Шаблоны удобно выносить в отдельные функции.
function renderUser(user, escape) {
return `
<div class="user-card">
${escape(user.name)}
</div>
`;
}
Использование:
new TomSelect('#users', {
render: {
option: function(data, escape) {
return renderUser(data, escape);
},
item: function(data, escape) {
return renderUser(data, escape);
}
}
});
Tom Select отлично сочетается с template literals ES6.
render: {
option: (data, escape) => `
<div class="city">
<strong>${escape(data.name)}</strong>
<small>${escape(data.country)}</small>
</div>
`
}
render: {
option: function(data, escape) {
const classes = [
'user',
data.admin ? 'admin' : '',
data.online ? 'online' : ''
].join(' ');
return `
<div class="${classes}">
${escape(data.name)}
</div>
`;
}
}
Если данные содержат markdown, необходимо выполнять преобразование отдельно.
render: {
option: function(data, escape) {
const html = marked.parse(data.description);
return `
<div class="markdown-content">
${html}
</div>
`;
}
}
При таком подходе требуется дополнительная защита от XSS.
Tom Select автоматически подсвечивает совпадения поиска. Кастомные шаблоны должны учитывать это поведение.
new TomSelect('#search', {
highlight: true
});
При сложной HTML-структуре следует тестировать корректность подсветки.
Tom Select может читать JSON из HTML-атрибутов.
<option
value="1"
data-data='{
"email":"alex@test.com",
"role":"admin"
}'
>
Alex
</option>
new TomSelect('#users', {
dataAttr: 'data-data',
render: {
option: function(data, escape) {
return `
<div>
${escape(data.text)}
(${escape(data.role)})
</div>
`;
}
}
});
Неверно:
option: function(data, escape) {
`
<div>${escape(data.text)}</div>
`;
}
Правильно:
option: function(data, escape) {
return `
<div>${escape(data.text)}</div>
`;
}
${data.username}
Если поле отсутствует, интерфейс может отображаться некорректно.
${data.text}
Следует использовать:
${escape(data.text)}
Пример CSS:
.user-option {
display: flex;
align-items: center;
gap: 12px;
}
.user-option .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.user-option .name {
font-weight: bold;
}
.user-option .role {
color: #777;
font-size: 13px;
}
render: {
option: function(data, escape) {
return `
<div class="
flex
items-center
gap-3
p-2
hover:bg-gray-100
">
<div class="font-bold">
${escape(data.name)}
</div>
</div>
`;
}
}
render: {
option: function(data, escape) {
return `
<div class="d-flex align-items-center p-2">
<span class="badge bg-primary me-2">
${escape(data.role)}
</span>
${escape(data.name)}
</div>
`;
}
}
render: {
option: function(data, escape) {
return `
<div class="product-card">
<img
src="${escape(data.image)}"
class="product-image"
>
<div class="product-info">
<div class="title">
${escape(data.title)}
</div>
<div class="price">
$${escape(data.price)}
</div>
</div>
</div>
`;
}
}
new TomSelect('#team', {
maxItems: null,
options: [
{
id: 1,
name: 'Alex',
email: 'alex@test.com',
avatar: '/img/alex.jpg'
}
],
valueField: 'id',
labelField: 'name',
searchField: ['name', 'email'],
render: {
option: function(data, escape) {
return `
<div class="member-option">
<img
class="avatar"
src="${escape(data.avatar)}"
>
<div class="member-info">
<div class="name">
${escape(data.name)}
</div>
<div class="email">
${escape(data.email)}
</div>
</div>
</div>
`;
},
item: function(data, escape) {
return `
<div class="member-tag">
${escape(data.name)}
</div>
`;
}
}
});