События взаимодействия пользователя

Библиотека Tom Select построена вокруг событийной архитектуры. Практически любое действие пользователя — ввод текста, выбор элемента, удаление значения, открытие выпадающего списка, потеря фокуса — генерирует событие, на которое можно подписаться.

События позволяют:

  • синхронизировать интерфейс с сервером;
  • валидировать ввод;
  • запускать AJAX-запросы;
  • изменять состояние формы;
  • логировать действия пользователя;
  • интегрировать компонент с другими библиотеками;
  • реализовывать сложные сценарии UI.

Tom Select использует собственную систему событий, работающую через методы:

tomselect.on(event, handler);
tomselect.off(event, handler);
tomselect.trigger(event, ...args);

Экземпляр компонента обычно получают следующим образом:

const select = new TomSelect('#users');

После этого становится доступна подписка на пользовательские события.


Подписка на события

Базовая подписка выглядит так:

select.on('change', (value) => {
    console.log('Новое значение:', value);
});

Первый аргумент — имя события.

Второй аргумент — обработчик.


Удаление обработчиков

Для удаления используется off.

function onChange(value) {
    console.log(value);
}

select.on('change', onChange);

select.off('change', onChange);

Если не удалять старые обработчики при повторной инициализации компонентов, возможно накопление подписок и утечки памяти.


Ручной вызов событий

Метод trigger запускает событие вручную.

select.trigger('custom:update', {
    status: 'ok'
});

Это особенно полезно при интеграции нескольких компонентов.


Событие initialize

Событие initialize вызывается после полной инициализации компонента.

select.on('initialize', () => {
    console.log('Компонент готов');
});

Типичные сценарии:

  • запуск дополнительной настройки;
  • загрузка данных;
  • применение пользовательских стилей;
  • регистрация внешних обработчиков.

Пример:

select.on('initialize', () => {
    select.focus();
});

Событие change

Одно из самых важных событий.

Вызывается при изменении выбранного значения.

select.on('change', (value) => {
    console.log('Выбрано:', value);
});

Для multiple-режима значение может быть массивом.

const select = new TomSelect('#skills', {
    maxItems: null
});

select.on('change', (values) => {
    console.log(values);
});

Синхронизация с сервером

select.on('change', async (value) => {
    await fetch('/api/profile', {
        method: 'POST',
        body: JSON.stringify({ country: value })
    });
});

Валидация

select.on('change', (value) => {

    if (!value) {
        select.control.classList.add('error');
        return;
    }

    select.control.classList.remove('error');
});

Событие item_add

Срабатывает после добавления элемента.

select.on('item_add', (value, item) => {
    console.log(value);
});

Аргументы:

Аргумент Описание
value значение элемента
item DOM-узел выбранного элемента

Работа с DOM-элементом

select.on('item_add', (value, item) => {

    item.classList.add('selected-item');

});

Ограничение количества элементов

select.on('item_add', () => {

    if (select.items.length >= 5) {
        select.disable();
    }

});

Отправка аналитики

select.on('item_add', (value) => {

    analytics.track('skill_added', {
        skill: value
    });

});

Событие item_remove

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

select.on('item_remove', (value) => {
    console.log('Удалено:', value);
});

Автоматическое включение компонента

select.on('item_remove', () => {

    if (select.isDisabled && select.items.length < 5) {
        select.enable();
    }

});

Очистка связанных данных

select.on('item_remove', (value) => {

    delete cache[value];

});

Событие clear

Срабатывает после полной очистки выбранных значений.

select.on('clear', () => {
    console.log('Все элементы удалены');
});

Сброс интерфейса

select.on('clear', () => {

    document.querySelector('.preview').innerHTML = '';

});

Повторная загрузка данных

select.on('clear', () => {

    select.clearOptions();
    loadDefaultOptions();

});

Событие option_add

Срабатывает при добавлении новой опции.

select.on('option_add', (value, data) => {
    console.log(value, data);
});

Динамическое создание опций

select.addOption({
    value: 'vue',
    text: 'Vue'
});

Событие:

select.on('option_add', (value, data) => {

    console.log('Новая опция:', data.text);

});

Событие option_remove

Вызывается после удаления опции.

select.on('option_remove', (value) => {
    console.log('Опция удалена:', value);
});

Обновление кеша

select.on('option_remove', (value) => {

    localCache.delete(value);

});

Событие option_clear

Полностью очищает список опций.

select.on('option_clear', () => {
    console.log('Список очищен');
});

Перезагрузка данных

select.on('option_clear', async () => {

    const response = await fetch('/api/options');
    const data = await response.json();

    select.addOptions(data);

});

Событие dropdown_open

Срабатывает при открытии выпадающего списка.

select.on('dropdown_open', () => {
    console.log('Dropdown открыт');
});

Ленивый AJAX-запрос

select.on('dropdown_open', async () => {

    if (select.loadedSearches.initialized) {
        return;
    }

    const response = await fetch('/api/tags');
    const tags = await response.json();

    select.addOptions(tags);

    select.loadedSearches.initialized = true;

});

Изменение стилей

select.on('dropdown_open', () => {

    document.body.classList.add('dropdown-active');

});

Событие dropdown_close

Срабатывает при закрытии списка.

select.on('dropdown_close', () => {

    document.body.classList.remove('dropdown-active');

});

Очистка временного состояния

select.on('dropdown_close', () => {

    tempSearchResults = [];

});

Событие type

Событие вызывается во время ввода текста пользователем.

select.on('type', (str) => {
    console.log(str);
});

Автодополнение

select.on('type', async (query) => {

    if (query.length < 2) {
        return;
    }

    const response = await fetch(`/api/search?q=${query}`);
    const items = await response.json();

    select.clearOptions();
    select.addOptions(items);

});

Дебаунсинг ввода

Без ограничения количества запросов пользователь может перегрузить сервер.

let timeout;

select.on('type', (query) => {

    clearTimeout(timeout);

    timeout = setTimeout(async () => {

        const response = await fetch(`/api/search?q=${query}`);
        const data = await response.json();

        select.clearOptions();
        select.addOptions(data);

    }, 300);

});

Событие load

Срабатывает после завершения загрузки данных.

select.on('load', (data) => {
    console.log(data);
});

Обновление состояния интерфейса

select.on('load', () => {

    loader.style.display = 'none';

});

Отображение количества результатов

select.on('load', (data) => {

    resultsCounter.textContent = `Найдено: ${data.length}`;

});

Событие focus

Вызывается при получении фокуса.

select.on('focus', () => {
    console.log('Фокус');
});

Автоматическая загрузка

select.on('focus', async () => {

    if (select.optionsLoaded) {
        return;
    }

    await loadOptions();

    select.optionsLoaded = true;

});

Событие blur

Срабатывает при потере фокуса.

select.on('blur', () => {
    console.log('Потеря фокуса');
});

Валидация после ухода из поля

select.on('blur', () => {

    if (select.items.length === 0) {
        select.control.classList.add('invalid');
    }

});

Событие destroy

Вызывается перед уничтожением экземпляра.

select.on('destroy', () => {
    console.log('Компонент уничтожен');
});

Очистка ресурсов

select.on('destroy', () => {

    window.removeEventListener('resize', onResize);

    clearInterval(syncTimer);

});

Событие item_select

Срабатывает при выборе уже существующего элемента внутри компонента.

select.on('item_select', (item) => {

    console.log(item);

});

Работа с атрибутами

select.on('item_select', (item) => {

    const id = item.dataset.id;

    console.log(id);

});

Пользовательские события

Tom Select позволяет создавать собственные события.

select.on('profile:updated', (data) => {
    console.log(data);
});

Вызов:

select.trigger('profile:updated', {
    id: 15,
    status: 'saved'
});

Комбинирование нескольких событий

Один обработчик может использоваться для нескольких сценариев.

function syncState() {

    console.log('Синхронизация');

}

select.on('item_add', syncState);
select.on('item_remove', syncState);
select.on('clear', syncState);

Централизованная система событий

В крупных приложениях события удобно собирать в отдельном модуле.

export function bindSelectEvents(select) {

    select.on('change', onChange);
    select.on('item_add', onAdd);
    select.on('item_remove', onRemove);
    select.on('dropdown_open', onOpen);

}

Асинхронные обработчики

Tom Select корректно работает с async/await.

select.on('change', async (value) => {

    try {

        const response = await fetch('/api/save', {
            method: 'POST',
            body: JSON.stringify({ value })
        });

        const result = await response.json();

        console.log(result);

    } catch (error) {

        console.error(error);

    }

});

Ошибки внутри обработчиков

Ошибки в событиях необходимо перехватывать вручную.

select.on('item_add', (value) => {

    try {

        processItem(value);

    } catch (error) {

        console.error(error);

    }

});

Производительность событий

При интенсивном вводе событий может быть очень много.

Особенно это касается:

  • type;
  • change;
  • load.

Оптимизация через debounce

function debounce(callback, delay) {

    let timeout;

    return (...args) => {

        clearTimeout(timeout);

        timeout = setTimeout(() => {
            callback(...args);
        }, delay);

    };

}

select.on('type', debounce((query) => {

    console.log(query);

}, 300));

Интеграция с внешними библиотеками

Vue

select.on('change', (value) => {

    app.selectedCountry = value;

});

React

select.on('change', (value) => {

    setState(value);

});

Alpine.js

select.on('change', (value) => {

    Alpine.store('form').country = value;

});

Последовательность пользовательских событий

Типичная цепочка взаимодействия:

  1. focus
  2. dropdown_open
  3. type
  4. load
  5. item_add
  6. change
  7. dropdown_close
  8. blur

Понимание последовательности особенно важно при:

  • AJAX-загрузке;
  • динамической фильтрации;
  • асинхронной валидации;
  • обновлении зависимых компонентов;
  • синхронизации состояния приложения.

Практический сценарий: поиск пользователей

const select = new TomSelect('#users', {
    valueField: 'id',
    labelField: 'name',
    searchField: 'name'
});

let loading = false;

select.on('type', async (query) => {

    if (loading || query.length < 2) {
        return;
    }

    loading = true;

    try {

        const response = await fetch(`/api/users?q=${query}`);
        const users = await response.json();

        select.clearOptions();
        select.addOptions(users);

    } finally {

        loading = false;

    }

});

select.on('item_add', (value) => {

    console.log('Пользователь выбран:', value);

});

select.on('clear', () => {

    console.log('Поиск очищен');

});