Миграция с Choices.js

Библиотеки Choices.js и Tom Select решают схожую задачу: расширение возможностей <select> и <input> с поддержкой поиска, тегов, асинхронной загрузки и кастомного отображения элементов. Однако архитектура, API и модель расширения у них различаются.

Наиболее частые причины перехода на Tom Select:

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

Tom Select особенно хорошо подходит для сложных административных интерфейсов, CRM-систем, каталогов и приложений с динамической загрузкой данных.


Архитектурные различия

Подход Choices.js

Choices.js предоставляет относительно высокоуровневый API. Основной акцент сделан на:

  • декларативную настройку;
  • минимальное количество кода;
  • встроенное управление DOM;
  • ограниченную расширяемость.

Типичная инициализация:

const choices = new Choices('#tags', {
  removeItemButton: true,
  searchEnabled: true
});

Подход Tom Select

Tom Select построен иначе:

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

Пример:

const select = new TomSelect('#tags', {
  plugins: ['remove_button'],
  create: true
});

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

Choices.js

<link rel="stylesheet" href="choices.min.css">

<script src="choices.min.js"></script>

Tom Select

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

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

Полная сборка complete включает встроенные плагины.


Замена базовой инициализации

Choices.js

const choices = new Choices('#country', {
  searchEnabled: true,
  itemSelectText: ''
});

Аналог в Tom Select

const select = new TomSelect('#country', {
  create: false
});

В Tom Select многие функции активны по умолчанию, поэтому часть параметров просто исчезает.


Работа с множественным выбором

Choices.js

<select id="skills" multiple>
  <option value="js">JavaScript</option>
  <option value="php">PHP</option>
</select>
new Choices('#skills', {
  removeItemButton: true
});

Tom Select

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

В Tom Select удаление элементов реализовано через плагин.


Создание новых элементов

Choices.js

new Choices('#tags', {
  addItems: true,
  addItemFilter: value => value.length > 2
});

Tom Select

new TomSelect('#tags', {
  create: input => {
    if (input.length < 3) {
      return false;
    }

    return {
      value: input,
      text: input
    };
  }
});

В Tom Select создание опций может возвращать полноценный объект.


Работа с данными

Choices.js

choices.setChoices([
  { value: '1', label: 'Red' },
  { value: '2', label: 'Green' }
], 'value', 'label', true);

Tom Select

new TomSelect('#colors', {
  options: [
    { value: '1', text: 'Red' },
    { value: '2', text: 'Green' }
  ]
});

Динамическое добавление опций

Choices.js

choices.setChoices(newData, 'value', 'label', false);

Tom Select

select.addOption({
  value: '3',
  text: 'Blue'
});

select.refreshOptions(false);

В Tom Select обновление выпадающего списка обычно выполняется вручную.


Получение выбранных значений

Choices.js

const values = choices.getValue(true);

Tom Select

const values = select.getValue();

Для multiple возвращается массив, для одиночного выбора — строка.


Очистка выбранных элементов

Choices.js

choices.removeActiveItems();

Tom Select

select.clear();

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

Choices.js

choices.destroy();

Tom Select

select.destroy();

API здесь практически идентичен.


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

Choices.js

fetch('/api/tags')
  .then(response => response.json())
  .then(data => {
    choices.setChoices(data, 'value', 'label', true);
  });

Tom Select

new TomSelect('#tags', {
  valueField: 'id',
  labelField: 'name',
  searchField: 'name',

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

Tom Select предоставляет встроенный механизм асинхронной загрузки через load().


Отличия в структуре данных

Choices.js

{
  value: 'js',
  label: 'JavaScript'
}

Tom Select

{
  value: 'js',
  text: 'JavaScript'
}

Одно из самых распространённых изменений при миграции — замена label на text.


Настройка полей

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

new TomSelect('#users', {
  valueField: 'id',
  labelField: 'username',
  searchField: ['username', 'email']
});

Это особенно полезно при интеграции с REST API.


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

Choices.js

element.addEventListener('addItem', event => {
  console.log(event.detail.value);
});

Tom Select

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

Таблица соответствия событий

Choices.js Tom Select
addItem item_add
removeItem item_remove
search type
showDropdown dropdown_open
hideDropdown dropdown_close
highlightItem item_select

Работа с поиском

Choices.js

new Choices('#cities', {
  searchEnabled: true,
  searchChoices: true
});

Tom Select

new TomSelect('#cities', {
  searchField: ['text']
});

Если searchField пустой, поиск отключается:

searchField: []

Кастомный рендеринг

Choices.js

Choices.js ограничивает возможности кастомизации.


Tom Select

Tom Select предоставляет полноценную систему шаблонов.

new TomSelect('#users', {
  render: {
    option(data, escape) {
      return `
        <div class="user-option">
          <strong>${escape(data.name)}</strong>
          <span>${escape(data.email)}</span>
        </div>
      `;
    },

    item(data, escape) {
      return `
        <div class="selected-user">
          ${escape(data.name)}
        </div>
      `;
    }
  }
});

Экранирование HTML

Choices.js в большинстве случаев самостоятельно ограничивает опасный HTML.

В Tom Select ответственность за безопасность лежит на разработчике.

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

escape(data.name)

Опасный вариант:

${data.name}

Без экранирования возможны XSS-атаки.


Работа с тегами

Choices.js

new Choices('#tags', {
  duplicateItemsAllowed: false
});

Tom Select

new TomSelect('#tags', {
  create: true,
  persist: false
});

Для предотвращения дубликатов:

createFilter(value) {
  return !this.options[value];
}

Плагины

Choices.js

Choices.js практически не имеет полноценной плагинной архитектуры.


Tom Select

Tom Select активно использует плагины.

Пример:

new TomSelect('#tags', {
  plugins: {
    remove_button: {
      title: 'Удалить'
    },

    restore_on_backspace: {}
  }
});

Часто используемые плагины

Плагин Назначение
remove_button Кнопка удаления
clear_button Полная очистка
checkbox_options Чекбоксы
drag_drop Drag & Drop
dropdown_header Заголовок списка
no_backspace_delete Блокировка удаления

Миграция CSS

Choices.js

Choices.js использует собственные классы:

.choices {}
.choices__inner {}
.choices__list {}

Tom Select

Tom Select использует другую структуру:

.ts-wrapper {}
.ts-control {}
.ts-dropdown {}
.option {}
.item {}

Проблемы при миграции стилей

Наиболее частые проблемы:

  • исчезновение размеров элементов;
  • некорректные отступы;
  • конфликты flex/grid;
  • поломка темизации;
  • потеря hover-состояний;
  • проблемы с z-index;
  • некорректная высота dropdown.

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

Choices.js

.choices__inner {
  border-radius: 8px;
}

Tom Select

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

Работа с placeholder

Choices.js

new Choices('#country', {
  placeholder: true,
  placeholderValue: 'Выберите страну'
});

Tom Select

<select placeholder="Выберите страну">
new TomSelect('#country');

Tom Select обычно использует атрибут HTML.


Управление блокировкой

Choices.js

choices.disable();
choices.enable();

Tom Select

select.disable();
select.enable();

Управление dropdown

Choices.js

choices.showDropdown();
choices.hideDropdown();

Tom Select

select.open();
select.close();

Очистка опций

Choices.js

choices.clearChoices();

Tom Select

select.clearOptions();

Удаление выбранных элементов

Choices.js

choices.removeActiveItems();

Tom Select

select.clear();

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

Tom Select обычно показывает лучшую производительность при:

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

Однако кастомные шаблоны могут существенно увеличить нагрузку на DOM.


Миграция сложного примера

Choices.js

const choices = new Choices('#users', {
  removeItemButton: true,
  searchEnabled: true,
  duplicateItemsAllowed: false
});

fetch('/api/users')
  .then(r => r.json())
  .then(users => {
    choices.setChoices(
      users,
      'id',
      'name',
      true
    );
  });

Tom Select

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

  plugins: ['remove_button'],

  create: false,

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

  onItemAdd(value) {
    console.log('Selected:', value);
  }
});

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

Choices.js

Choices.js скрывает значительную часть внутренней логики.


Tom Select

Tom Select предоставляет доступ к:

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

Пример:

console.log(select.options);
console.log(select.items);
console.log(select.control);

Совместимость с Selectize.js

Tom Select создавался как современное развитие Selectize.js.

Поэтому многие проекты мигрируют по цепочке:

Selectize.js → Tom Select
Choices.js → Tom Select

API Selectize.js во многом сохраняется.


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

Использование label вместо text

Ошибка:

{
  value: 1,
  label: 'Admin'
}

Правильно:

{
  value: 1,
  text: 'Admin'
}

Отсутствие escape()

Ошибка:

return `<div>${data.name}</div>`;

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

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

Отсутствие refreshOptions()

После добавления данных dropdown может не обновиться.

select.addOption(option);
select.refreshOptions(false);

Конфликт CSS

Стили Choices.js и Tom Select нельзя подключать одновременно.

Проблемы:

  • двойные рамки;
  • поломка dropdown;
  • конфликт positioning;
  • неправильные размеры.

Неправильная работа с async

Ошибка:

load(query) {
  fetch(...);
}

Правильно:

load(query, callback) {
  fetch(...)
    .then(r => r.json())
    .then(data => callback(data))
    .catch(() => callback());
}

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

При большом проекте часто используется стратегия поэтапной замены.

Этап 1

Старые компоненты продолжают работать на Choices.js.


Этап 2

Новые формы создаются на Tom Select.


Этап 3

Создаётся слой адаптации:

function createSelect(element, options) {
  return new TomSelect(element, options);
}

Этап 4

Удаляется Choices.js и его CSS.


Стратегия адаптера

Иногда создают совместимый API:

class ChoicesAdapter {
  constructor(selector, options) {
    this.instance = new TomSelect(selector, {
      plugins: options.removeItemButton
        ? ['remove_button']
        : []
    });
  }

  getValue() {
    return this.instance.getValue();
  }

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

Это позволяет постепенно переписывать кодовую базу.


Интеграция с React/Vue

Tom Select проще адаптировать к компонентному подходу благодаря:

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

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

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

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

Работа с большими списками

При десятках тысяч элементов рекомендуется:

  • использовать async loading;
  • ограничивать количество результатов;
  • отключать ненужные плагины;
  • минимизировать HTML в render;
  • избегать тяжёлых CSS-эффектов.

Оптимизация рендеринга

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

option(data) {
  return `
    <div>
      <img src="${data.avatar}">
      <div>
        <strong>${data.name}</strong>
        <p>${data.description}</p>
      </div>
    </div>
  `;
}

Оптимизированный вариант:

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

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

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

  • клавиатурную навигацию;
  • мобильные устройства;
  • accessibility;
  • screen readers;
  • поведение поиска;
  • async loading;
  • очистку элементов;
  • destroy/init циклы;
  • SSR-совместимость;
  • работу внутри modal/dialog.