Ограничения множественного выбора

Система шаблонов в 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

Объект 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 Заголовок группы

Шаблон option

Наиболее часто переопределяемый шаблон.

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>
            `;
        }
    }
});

Шаблон item

Шаблон выбранного элемента может отличаться от шаблона 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 особенно важен, поскольку именно он определяет внешний вид тегов.


Использование HTML-структур

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>
        `;
    }
}

Внутри шаблонов допустимо использовать:

  • flex-layout
  • grid-layout
  • таблицы
  • SVG
  • иконки
  • badge-элементы
  • progress-bar
  • status-label
  • кнопки
  • счетчики

Безопасность HTML

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>
        `;
    }
}

Шаблон option_create

Используется при включённой опции 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 содержит текст, введённый пользователем.


Кастомизация no_results

Сообщение при отсутствии результатов можно полностью изменить.

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>
            `;
        }
    }
});

Использование dataset-атрибутов

render: {
    option: function(data, escape) {
        return `
            <div
                class="item"
                data-id="${escape(data.id)}"
                data-role="${escape(data.role)}"
            >
                ${escape(data.name)}
            </div>
        `;
    }
}

Подход полезен для интеграции со сторонними скриптами.


Использование SVG-иконок

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>
        `;
    }
}

Подключение иконок Font Awesome

render: {
    option: function(data, escape) {
        return `
            <div class="menu-item">
                <i class="fa fa-user"></i>

                ${escape(data.text)}
            </div>
        `;
    }
}

Интерактивные элементы внутри option

Внутри шаблонов допустимо размещать дополнительные элементы управления.

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, поэтому некоторые события могут перехватываться библиотекой.


Использование render совместно с load()

Часто шаблоны применяются при 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-шаблоны могут ухудшать производительность.

Основные причины:

  • большое количество DOM-узлов
  • сложная вложенность
  • изображения большого размера
  • inline SVG большого объёма
  • частые перерасчёты layout
  • дорогостоящие CSS-эффекты

Проблемный вариант:

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);
        }
    }
});

Использование template literals

Tom Select отлично сочетается с template literals ES6.

render: {
    option: (data, escape) => `
        <div class="city">
            <strong>${escape(data.name)}</strong>
            <small>${escape(data.country)}</small>
        </div>
    `
}

Работа с CSS-классами

render: {
    option: function(data, escape) {

        const classes = [
            'user',
            data.admin ? 'admin' : '',
            data.online ? 'online' : ''
        ].join(' ');

        return `
            <div class="${classes}">
                ${escape(data.name)}
            </div>
        `;
    }
}

Поддержка markdown-контента

Если данные содержат markdown, необходимо выполнять преобразование отдельно.

render: {
    option: function(data, escape) {

        const html = marked.parse(data.description);

        return `
            <div class="markdown-content">
                ${html}
            </div>
        `;
    }
}

При таком подходе требуется дополнительная защита от XSS.


Комбинирование с highlight

Tom Select автоматически подсвечивает совпадения поиска. Кастомные шаблоны должны учитывать это поведение.

new TomSelect('#search', {
    highlight: true
});

При сложной HTML-структуре следует тестировать корректность подсветки.


Использование dataAttr

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>
            `;
        }
    }
});

Ошибки при работе с render

Отсутствие return

Неверно:

option: function(data, escape) {
    `
        <div>${escape(data.text)}</div>
    `;
}

Правильно:

option: function(data, escape) {
    return `
        <div>${escape(data.text)}</div>
    `;
}

Использование несуществующих полей

${data.username}

Если поле отсутствует, интерфейс может отображаться некорректно.


Неэкранированный HTML

${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;
}

Интеграция с Tailwind CSS

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>
        `;
    }
}

Интеграция с Bootstrap

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>
            `;
        }
    }
});