Создание плагинов

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

Плагин в Choices.js представляет собой объект или функцию, возвращающую объект с набором хуков (hooks), которые вызываются в строго определённых точках жизненного цикла экземпляра Choices. Основная идея заключается в инверсии управления: библиотека предоставляет точки расширения, а плагин внедряет туда собственную логику.

Базовая структура плагина

Типовой плагин реализуется как функция, принимающая экземпляр choices и возвращающая объект с методами-хуками.

function MyPlugin(choices) {
  return {
    init() {
      // инициализация плагина
    },
    onCreate() {
      // вызывается после создания экземпляра
    },
    onChoiceAdd(choice) {
      // вызывается при добавлении элемента
    },
    onChoiceRemove(choice) {
      // вызывается при удалении элемента
    },
    destroy() {
      // очистка ресурсов
    }
  };
}

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

Регистрация и подключение плагина

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

const instance = new Choices('#select', {
  plugins: [MyPlugin]
});

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

Порядок инициализации

  1. Создание экземпляра Choices
  2. Построение DOM-структуры
  3. Инициализация core-модулей
  4. Инициализация плагинов
  5. Вызов init() у каждого плагина
  6. Запуск пользовательских событий onCreate

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

Контракт плагина и жизненный цикл

Каждый плагин взаимодействует с объектом choices, который содержит:

  • config — конфигурация экземпляра
  • store — состояние выбранных значений
  • container — DOM-контейнер
  • input — исходный элемент <select> или <input>
  • triggerEvent — метод генерации событий

Основные хуки жизненного цикла

init()

Вызывается один раз при подключении плагина. Используется для:

  • регистрации событий
  • модификации DOM
  • кэширования ссылок на элементы
init() {
  this.choices.container.classList.add('plugin-enabled');
}

onCreate()

Срабатывает после полной инициализации экземпляра.

onCreate() {
  this.choices._storePluginData = {};
}

onChoiceAdd(choice)

Вызывается при добавлении нового выбранного элемента.

onChoiceAdd(choice) {
  console.log('Добавлен выбор:', choice);
}

onChoiceRemove(choice)

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

onChoiceRemove(choice) {
  console.log('Удалён выбор:', choice);
}

destroy()

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

destroy() {
  this.choices.container.classList.remove('plugin-enabled');
}

Работа с событиями внутри плагинов

Choices.js использует внутреннюю систему событий, основанную на паттерне observer. Плагины могут подписываться на события через triggerEvent или напрямую через внутренний emitter.

Подписка на события

function MyPlugin(choices) {
  function handleChange(event) {
    console.log('Изменение:', event.detail);
  }

  return {
    init() {
      choices.container.addEventListener('change', handleChange);
    },

    destroy() {
      choices.container.removeEventListener('change', handleChange);
    }
  };
}

Внутренние события Choices.js

Наиболее значимые события:

  • addItem
  • removeItem
  • highlightItem
  • showDropdown
  • hideDropdown
  • search

Плагины могут использовать эти события для синхронизации состояния.

Модификация DOM из плагинов

Одной из ключевых возможностей является расширение интерфейса без изменения ядра.

Добавление кастомных элементов

function AddBadgePlugin(choices) {
  let badge;

  return {
    init() {
      badge = document.createElement('div');
      badge.className = 'choices__badge';
      badge.textContent = 'Custom';

      choices.container.appendChild(badge);
    },

    destroy() {
      if (badge) {
        badge.remove();
      }
    }
  };
}

Инъекция в dropdown

init() {
  const dropdown = this.choices.dropdown.element;

  const footer = document.createElement('div');
  footer.className = 'custom-footer';
  footer.textContent = 'Дополнительная информация';

  dropdown.appendChild(footer);
}

Управление состоянием через плагин

Плагин имеет доступ к внутреннему хранилищу выбранных значений через store.

function CounterPlugin(choices) {
  let counter;

  return {
    init() {
      counter = document.createElement('span');
      counter.className = 'choices-counter';
      choices.container.appendChild(counter);

      this.update();
    },

    onChoiceAdd() {
      this.update();
    },

    onChoiceRemove() {
      this.update();
    },

    update() {
      const count = choices.store.activeItems.length;
      counter.textContent = `Выбрано: ${count}`;
    },

    destroy() {
      counter.remove();
    }
  };
}

Взаимодействие нескольких плагинов

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

const instance = new Choices('#select', {
  plugins: [PluginA, PluginB, PluginC]
});

Конфликты плагинов

Типовые проблемы:

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

Для предотвращения конфликтов используется:

  • изолированное пространство DOM
  • нейтральные CSS-классы
  • проверка существования элементов перед созданием

Расширение конфигурации через плагины

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

function ConfigPlugin(choices) {
  const config = choices.config.pluginOptions?.configPlugin || {};

  return {
    init() {
      if (config.enableLogging) {
        console.log('Логирование включено');
      }
    }
  };
}

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

const instance = new Choices('#select', {
  plugins: [ConfigPlugin],
  pluginOptions: {
    configPlugin: {
      enableLogging: true
    }
  }
});

Асинхронные плагины

Плагины могут работать асинхронно, например, при загрузке данных или удалённых ресурсов.

function AsyncPlugin(choices) {
  return {
    async init() {
      const response = await fetch('/api/data');
      const data = await response.json();

      data.forEach(item => {
        choices.setChoices([
          { value: item.id, label: item.name }
        ], 'value', 'label', true);
      });
    }
  };
}

Безопасность и стабильность плагинов

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

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

Нарушение этих правил приводит к утечкам памяти и нестабильному поведению экземпляра.

Паттерны проектирования в плагинах

Модульный паттерн

Изоляция логики через замыкания:

function Plugin() {
  let state = {};

  return function(choices) {
    return {
      init() {
        state.initialized = true;
      }
    };
  };
}

Фабрика плагинов

Позволяет параметризовать поведение:

function createPlugin(options) {
  return function(choices) {
    return {
      init() {
        if (options.debug) {
          console.log('Debug mode');
        }
      }
    };
  };
}

Декоратор

Расширение существующего поведения:

function decoratorPlugin(choices) {
  const originalAddItem = choices._addItem;

  choices._addItem = function() {
    console.log('Before add');
    const result = originalAddItem.apply(this, arguments);
    console.log('After add');
    return result;
  };
}

Тестирование плагинов

Тестирование строится вокруг имитации экземпляра Choices.

const mockChoices = {
  container: document.createElement('div'),
  store: { activeItems: [] },
  triggerEvent: () => {}
};

const plugin = MyPlugin(mockChoices);
plugin.init();

Проверяются:

  • корректность инициализации
  • реакция на события
  • отсутствие утечек DOM
  • корректное удаление ресурсов

Типичные ошибки при создании плагинов

  • отсутствие destroy()
  • прямое изменение приватных API
  • зависимость от внутренней структуры DOM
  • некорректная работа с асинхронностью
  • конфликт классов CSS
  • дублирование обработчиков событий

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