Миграция с Selectize

Библиотека Tom Select появилась как современное развитие идей Selectize.js. Несмотря на внешнюю схожесть API, между ними существуют важные различия в архитектуре, зависимостях, механизмах рендеринга и расширяемости.

Selectize исторически опирался на jQuery и внутренние плагины jQuery-подобного типа. Tom Sel ect полностью избавлен от зависимости на jQuery и работает как самостоятельная ES-библиотека.

Основные изменения

Область Selectize Tom Select
Зависимости Требуется jQuery Не требует jQuery
Архитектура Старый подход Современный модульный код
Сборка Ограниченная ESM, CommonJS
Плагины Старый формат Современная система
Типизация Отсутствует Поддержка TypeScript
Производительность Ниже Выше
Поддержка Практически остановлена Активно развивается

Удаление зависимости от jQuery

Самое заметное изменение при миграции — отказ от jQuery.

Код Selectize

$('#skills').selectize({
    create: true,
    maxItems: 5
});

Код Tom Select

new TomSelect('#skills', {
    create: true,
    maxItems: 5
});

Tom Sel ect использует обычные CSS-селекторы и DOM API.

Миграция событий

Selectize

$('#skills')[0].selectize.on('change', function(value) {
    console.log(value);
});

Tom Select

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

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

Подключение библиотеки

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

<link rel="stylesheet" href="selectize.css">

<script src="jquery.js"></script>
<script src="selectize.js"></script>

Подключение Tom Select

<link rel="stylesheet" href="tom-select.css">

<script src="tom-select.complete.min.js"></script>

Установка через npm

Selectize

npm install selectize

Tom Select

npm install tom-select

Использование ES-модулей

Tom Sel ect поддерживает современный импорт.

import TomSelect fr om 'tom-select';

new TomSelect('#users');

Импорт стилей:

import 'tom-select/dist/css/tom-select.css';

Изменения в инициализации

Selectize

const selectize = $('#sel ect')[0].selectize;

Tom Select

const sel ect = new TomSelect('#sel ect');

Экземпляр создаётся напрямую через конструктор.


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

Selectize

const control = $('#sel ect')[0].selectize;

Tom Select

const element = document.querySelector('#select');
const control = new TomSelect(element);

Изменения API

Большая часть API совместима, однако существуют отличия.

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

Selectize

selectize.addOption({
    value: 1,
    text: 'JavaScript'
});

Tom Select

control.addOption({
    value: 1,
    text: 'JavaScript'
});

Сигнатура практически идентична.


Обновление списка опций

Selectize

selectize.refreshOptions();

Tom Select

control.refreshOptions(false);

Tom Select требует более явного управления поведением.


Очистка выбранных значений

control.clear();

Метод полностью совместим.


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

Selectize

selectize.destroy();

Tom Select

control.destroy();

Однако Tom Select корректнее освобождает обработчики событий и DOM-ссылки.


Изменения в системе плагинов

Selectize использовал старую архитектуру плагинов.

Плагин в Selectize

Selectize.define('custom_plugin', function(options) {
    // plugin code
});

Плагин в Tom Select

TomSelect.define('custom_plugin', function(options) {
    // plugin code
});

Синтаксис похож, но внутренняя архитектура изменилась.


Использование встроенных плагинов

Selectize

$('#select').selectize({
    plugins: ['remove_button']
});

Tom Select

new TomSelect('#select', {
    plugins: ['remove_button']
});

Изменения в рендеринге

Tom Select более строго относится к HTML-рендерингу.

Selectize

render: {
    option: function(item, escape) {
        return '<div>' + item.text + '</div>';
    }
}

Tom Select

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

Важное отличие

Tom Select настоятельно требует экранирования пользовательских данных через escape().


Безопасность рендеринга

В старых проектах на Selectize часто встречается небезопасный код:

render: {
    option(item) {
        return `<div>${item.name}</div>`;
    }
}

При миграции необходимо обязательно внедрять экранирование.

Безопасный вариант

render: {
    option(item, escape) {
        return `<div>${escape(item.name)}</div>`;
    }
}

Изменения в загрузке данных

Selectize

load: function(query, callback) {
    $.ajax({
        url: '/search',
        success: callback
    });
}

Tom Select

load(query, callback) {
    fetch(`/search?q=${encodeURIComponent(query)}`)
        .then(response => response.json())
        .then(data => callback(data))
        .catch(() => callback());
}

Tom Select ориентирован на Fetch API и современные Promise.


Асинхронная загрузка

Tom Select лучше работает с асинхронными источниками.

new TomSelect('#users', {
    loadThrottle: 300,

    load(query, callback) {
        if (!query.length) {
            return callback();
        }

        fetch(`/api/users?q=${query}`)
            .then(res => res.json())
            .then(json => callback(json.items))
            .catch(() => callback());
    }
});

Изменения в CSS-классах

Некоторые CSS-классы отличаются.

Примеры

Selectize Tom Select
.selectize-control .ts-wrapper
.selectize-input .ts-control
.selectize-dropdown .ts-dropdown

После миграции старые стили могут перестать работать.


Адаптация пользовательских стилей

Selectize

.selectize-input {
    border-radius: 4px;
}

Tom Select

.ts-control {
    border-radius: 4px;
}

Изменения структуры DOM

Tom Select использует другую внутреннюю структуру контейнеров.

Возможные проблемы

  • сломанные CSS-селекторы;
  • некорректные flex-layout;
  • исчезновение кастомных отступов;
  • неправильное позиционирование dropdown;
  • конфликтующие transition-анимации.

Миграция обработчиков событий

Selectize

selectize.onItem Add = function(value) {
    console.log(value);
};

Tom Select

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

События Tom Select

Часто используемые события

Событие Назначение
change Изменение значения
item_add Добавление элемента
item_remove Удаление элемента
dropdown_open Открытие списка
dropdown_close Закрытие списка
type Ввод текста

Работа с create

Selectize

create: true

Tom Select

create: true

Поведение совместимо, но Tom Select строже обрабатывает новые элементы.


Пользовательское создание элементов

new TomSelect('#tags', {
    create(input) {
        return {
            value: input,
            text: input
        };
    }
});

Изменения в работе поиска

Tom Select использует улучшенный движок поиска.

Настройка поиска

new TomSelect('#users', {
    searchField: ['name', 'email']
});

Изменения в сортировке

Selectize

sortField: 'text'

Tom Select

sortField: [
    {
        field: 'text',
        direction: 'asc'
    }
]

Tom Select поддерживает более гибкую конфигурацию сортировки.


Миграция старых конфигураций

Старый вариант

$('#select').selectize({
    valueField: 'id',
    labelField: 'name',
    searchField: 'name'
});

Новый вариант

new TomSelect('#select', {
    valueField: 'id',
    labelField: 'name',
    searchField: ['name']
});

Поддержка TypeScript

Tom Select содержит встроенные типы.

import TomSelect fr om 'tom-select';

const control = new TomSelect('#users', {
    maxItems: 3
});

Типизация событий

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

Миграция jQuery-плагинов

Старые jQuery-плагины для Selectize обычно несовместимы напрямую.

Проблемный код

$.fn.customSelectize = function() {
    return this.selectize();
};

Адаптация

function customSelect(selector, options) {
    return new TomSelect(selector, options);
}

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

Tom Sel ect лучше интегрируется с современными фреймворками.

import { useEffect, useRef } fr om 'react';
import TomSelect fr om 'tom-select';

function UserSelect() {
    const ref = useRef();

    useEffect(() => {
        const control = new TomSelect(ref.current);

        return () => control.destroy();
    }, []);

    return <input ref={ref} />;
}

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

mounted() {
    this.control = new TomSelect(this.$refs.select);
},

beforeUnmount() {
    this.control.destroy();
}

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

import TomSelect fr om 'tom-select';
import 'tom-select/dist/css/tom-select.css';

Tom Sel ect корректно работает с современными bundler-системами.


Tree-shaking

Tom Sel ect поддерживает частичную загрузку модулей.

import TomSelect fr om 'tom-select/base';

Подключение только нужных плагинов:

import TomSelect fr om 'tom-select/base';
import remove_button fr om 'tom-select/plugins/remove_button.js';

TomSelect.define('remove_button', remove_button);

Оптимизация bundle size

Полная сборка

import TomSelect fr om 'tom-select';

Минимальная сборка

import TomSelect fr om 'tom-select/base';

Совместимость старого кода

Многие проекты используют промежуточный слой совместимости.

function initSelect(selector, options) {
    return new TomSelect(selector, options);
}

Такой подход упрощает постепенную миграцию.


Поэтапная миграция

Типичный план

  1. Удаление jQuery-зависимостей.
  2. Замена инициализации.
  3. Обновление CSS.
  4. Переписывание событий.
  5. Проверка рендеринга.
  6. Проверка плагинов.
  7. Обновление асинхронной загрузки.
  8. Тестирование accessibility.
  9. Проверка производительности.

Типичные проблемы миграции

Неработающие стили

Причина:

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

Ошибки событий

Причина:

  • изменение названий событий;
  • отказ от jQuery .trigger();
  • отсутствие контекста this.

Проблемы с AJAX

Причина:

  • удаление jQuery AJAX;
  • переход на Fetch API;
  • Promise-ориентированная архитектура.

XSS после миграции

Причина:

  • старые render-функции;
  • отсутствие escape();
  • прямой вывод HTML.

Проверка совместимости

После миграции обычно тестируются:

  • поиск;
  • keyboard navigation;
  • множественный выбор;
  • удаление элементов;
  • асинхронная загрузка;
  • пользовательский ввод;
  • мобильное отображение;
  • accessibility;
  • destroy/re-init;
  • интеграция с формами.

Accessibility в Tom Select

Tom Select значительно улучшил поддержку accessibility.

Улучшения

  • ARIA-атрибуты;
  • корректная клавиатурная навигация;
  • улучшенное управление focus;
  • совместимость со screen readers.

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

Tom Select работает быстрее благодаря:

  • отсутствию jQuery;
  • оптимизированному DOM;
  • улучшенному рендерингу;
  • сокращению лишних repaint/reflow;
  • более эффективному поисковому движку.

Миграция legacy-кода

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

class LegacySelect {
    constructor(selector, options) {
        this.instance = new TomSelect(selector, options);
    }

    clear() {
        this.instance.clear();
    }

    destroy() {
        this.instance.destroy();
    }
}

Стратегия безопасной миграции

Рекомендуемый подход

  1. Создание совместимого API-слоя.
  2. Изоляция старых jQuery-вызовов.
  3. Параллельное тестирование.
  4. Постепенная замена render-функций.
  5. Проверка XSS-безопасности.
  6. Аудит CSS.
  7. Проверка memory leaks.
  8. Нагрузочное тестирование.
  9. Финальное удаление Selectize.

Проверка утечек памяти

Tom Select корректнее работает при динамическом создании компонентов.

Важное правило

control.destroy();

Без уничтожения экземпляров возможны:

  • висящие обработчики;
  • утечки DOM;
  • накопление listeners;
  • деградация производительности SPA.

Миграция динамических форм

Старый подход

$('.dynamic-select').selectize();

Новый подход

document.querySelectorAll('.dynamic-select')
    .forEach(element => {
        new TomSelect(element);
    });

Изменения в callback-логике

Selectize

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

Tom Select

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

Tom Select ориентирован на современный синтаксис методов объекта.


Полный пример миграции

Selectize

$('#users').selectize({
    valueField: 'id',
    labelField: 'name',
    searchField: 'name',

    create: false,

    load: function(query, callback) {
        $.ajax({
            url: '/api/users',
            data: { q: query },
            success: callback
        });
    },

    render: {
        option: function(item) {
            return '<div>' + item.name + '</div>';
        }
    }
});

Tom Select

new TomSelect('#users', {
    valueField: 'id',
    labelField: 'name',
    searchField: ['name'],

    create: false,

    load(query, callback) {
        fetch(`/api/users?q=${encodeURIComponent(query)}`)
            .then(response => response.json())
            .then(data => callback(data))
            .catch(() => callback());
    },

    render: {
        option(item, escape) {
            return `<div>${escape(item.name)}</div>`;
        }
    }
});