Архитектура плагинов

Архитектура плагинов в Tom Select построена вокруг идеи минимального ядра и расширяемого поведения через подключаемые модули. Ядро библиотеки отвечает только за базовые операции: управление списком опций, выбранными значениями, вводом пользователя и синхронизацией состояния с DOM. Любая дополнительная логика — поиск с автодополнением, создание новых элементов, кастомные рендеры, работа с удалёнными источниками — реализуется через плагины.

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

Ключевая идея:

плагин не наследует класс, а модифицирует экземпляр через композицию


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

Подключение плагинов происходит на этапе инициализации экземпляра или через явное добавление в рантайме.

Типичная форма регистрации:

import TomSelect from 'tom-select'
import pluginRemoveButton from 'tom-select/dist/js/plugins/remove_button'

new TomSelect('#select', {
  plugins: [pluginRemoveButton]
})

Плагин может быть представлен как:

  • функция (instance) => void
  • объект с методом init
  • модуль с побочными эффектами (реже)

Базовый контракт:

function plugin(instance) {
  // модификация instance
}

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


Внутренний механизм применения плагинов

При инициализации TomSelect выполняется цикл загрузки плагинов:

  1. создаётся базовый экземпляр
  2. инициализируется состояние
  3. загружаются плагины из конфигурации
  4. каждый плагин вызывается с текущим instance
  5. результатом становится расширенный объект

Упрощённая модель:

class TomSelect {
  constructor(el, options) {
    this.options = options
    this.setup()

    ;(options.plugins || []).forEach(plugin => {
      plugin(this)
    })
  }
}

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


Жизненный цикл плагина

Плагин в архитектуре Tom Select не имеет строгого жизненного цикла, но фактически проходит несколько стадий взаимодействия:

1. Инициализация

Плагин получает уже созданный экземпляр и может:

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

2. Активная фаза

После инициализации плагин реагирует на:

  • пользовательский ввод
  • изменение значений
  • внешние вызовы API
  • обновления опций

3. Завершение (опционально)

Некоторые плагины предоставляют метод destroy или аналогичный механизм очистки:

function plugin(instance) {
  function cleanup() {
    // удаление обработчиков
  }

  instance.on('destroy', cleanup)
}

Хуки и система событий

Основной механизм интеграции плагинов — это событийная система. Экземпляр TomSelect реализует publish/subscribe модель.

Типичные события:

  • initialize
  • change
  • item_add
  • item_remove
  • dropdown_open
  • dropdown_close
  • type
  • load

Плагин может подписываться на события:

function plugin(instance) {
  instance.on('item_add', (value) => {
    console.log('Добавлен элемент:', value)
  })
}

Также поддерживается отмена поведения через возвращаемые значения:

instance.on('before_add_item', (value) => {
  if (value === 'forbidden') {
    return false
  }
})

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


Расширение экземпляра: методы и свойства

Плагины часто расширяют экземпляр новыми методами:

function plugin(instance) {
  instance.clearAll = function () {
    Object.keys(instance.items).forEach(item => {
      instance.removeItem(item)
    })
  }
}

Такой подход создаёт гибридную модель:

  • ядро остаётся неизменным
  • поведение расширяется динамически
  • API становится композиционным

Также плагины могут добавлять внутренние свойства:

instance.settings.myPluginEnabled = true
instance.state.myPluginCache = {}

Перехват и переопределение методов

Более сложный механизм — monkey patching методов экземпляра.

function plugin(instance) {
  const originalAddItem = instance.addItem

  instance.addItem = function (value, silent) {
    if (value === 'blocked') return

    return originalAddItem.call(this, value, silent)
  }
}

Такая техника позволяет:

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

Риск архитектуры — возможные конфликты при нескольких плагинах, перезаписывающих один и тот же метод.


Взаимодействие плагинов между собой

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

new TomSelect('#select', {
  plugins: [pluginA, pluginB, pluginC]
})

Порядок имеет значение:

  • pluginA может изменить метод
  • pluginB будет работать уже с изменённой версией
  • pluginC увидит итоговое состояние

Это создаёт модель цепочки трансформаций экземпляра.


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

Tom Select хранит состояние в нескольких слоях:

  • this.items — выбранные элементы
  • this.options — доступные опции
  • this.settings — конфигурация
  • this.control — DOM-контейнеры
  • внутренние кеши

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

Пример изменения состояния:

function plugin(instance) {
  instance.on('change', () => {
    instance.state.lastChange = Date.now()
  })
}

Архитектурная особенность: состояние не инкапсулировано строго, что упрощает расширение, но снижает защиту от побочных эффектов.


Пример архитектурного плагина: кастомная валидация

function validatePlugin(instance) {
  const originalAddItem = instance.addItem

  instance.addItem = function (value, silent) {
    if (!/^[a-z]+$/.test(value)) {
      instance.trigger('invalid', value)
      return
    }

    return originalAddItem.call(this, value, silent)
  }

  instance.on('invalid', (value) => {
    console.warn('Недопустимое значение:', value)
  })
}

Здесь сочетаются три механизма:

  • перехват метода
  • событие как канал обратной связи
  • расширение поведения без модификации ядра

Пример плагина через события: логирование

function loggerPlugin(instance) {
  instance.on('item_add', (value) => {
    console.log('[ADD]', value)
  })

  instance.on('item_remove', (value) => {
    console.log('[REMOVE]', value)
  })
}

В этом случае плагин не вмешивается в логику, а работает как наблюдатель.


Динамическое подключение плагинов

Некоторые плагины могут добавляться после инициализации:

const select = new TomSelect('#select')

plugin(select)

Такой подход требует аккуратности, так как:

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

Конфликты и приоритеты

Основная проблема архитектуры — конфликты при:

  • переопределении одинаковых методов
  • конкурирующих событиях
  • изменении DOM одной и той же области

Типичные стратегии:

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

Структурные особенности архитектуры плагинов

Архитектура Tom Select опирается на несколько ключевых принципов:

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

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