События жизненного цикла

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

События жизненного цикла используются для:

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

Экземпляр Tom Select предоставляет единый механизм подписки:

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

select.on('initialize', () => {
    console.log('Компонент инициализирован');
});

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

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

select.on('change', onChange);

select.off('change', onChange);

Подписка сразу на несколько событий:

select.on('item_add item_remove', () => {
    console.log('Список выбранных элементов изменился');
});

Событие initialize

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

На этом этапе:

  • DOM уже преобразован;
  • настройки применены;
  • данные загружены;
  • экземпляр готов к работе.

Пример:

new TomSelect('#tags', {
    options: [
        { value: 'js', text: 'JavaScript' },
        { value: 'ts', text: 'TypeScript' }
    ],

    onInitialize() {
        console.log('Tom Select готов');
    }
});

Альтернативный вариант через .on():

const control = new TomSelect('#tags');

control.on('initialize', () => {
    console.log('Инициализация завершена');
});

Практическое применение

Инициализация сторонних компонентов

control.on('initialize', () => {
    initTooltips();
    initAnalytics();
});

Динамическое изменение конфигурации

control.on('initialize', () => {
    control.settings.maxOptions = 500;
});

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

control.on('initialize', async () => {
    const response = await fetch('/api/default-tags');
    const tags = await response.json();

    tags.forEach(tag => {
        control.addItem(tag.id);
    });
});

Событие change

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

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

Для одиночного select:

"javascript"

Для multi-select:

["js", "ts", "react"]

Особенности события

Событие вызывается при:

  • добавлении элемента;
  • удалении элемента;
  • очистке списка;
  • программном изменении значения;
  • восстановлении состояния.

Отслеживание формы

control.on('change', () => {
    form.classList.add('modified');
});

Автоматическая отправка данных

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

Синхронизация нескольких компонентов

countrySelect.on('change', (country) => {
    citySelect.clear();
    citySelect.load(`/api/cities?country=${country}`);
});

Событие item_add

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

control.on('item_add', (value, item) => {
    console.log('Добавлен:', value);
});

Аргументы события

value

Значение добавленного элемента.

item

DOM-элемент выбранного пункта.

control.on('item_add', (value, item) => {
    item.classList.add('new-item');
});

Анимация новых элементов

control.on('item_add', (value, item) => {
    item.animate([
        { opacity: 0 },
        { opacity: 1 }
    ], {
        duration: 300
    });
});

Логирование действий пользователя

control.on('item_add', value => {
    analytics.track('tag_added', {
        tag: value
    });
});

Ограничение бизнес-логики

control.on('item_add', () => {
    if (control.items.length >= 5) {
        control.control_input.disabled = true;
    }
});

Событие item_remove

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

control.on('item_remove', (value, item) => {
    console.log('Удалён:', value);
});

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

control.on('item_remove', value => {
    cache.delete(value);
});

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

control.on('item_remove', () => {
    updateCounters();
});

Возврат доступности поля

control.on('item_remove', () => {
    if (control.items.length < 5) {
        control.control_input.disabled = false;
    }
});

Событие clear

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

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

Отличие от item_remove

item_remove вызывается для каждого удаляемого элемента отдельно.

clear вызывается один раз после завершения полной очистки.

Очистка зависимых блоков

control.on('clear', () => {
    preview.innerHTML = '';
});

Сброс состояния приложения

control.on('clear', () => {
    appState.filters = {};
});

Событие option_add

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

control.on('option_add', value => {
    console.log('Опция добавлена:', value);
});

Динамическое пополнение данных

control.on('option_add', value => {
    syncOptionWithServer(value);
});

Обработка пользовательских значений

new TomSelect('#skills', {
    create: true,

    onOptionAdd(value) {
        console.log('Создан новый навык:', value);
    }
});

Событие option_remove

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

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

Очистка внутренних структур

control.on('option_remove', value => {
    delete metadata[value];
});

Событие option_clear

Полная очистка списка опций.

control.on('option_clear', () => {
    console.log('Все опции очищены');
});

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

control.on('option_clear', () => {
    loader.show();
});

Событие dropdown_open

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

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

Ленивые вычисления

control.on('dropdown_open', () => {
    preloadVisibleItems();
});

Интеграция аналитики

control.on('dropdown_open', () => {
    analytics.track('dropdown_opened');
});

Динамическая загрузка

control.on('dropdown_open', async () => {
    if (!control.optionsLoaded) {
        await loadOptions();
    }
});

Событие dropdown_close

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

control.on('dropdown_close', () => {
    console.log('Dropdown закрыт');
});

Очистка временных структур

control.on('dropdown_close', () => {
    temporaryCache.clear();
});

Сброс поискового текста

control.on('dropdown_close', () => {
    control.setTextboxValue('');
});

Событие type

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

control.on('type', str => {
    console.log('Поисковая строка:', str);
});

Онлайн-поиск

control.on('type', debounce(async query => {
    await search(query);
}, 300));

Ограничение запросов

control.on('type', query => {
    if (query.length < 3) {
        return;
    }

    loadResults(query);
});

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

control.on('type', value => {
    showSuggestions(value);
});

Событие load

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

control.on('load', data => {
    console.log('Получено элементов:', data.length);
});

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

control.on('load', () => {
    spinner.hide();
});

Постобработка данных

control.on('load', options => {
    options.forEach(option => {
        option.loadedAt = Date.now();
    });
});

Событие focus

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

control.on('focus', () => {
    console.log('Фокус установлен');
});

Визуальная индикация

control.on('focus', () => {
    wrapper.classList.add('focused');
});

Автоматическое открытие

control.on('focus', () => {
    control.open();
});

Событие blur

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

control.on('blur', () => {
    console.log('Фокус потерян');
});

Валидация

control.on('blur', () => {
    validateField(control.getValue());
});

Очистка UI

control.on('blur', () => {
    wrapper.classList.remove('focused');
});

Событие destroy

Событие уничтожения экземпляра.

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

Очистка подписок

control.on('destroy', () => {
    window.removeEventListener('resize', onResize);
});

Освобождение памяти

control.on('destroy', () => {
    cache.clear();
    observers.disconnect();
});

Уничтожение экземпляра

control.destroy();

После вызова:

  • удаляются обработчики;
  • восстанавливается исходный select;
  • очищаются внутренние структуры;
  • уничтожаются DOM-ссылки.

Использование callback-методов конфигурации

Tom Select поддерживает два подхода:

  1. callbacks внутри настроек;
  2. подписка через .on().

Callback в конфигурации

new TomSelect('#users', {
    onChange(value) {
        console.log(value);
    }
});

Подписка через on

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

control.on('change', value => {
    console.log(value);
});

Отличия подходов

Callback-конфигурация

Подходит для:

  • локальной логики;
  • базовой настройки;
  • декларативной конфигурации.

.on()

Подходит для:

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

Порядок вызова событий

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

Добавление элемента

Типичная последовательность:

type
dropdown_open
load
item_add
change
dropdown_close

Очистка списка

item_remove
item_remove
item_remove
clear
change

Уничтожение

destroy

Каскадные события

Одно действие может запускать цепочку событий.

Пример:

control.on('item_add', value => {
    if (value === 'admin') {
        control.addItem('permissions');
    }
});

Цепочка:

item_add(admin)
change
item_add(permissions)
change

Защита от рекурсии

let updating = false;

control.on('change', value => {
    if (updating) return;

    updating = true;

    syncData(value);

    updating = false;
});

Namespaced-подход к событиям

Tom Select не поддерживает пространства имён событий напрямую, однако их можно эмулировать.

const handlers = {
    analyticsChange(value) {
        analytics.track(value);
    }
};

control.on('change', handlers.analyticsChange);

Удаление:

control.off('change', handlers.analyticsChange);

Архитектура событий в крупных приложениях

Централизованный Event Bus

control.on('change', value => {
    eventBus.emit('filters.updated', value);
});

Интеграция с Redux/Vuex

control.on('item_add', value => {
    store.dispatch({
        type: 'ADD_TAG',
        payload: value
    });
});

Реактивная синхронизация

control.on('change', value => {
    state.selected = value;
    render();
});

Отладка жизненного цикла

Логирование всех событий

[
    'initialize',
    'change',
    'item_add',
    'item_remove',
    'clear',
    'dropdown_open',
    'dropdown_close',
    'type',
    'load',
    'focus',
    'blur',
    'destroy'
].forEach(event => {
    control.on(event, (...args) => {
        console.log(event, args);
    });
});

Анализ последовательности

const timeline = [];

function log(event) {
    timeline.push({
        event,
        time: performance.now()
    });
}

control.on('change', () => log('change'));
control.on('load', () => log('load'));
control.on('item_add', () => log('item_add'));

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

Избежание тяжёлых операций

Плохой вариант:

control.on('type', query => {
    expensiveOperation(query);
});

Лучше использовать debounce:

control.on('type', debounce(query => {
    expensiveOperation(query);
}, 300));

Минимизация DOM-операций

Плохой вариант:

control.on('item_add', () => {
    renderLayout();
});

Лучше:

const rerender = debounce(renderLayout, 100);

control.on('item_add item_remove', rerender);

Интеграция событий с плагинами

Плагины Tom Select активно используют жизненный цикл.

TomSelect.define('logger', function() {

    this.on('item_add', value => {
        console.log('Добавлен элемент:', value);
    });

    this.on('destroy', () => {
        console.log('Плагин уничтожен');
    });

});

Подключение:

new TomSelect('#users', {
    plugins: ['logger']
});

Комбинирование событий

Автосохранение

control.on('change blur', () => {
    saveForm();
});

Загрузка при открытии

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

    if (control.loading) {
        return;
    }

    control.loading = true;

    await loadRemoteData();

    control.loading = false;
});

Динамическая валидация

control.on('type change blur', () => {
    validate();
});

Типичные ошибки при работе с жизненным циклом

Множественные подписки

Ошибка:

function init() {
    control.on('change', handler);
}

Если init() вызывается повторно, количество обработчиков растёт.

Решение:

control.off('change', handler);
control.on('change', handler);

Утечки памяти

Ошибка:

control.on('change', () => {
    window.addEventListener('resize', resizeHandler);
});

Обработчики накапливаются.

Правильный вариант:

window.addEventListener('resize', resizeHandler);

control.on('destroy', () => {
    window.removeEventListener('resize', resizeHandler);
});

Бесконечные циклы

Ошибка:

control.on('change', value => {
    control.setValue(value);
});

Правильный вариант:

let syncing = false;

control.on('change', value => {

    if (syncing) return;

    syncing = true;

    control.setValue(value);

    syncing = false;
});