Миграция с нативных select

Нативный элемент <select> в HTML остаётся базовым способом работы с выпадающими списками, однако его поведение и внешний вид сильно ограничены. Основные проблемы, которые подталкивают к миграции на Choices.js:

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

Choices.js решает эти задачи, предоставляя управляемый, расширяемый компонент поверх стандартного <select> или <input>.


Базовая модель миграции: от DOM к экземпляру Choices

Миграция начинается с понимания ключевого принципа: библиотека не заменяет данные, а оборачивает существующий элемент и синхронизирует состояние.

Исходный HTML:

<sel ect id="city">
  <option value="almaty">Almaty</option>
  <option value="astana">Astana</option>
  <option value="shymkent">Shymkent</option>
</select>

Инициализация Choices.js:

import Choices fr om 'choices.js';

const element = document.getElementById('city');

const cityChoices = new Choices(element, {
  searchEnabled: true,
  shouldSort: false
});

После инициализации:

  • оригинальный <select> скрывается;
  • создаётся виртуальное DOM-представление;
  • все изменения происходят через API Choices;
  • значения синхронизируются обратно в <select>.

Ключевой этап: анализ существующего поведения формы

Перед миграцией важно определить, как именно используется <select>:

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

Choices.js поддерживает большинство сценариев, но поведение нужно перенести явно, иначе возникают расхождения между старой и новой логикой.


Миграция одиночного select

Стандартный сценарий — одиночный выбор.

Было:

<select id="language">
  <option value="ru">Русский</option>
  <option value="en">English</option>
</select>

Стало:

const language = new Choices('#language', {
  searchEnabled: false,
  itemSelectText: ''
});

Особенности миграции:

  • searchEnabled: false имитирует поведение обычного select;
  • itemSelectText убирает подсказки интерфейса;
  • выбранное значение сохраняется в оригинальном <select>.

Миграция multiple select

Множественный выбор — один из наиболее частых кейсов, где native <select> быстро становится неудобным.

Было:

<select id="tags" multiple>
  <option value="js">JavaScript</option>
  <option value="css">CSS</option>
  <option value="html">HTML</option>
</select>

Стало:

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

Изменения в поведении:

  • выбранные элементы превращаются в теги;
  • появляется возможность удаления через UI;
  • управление происходит через API, а не через DOM selected.

Работа с динамическими данными

При миграции часто возникает необходимость заменить статические <option> на динамическую загрузку.

Добавление опций после инициализации:

const cities = new Choices('#city', {
  shouldSort: false
});

cities.setChoices([
  { value: 'almaty', label: 'Almaty' },
  { value: 'astana', label: 'Astana' }
], 'value', 'label', true);

Ключевые моменты:

  • четкое разделение данных и UI;
  • возможность полной перезагрузки списка;
  • контроль над тем, заменяются ли существующие опции.

Сохранение интеграции с формой

Одним из критических моментов миграции является сохранение совместимости с backend-логикой.

Choices.js не меняет принцип отправки формы:

  • значение остаётся в <select>;
  • отправка происходит стандартным HTML submit;
  • сервер не требует изменений.

Однако при кастомных сценариях важно учитывать:

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

Обработка событий вместо DOM-манипуляций

В нативном <select> часто используются события change напрямую на элементе.

document.getElementById('city').addEventListener('change', (e) => {
  console.log(e.target.value);
});

После миграции:

const city = new Choices('#city');

document.querySelector('#city').addEventListener('change', (e) => {
  console.log(e.target.value);
});

Choices.js также предоставляет собственные события:

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

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

  • addItem — добавление значения;
  • removeItem — удаление;
  • change — изменение состояния;
  • search — ввод в поиске.

Миграция optgroup

Группировка опций в нативном <select>:

<select id="cars">
  <optgroup label="German">
    <option value="bmw">BMW</option>
    <option value="audi">Audi</option>
  </optgroup>
</select>

Choices.js поддерживает аналогичную структуру через choices:

const cars = new Choices('#cars', {
  shouldSort: false
});

cars.setChoices([
  {
    label: 'German',
    id: 'german',
    disabled: false,
    choices: [
      { value: 'bmw', label: 'BMW' },
      { value: 'audi', label: 'Audi' }
    ]
  }
], 'value', 'label', true);

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

Потеря синхронизации состояния

Возникает при прямом изменении DOM:

document.querySelector('#city').innerHTML = '';

Choices.js не отслеживает такие изменения автоматически. Нужно использовать API:

city.clearStore();

Дублирование инициализации

Частая ошибка в SPA:

new Choices('#city');
new Choices('#city');

Результат — конфликт экземпляров и утечка памяти.

Решение:

if (!element.dataset.choicesInit) {
  new Choices(element);
  element.dataset.choicesInit = true;
}

Несоответствие значений

Нативный <select> допускает простые строки, но Choices.js требует структурированных объектов при динамической загрузке.

Ошибочный подход:

setChoices(['Almaty', 'Astana']);

Корректный:

setChoices([
  { value: 'almaty', label: 'Almaty' },
  { value: 'astana', label: 'Astana' }
]);

Обратная совместимость и постепенная миграция

В крупных проектах редко происходит полная замена сразу. Чаще применяется поэтапный подход:

  • выборочные инстансы Choices.js;
  • сохранение старых <select> в части формы;
  • постепенное расширение покрытия;
  • унификация API работы с формами.

Интеграция с фреймворками

React

useEffect(() => {
  const instance = new Choices(ref.current);

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

Vue

mounted() {
  this.choices = new Choices(this.$refs.select);
},
beforeUnmount() {
  this.choices.destroy();
}

Angular

ngAfterViewInit() {
  this.choices = new Choices(this.select.nativeElement);
}

ngOnDestroy() {
  this.choices.destroy();
}

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

При миграции важно учитывать жизненный цикл компонента:

city.destroy();

Это:

  • восстанавливает нативный <select>;
  • удаляет события;
  • очищает DOM-обёртку;
  • предотвращает утечки памяти.

Итоговая модель мышления при миграции

Переход от нативного <select> к Choices.js требует смены подхода:

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

Главное изменение заключается не в синтаксисе, а в переносе логики управления выбором из HTML в JavaScript-слой.