Интеграция сторонних решений

Библиотека Choices.js предоставляет развитый API, благодаря которому компонент выбора можно интегрировать практически с любыми внешними системами: UI-фреймворками, серверными API, библиотеками валидации, менеджерами состояния, CMS, инструментами аналитики и собственными плагинами.

Интеграция строится вокруг нескольких ключевых механизмов:

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

Базовая схема подключения:

import Choices from 'choices.js';

const element = document.querySelector('#categories');

const choices = new Choices(element, {
  searchEnabled: true,
  removeItemButton: true
});

После создания экземпляра компонент становится полноценным интерактивным слоем поверх обычного <select> или <input>.


Интеграция с REST API

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

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

Пример загрузки данных через Fetch API:

const sel ect = document.querySelector('#users');

const choices = new Choices(select, {
  searchEnabled: true,
  shouldSort: false
});

async function loadUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();

  choices.setChoices(
    users,
    'id',
    'name',
    true
  );
}

loadUsers();

Аргументы setChoices

Метод принимает:

choices.setChoices(data, valueKey, labelKey, replaceChoices);
Аргумент Назначение
data массив объектов
valueKey поле значения
labelKey отображаемый текст
replaceChoices очистка старых элементов

Работа с удалённым поиском

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

const choices = new Choices('#products', {
  searchEnabled: true,
  searchChoices: false
});

const input = choices.input.element;

input.addEventListener('input', async (event) => {
  const query = event.target.value;

  const response = await fetch(`/api/products?q=${query}`);
  const products = await response.json();

  await choices.clearChoices();

  choices.setChoices(products, 'id', 'title', true);
});

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

Важно учитывать:

  • debounce запросов;
  • отмену предыдущих запросов;
  • защиту от race condition;
  • кеширование результатов;
  • пагинацию.

Debounce для API-запросов

Без debounce приложение начинает отправлять запрос на каждый ввод символа.

function debounce(callback, delay) {
  let timeout;

  return (...args) => {
    clearTimeout(timeout);

    timeout = setTimeout(() => {
      callback(...args);
    }, delay);
  };
}

const searchHandler = debounce(async (value) => {
  const response = await fetch(`/api/search?q=${value}`);
  const data = await response.json();

  choices.clearChoices();
  choices.setChoices(data, 'id', 'name', true);
}, 300);

choices.input.element.addEventListener('input', (e) => {
  searchHandler(e.target.value);
});

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

Использование через useEffect

Choices.js напрямую изменяет DOM, поэтому интеграция с React требует контроля жизненного цикла.

import { useEffect, useRef } fr om 'react';
import Choices from 'choices.js';

function UserSelect() {
  const selectRef = useRef(null);
  const choicesRef = useRef(null);

  useEffect(() => {
    choicesRef.current = new Choices(selectRef.current, {
      removeItemButton: true
    });

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

  return (
    <sel ect ref={selectRef}>
      <option value="1">Admin</option>
      <option value="2">Editor</option>
    </select>
  );
}

Синхронизация состояния React

React использует однонаправленный поток данных, тогда как Choices.js управляет DOM самостоятельно.

Для синхронизации необходимо использовать события:

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

  const handler = (event) => {
    setValue(event.detail.value);
  };

  selectRef.current.addEventListener('change', handler);

  return () => {
    selectRef.current.removeEventListener('change', handler);
    instance.destroy();
  };
}, []);

Контролируемый компонент

useEffect(() => {
  if (!choicesRef.current) return;

  choicesRef.current.removeActiveItems();

  choicesRef.current.setChoiceByValue(value);
}, [value]);

Такой подход позволяет синхронизировать внутреннее состояние библиотеки с React State.


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

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

export default {
  mounted() {
    this.choices = new Choices(this.$refs.select, {
      searchEnabled: true
    });
  },

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

Двусторонняя привязка

mounted() {
  this.choices = new Choices(this.$refs.select);

  this.$refs.select.addEventListener('change', (event) => {
    this.modelValue = event.detail.value;
  });
}

Интеграция с Angular

Создание директивы

В Angular чаще всего создают собственную директиву.

import {
  Directive,
  ElementRef,
  OnInit,
  OnDestroy
} fr om '@angular/core';

import Choices from 'choices.js';

@Directive({
  selector: '[appChoices]'
})
export class ChoicesDirective implements OnInit, OnDestroy {

  private choices;

  constructor(private el: ElementRef) {}

  ngOnInit() {
    this.choices = new Choices(this.el.nativeElement);
  }

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

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

Сохранение состояния выбора

sel ect.addEventListener('change', (event) => {
  store.dispatch({
    type: 'SET_CATEGORY',
    payload: event.detail.value
  });
});

Обновление Choices.js из Store

store.subscribe(() => {
  const state = store.getState();

  choices.removeActiveItems();
  choices.setChoiceByValue(state.category);
});

Интеграция с Formik

Работа с React Formik

<Field name="country">
  {({ field, form }) => (
    <select
      ref={(ref) => {
        if (!ref) return;

        const instance = new Choices(ref);

        ref.addEventListener('change', (event) => {
          form.setFieldValue(
            field.name,
            event.detail.value
          );
        });
      }}
    >
      <option value="kz">Kazakhstan</option>
      <option value="us">USA</option>
    </select>
  )}
</Field>

Интеграция с Yup

Валидация выбранных значений

const schema = yup.object({
  tags: yup.array()
    .min(1)
    .required()
});

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


Интеграция с jQuery

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

$(document).ready(function() {
  const choices = new Choices('#status');
});

Совместная работа с jQuery AJAX

$.ajax({
  url: '/api/statuses',
  success(data) {
    choices.setChoices(data, 'id', 'title', true);
  }
});

Интеграция с Bootstrap

Использование Bootstrap-стилей

Choices.js не зависит от Bootstrap, но легко адаптируется под него.

const choices = new Choices('#roles', {
  classNames: {
    containerOuter: 'choices form-control',
    containerInner: 'choices__inner'
  }
});

Встраивание в Bootstrap Modal

Модальные окна часто создают проблемы с фокусом.

const modal = document.getElementById('userModal');

modal.addEventListener('shown.bs.modal', () => {
  choices.showDropdown();
});

Интеграция с Tailwind CSS

Переопределение классов

const choices = new Choices('#tags', {
  classNames: {
    containerOuter:
      'choices border rounded-lg p-2',
    item:
      'choices__item bg-blue-500 text-white'
  }
});

Интеграция с Alpine.js

Инициализация через x-init

<div
  x-data
  x-init="
    new Choices($refs.select, {
      removeItemButton: true
    });
  "
>
  <select x-ref="select"></select>
</div>

Интеграция с Laravel

Передача данных из Blade

<select id="users">
  @foreach($users as $user)
    <option value="{{ $user->id }}">
      {{ $user->name }}
    </option>
  @endforeach
</select>
new Choices('#users');

Получение данных через Laravel API

async function loadUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();

  choices.setChoices(users, 'id', 'name', true);
}

Интеграция с Symfony

Использование внутри Twig

<select id="categories">
  {% for category in categories %}
    <option value="{{ category.id }}">
      {{ category.title }}
    </option>
  {% endfor %}
</select>

Интеграция с CMS

WordPress

wp_enqueue_script(
  'choices-js',
  'https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js',
  [],
  null,
  true
);
document.addEventListener('DOMContentLoaded', () => {
  new Choices('#categories');
});

MODX

document.addEventListener('DOMContentLoaded', () => {
  new Choices('.js-select');
});

Интеграция с TypeScript

Типизация экземпляра

import Choices fr om 'choices.js';

let choices: Choices;

choices = new Choices('#users');

Типизация данных

interface User {
  id: number;
  name: string;
}

const users: User[] = await response.json();

choices.setChoices(users, 'id', 'name', true);

Интеграция с GraphQL

Загрузка данных через Apollo

const { data } = await apolloClient.query({
  query: GET_USERS
});

choices.setChoices(
  data.users,
  'id',
  'name',
  true
);

Интеграция с WebSocket

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

socket.onmess age = (event) => {
  const data = JSON.parse(event.data);

  choices.setChoices(data, 'id', 'name', true);
};

Интеграция с IndexedDB

Кеширование данных

async function loadCachedUsers() {
  const users = await db.users.toArray();

  choices.setChoices(users, 'id', 'name', true);
}

Интеграция с LocalStorage

Сохранение выбранных значений

sel ect.addEventListener('change', (event) => {
  localStorage.setItem(
    'selected_role',
    event.detail.value
  );
});

Восстановление состояния

const saved = localStorage.getItem('selected_role');

if (saved) {
  choices.setChoiceByValue(saved);
}

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

Отправка событий

select.addEventListener('change', (event) => {
  analytics.track('role_changed', {
    value: event.detail.value
  });
});

Интеграция с Google Analytics

select.addEventListener('addItem', (event) => {
  gtag('event', 'select_item', {
    item: event.detail.value
  });
});

Интеграция с системой валидации

Проверка перед отправкой формы

form.addEventListener('submit', (event) => {
  const value = choices.getValue(true);

  if (!value.length) {
    event.preventDefault();
  }
});

Отображение ошибок

choices.containerOuter.element.classList.add('is-invalid');

Интеграция с Drag and Drop

Совместная работа с Sortable.js

new Sortable(
  choices.choiceList.element,
  {
    animation: 150
  }
);

Интеграция с виртуализацией

При больших объёмах данных стандартный рендер может создавать проблемы производительности.

Типичные решения

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

Интеграция с SSR

Проблемы серверного рендеринга

Choices.js зависит от DOM API:

window
document
HTMLElement

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


Проверка окружения

if (typeof window !== 'undefined') {
  new Choices('#users');
}

Интеграция с Nuxt

onMounted(() => {
  new Choices('#countries');
});

Интеграция с Next.js

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

import dynamic fr om 'next/dynamic';

const ChoicesComponent = dynamic(
  () => import('./ChoicesComponent'),
  {
    ssr: false
  }
);

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

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

Choices.js генерирует множество пользовательских событий.

Событие Описание
addItem добавление элемента
removeItem удаление
change изменение значения
search поиск
showDropdown открытие списка
hideDropdown закрытие

Обработка событий

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

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

Переопределение render-функций

const choices = new Choices('#users', {
  callbackOnCreateTemplates(template) {
    return {
      item(classNames, data) {
        return template(`
          <div class="${classNames.item}">
            <strong>${data.label}</strong>
          </div>
        `);
      }
    };
  }
});

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

Блокировка выбора

if (!user.isAdmin) {
  choices.disable();
}

Интеграция с ACL

if (permissions.includes('edit_categories')) {
  choices.enable();
}

Интеграция с международными проектами

Локализация текстов

new Choices('#countries', {
  loadingText: 'Загрузка...',
  noResultsText: 'Ничего не найдено',
  noChoicesText: 'Нет вариантов'
});

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

В архитектуре микрофронтендов важно:

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

Интеграция с Shadow DOM

Инициализация внутри Web Components

class UserSelect extends HTMLElement {
  connectedCallback() {
    const select = this.shadowRoot.querySelector('select');

    this.choices = new Choices(select);
  }

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

Интеграция с системой доступа

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

const filtered = data.filter(item => {
  return permissions.includes(item.permission);
});

choices.setChoices(filtered, 'id', 'name', true);

Интеграция с Electron

Использование в desktop-приложениях

document.addEventListener('DOMContentLoaded', () => {
  new Choices('#projects');
});

Choices.js хорошо работает внутри Electron благодаря полной поддержке DOM API.


Интеграция с Capacitor и Cordova

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

  • производительность поиска;
  • размер dropdown;
  • виртуальную клавиатуру;
  • обработку touch-событий;
  • ограничения памяти.

Интеграция с CI/CD

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

Во время сборки обычно проверяют:

  • отсутствие утечек памяти;
  • корректное уничтожение экземпляров;
  • отсутствие конфликтов CSS;
  • размер bundle;
  • tree shaking.

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

Jest

test('choices initializes', () => {
  document.body.innerHTML =
    '<select id="users"></select>';

  const choices = new Choices('#users');

  expect(choices).toBeDefined();
});

Cypress

cy.get('.choices').click();

cy.get('.choices__item')
  .contains('Admin')
  .click();

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

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

select.addEventListener('change', (event) => {
  logger.info('Selection changed', {
    value: event.detail.value
  });
});

Интеграция с механизмом автосохранения

select.addEventListener('change', async () => {
  await fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify({
      value: choices.getValue(true)
    })
  });
});

Интеграция с системой фильтрации

Связанные селекты

countrySelect.addEventListener('change', async (e) => {
  const country = e.detail.value;

  const response = await fetch(
    `/api/cities?country=${country}`
  );

  const cities = await response.json();

  cityChoices.clearChoices();

  cityChoices.setChoices(
    cities,
    'id',
    'name',
    true
  );
});

Интеграция с модульной архитектурой

Выделение отдельного адаптера

export class ChoicesAdapter {
  constructor(selector, options = {}) {
    this.instance = new Choices(selector, options);
  }

  setData(data) {
    this.instance.setChoices(
      data,
      'id',
      'name',
      true
    );
  }

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

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