Множественные экземпляры на странице

Библиотека Choices.js предназначена для замены стандартных <select> и <input> на более функциональные компоненты с поиском, множественным выбором, тегами и кастомным поведением. При использовании на реальных интерфейсах часто возникает необходимость создавать несколько независимых экземпляров на одной странице, каждый из которых имеет собственную конфигурацию, набор данных и поведение.

Ключевой принцип работы с множественными экземплярами заключается в том, что каждый DOM-элемент инициализируется отдельным объектом Choices, не пересекающимся по состояниям с другими инстансами.


Базовый паттерн множественной инициализации

Типичный сценарий — несколько <select> элементов на странице:

<sel ect class="js-choice" id="country">
  <option value="kz">Kazakhstan</option>
  <option value="ru">Russia</option>
</select>

<select class="js-choice" id="city">
  <option value="karaganda">Karaganda</option>
  <option value="almaty">Almaty</option>
</select>

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

import Choices fr om 'choices.js';

const elements = document.querySelectorAll('.js-choice');

const instances = [];

elements.forEach((el) => {
  const instance = new Choices(el, {
    searchEnabled: true,
    shouldSort: false
  });

  instances.push(instance);
});

Каждый вызов new Choices() создаёт изолированный экземпляр со своим состоянием, DOM-обвязкой и обработчиками событий.


Изоляция состояния между экземплярами

Choices.js не использует глобальное состояние для хранения данных, поэтому:

  • выбор в одном sel ect не влияет на другие
  • фильтрация работает локально
  • события привязаны к конкретному элементу

Это позволяет безопасно использовать библиотеку в сложных интерфейсах: фильтры, формы, панели администрирования.


Разные конфигурации для каждого экземпляра

Часто требуется различное поведение для разных селектов. Например, один с поиском, другой без него:

document.querySelectorAll('.js-choice').forEach((el) => {
  const type = el.dataset.type;

  const config = {
    shouldSort: false
  };

  if (type === 'searchable') {
    config.searchEnabled = true;
  }

  if (type === 'tags') {
    config.removeItemButton = true;
    config.maxItemCount = 5;
  }

  new Choices(el, config);
});

HTML:

<select class="js-choice" data-type="searchable"></select>
<select class="js-choice" data-type="tags" multiple></select>

Использование data-* атрибутов позволяет масштабировать конфигурацию без усложнения JavaScript-кода.


Паттерн фабрики экземпляров

При увеличении количества полей удобно выносить логику создания экземпляров в отдельную функцию:

function createChoice(element) {
  const baseConfig = {
    shouldSort: false,
    searchEnabled: true
  };

  if (element.classList.contains('is-tags')) {
    baseConfig.removeItemButton = true;
    baseConfig.maxItemCount = 10;
  }

  if (element.dataset.ajax === 'true') {
    baseConfig.searchEnabled = true;
  }

  return new Choices(element, baseConfig);
}

const instances = Array.fr om(
  document.querySelectorAll('.js-choice')
).map(createChoice);

Такой подход упрощает поддержку и централизует правила конфигурации.


Динамическое добавление экземпляров

В SPA или интерфейсах с динамическим DOM элементы могут появляться после загрузки страницы.

function initNewSelect(container) {
  const el = container.querySelector('select.js-choice');
  if (!el) return;

  return new Choices(el, {
    searchEnabled: true
  });
}

При добавлении новых блоков:

const container = document.createElement('div');
container.innerHTML = `
  <select class="js-choice">
    <option value="1">One</option>
  </select>
`;

document.body.appendChild(container);

initNewSelect(container);

Управление экземплярами через массив

При множественных инстансах часто требуется централизованное управление:

const instances = new Map();

document.querySelectorAll('.js-choice').forEach((el) => {
  const instance = new Choices(el, {});
  instances.set(el, instance);
});

Использование Map позволяет:

  • получать доступ к конкретному инстансу по DOM-узлу
  • уничтожать экземпляры при удалении элементов
  • переинициализировать компоненты

Уничтожение экземпляров (destroy)

При удалении элементов из DOM необходимо корректно освобождать ресурсы:

function removeSelect(el) {
  const instance = instances.get(el);

  if (instance) {
    instance.destroy();
    instances.delete(el);
  }

  el.remove();
}

Метод destroy():

  • удаляет дополнительные DOM-обёртки
  • снимает обработчики событий
  • восстанавливает исходный элемент

Переинициализация экземпляров

Иногда требуется обновить конфигурацию без перезагрузки страницы:

function reinit(el, newConfig) {
  const oldInstance = instances.get(el);

  if (oldInstance) {
    oldInstance.destroy();
  }

  const newInstance = new Choices(el, newConfig);
  instances.set(el, newInstance);
}

Общие конфигурации и наследование настроек

При множественных экземплярах удобно использовать базовую конфигурацию:

const baseConfig = {
  shouldSort: false,
  searchEnabled: true,
  itemSelectText: ''
};

function createInstance(el, override = {}) {
  return new Choices(el, {
    ...baseConfig,
    ...override
  });
}

Это уменьшает дублирование и упрощает поддержку единых правил UI.


Обработка событий для нескольких экземпляров

Каждый экземпляр имеет собственные события, например addItem:

const instance = new Choices(el, {});

el.addEventListener('addItem', (event) => {
  console.log('Добавлено значение:', event.detail.value);
});

При множественных экземплярах важно не использовать глобальные обработчики без фильтрации источника:

document.addEventListener('addItem', (e) => {
  if (!e.target.classList.contains('js-choice')) return;

  console.log('Сработало для нужного select');
});

Изоляция в сложных формах

В формах с повторяющимися блоками (например, динамические строки таблицы) каждый select должен инициализироваться отдельно:

function initForm(form) {
  form.querySelectorAll('select.js-choice').forEach((el) => {
    new Choices(el, {
      removeItemButton: true
    });
  });
}

Это предотвращает:

  • конфликт идентификаторов
  • пересечение состояний
  • утечку событий между блоками

Работа с AJAX-данными в нескольких экземплярах

При загрузке данных с сервера каждый экземпляр может иметь собственный endpoint:

document.querySelectorAll('.js-choice').forEach((el) => {
  const url = el.dataset.url;

  const instance = new Choices(el, {
    searchEnabled: true
  });

  el.addEventListener('search', async (e) => {
    const query = e.detail.value;

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

    instance.setChoices(data, 'value', 'label', true);
  });
});

Оптимизация при большом количестве экземпляров

При десятках и сотнях инстансов важны:

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

Пример ленивой инициализации:

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;

    new Choices(entry.target, {
      searchEnabled: true
    });

    observer.unobserve(entry.target);
  });
});

document.querySelectorAll('.js-choice').forEach((el) => {
  observer.observe(el);
});

Частые ошибки при множественных экземплярах

  • повторная инициализация одного и того же элемента без destroy()
  • хранение экземпляров без привязки к DOM
  • глобальные обработчики событий без фильтрации источника
  • попытка переиспользовать один экземпляр для разных элементов
  • отсутствие очистки при удалении DOM-узлов

Архитектурный подход к множественным Choices-инстансам

При масштабных интерфейсах применяется структура:

  • слой инициализации (bootstrap)
  • слой конфигураций (config factory)
  • слой управления жизненным циклом (store / registry)
  • слой UI (DOM)

Пример регистра:

class ChoiceRegistry {
  constructor() {
    this.map = new Map();
  }

  init(el, config) {
    const instance = new Choices(el, config);
    this.map.set(el, instance);
    return instance;
  }

  destroy(el) {
    const instance = this.map.get(el);
    if (instance) instance.destroy();
    this.map.delete(el);
  }
}

const registry = new ChoiceRegistry();

Итоговая модель поведения

При множественных экземплярах Choices.js на странице ключевыми принципами являются:

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