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

Библиотека Choices.js построена вокруг класса Choices, инкапсулирующего состояние списка, выбранных значений, элементов интерфейса и событийную модель. Добавление собственных методов к экземпляру требует понимания того, как именно создаётся объект и где допустимо расширение поведения без нарушения внутренней логики.

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


Расширение через обёртку экземпляра (wrapper pattern)

Наиболее безопасный способ добавления кастомных методов — создание функции-обёртки, возвращающей расширенный экземпляр Choices.

Идея заключается в том, чтобы не изменять оригинальный класс, а добавлять поверх него дополнительные методы:

import Choices from 'choices.js';

function createExtendedChoices(element, options = {}) {
  const instance = new Choices(element, options);

  instance.clearAll = function () {
    const currentValue = this.getValue(true);
    if (!Array.isArray(currentValue)) return;

    currentValue.forEach(value => {
      this.removeActiveItemsByValue(value);
    });
  };

  instance.selectByLabel = function (label) {
    const choices = this._store.choices;
    const found = choices.find(item => item.label === label);
    if (found) {
      this.setChoiceByValue(found.value);
    }
  };

  return instance;
}

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


Наследование класса Choices

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

import Choices from 'choices.js';

class ExtendedChoices extends Choices {
  constructor(element, options) {
    super(element, options);
  }

  clearAll() {
    const values = this.getValue(true);

    if (Array.isArray(values)) {
      values.forEach(v => this.removeActiveItemsByValue(v));
    }
  }

  toggleValue(value) {
    const current = this.getValue(true);

    if (current.includes(value)) {
      this.removeActiveItemsByValue(value);
    } else {
      this.setChoiceByValue(value);
    }
  }
}

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


Прототипное расширение (monkey patching)

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

import Choices from 'choices.js';

Choices.prototype.clearAll = function () {
  const values = this.getValue(true);

  if (!Array.isArray(values)) return;

  values.forEach(value => {
    this.removeActiveItemsByValue(value);
  });
};

Choices.prototype.selectFirst = function () {
  const choices = this._store.choices;

  if (choices.length > 0) {
    this.setChoiceByValue(choices[0].value);
  }
};

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


Инъекция методов через фабричную композицию

Более управляемая альтернатива monkey patching — композиция через функцию расширения.

function extendChoices(instance) {
  instance.clearAll = function () {
    const values = this.getValue(true);

    if (Array.isArray(values)) {
      values.forEach(v => this.removeActiveItemsByValue(v));
    }
  };

  instance.syncWithExternal = function (externalList) {
    const current = this.getValue(true);

    externalList.forEach(item => {
      if (!current.includes(item)) {
        this.setChoiceByValue(item);
      }
    });
  };

  return instance;
}

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

const choices = extendChoices(new Choices(element, options));

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


Работа с внутренним состоянием Choices.js

Многие кастомные методы требуют доступа к внутреннему хранилищу _store. Эта структура содержит:

  • список доступных опций (choices)
  • выбранные элементы
  • группы
  • метаданные состояния

Пример метода, работающего с внутренним store:

instance.findByPrefix = function (prefix) {
  return this._store.choices.filter(item =>
    item.label.toLowerCase().startsWith(prefix.toLowerCase())
  );
};

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


Практические примеры кастомных методов

Очистка всех выбранных значений с учётом одиночного режима

instance.clearAllSafe = function () {
  if (this.config.maxItemCount === 1) {
    this.removeActiveItems();
    return;
  }

  const values = this.getValue(true);

  values.forEach(v => this.removeActiveItemsByValue(v));
};

Переключение состояния значения

instance.toggleItem = function (value) {
  const selected = this.getValue(true);

  if (selected.includes(value)) {
    this.removeActiveItemsByValue(value);
  } else {
    this.setChoiceByValue(value);
  }
};

Выбор по регулярному выражению

instance.selectByRegex = function (regex) {
  const pattern = new RegExp(regex);
  const matches = this._store.choices.filter(item =>
    pattern.test(item.label)
  );

  matches.forEach(match => {
    this.setChoiceByValue(match.value);
  });
};

Синхронизация с внешним источником данных

instance.syncFromArray = function (data) {
  this.clearStore?.();

  data.forEach(item => {
    this.setChoices([
      {
        value: item.value,
        label: item.label,
        selected: item.selected || false
      }
    ]);
  });
};

Инкапсуляция расширений через модульный слой

При масштабных приложениях кастомные методы часто выносятся в отдельный слой расширений:

export function attachCustomMethods(instance) {
  instance.resetAll = function () {
    this.removeActiveItems();
  };

  instance.hasValue = function (value) {
    return this.getValue(true).includes(value);
  };

  instance.selectMany = function (values) {
    values.forEach(v => this.setChoiceByValue(v));
  };

  return instance;
}

Такой слой позволяет поддерживать единый стандарт расширений и переиспользовать их в разных частях приложения.


Управление конфликтами имён методов

При добавлении кастомных методов критически важно учитывать возможные конфликты с будущими версиями библиотеки. Используется префиксация:

instance.ext_clearAll = function () {
  const values = this.getValue(true);

  values.forEach(v => this.removeActiveItemsByValue(v));
};

Или использование namespace-объекта:

instance.custom = {
  clearAll: () => {},
  toggle: () => {},
  selectByRegex: () => {}
};

Второй вариант уменьшает риск пересечения с внутренними API и упрощает поддержку.


Работа с событиями внутри кастомных методов

Многие расширения требуют интеграции с системой событий Choices.js:

instance.clearAndNotify = function () {
  this.removeActiveItems();

  this.passedElement.triggerEvent('custom:cleared', {
    timestamp: Date.now()
  });
};

Использование событий позволяет интегрировать кастомные методы в общий жизненный цикл компонента без изменения ядра библиотеки.


Ограничения расширений и стабильность API

При работе с кастомными методами ключевым ограничением остаётся отсутствие гарантий стабильности внутренних структур _store и приватных методов. Любое расширение должно учитывать:

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

Наиболее устойчивыми считаются методы, опирающиеся только на публичный API: setChoiceByValue, getValue, removeActiveItems, setChoices.