Работа в Vue

Архитектурные особенности интеграции

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

Основная сложность заключается в синхронизации состояний:

  • Vue хранит источник истины в реактивных данных
  • Choices.js хранит внутреннее состояние выбранных значений
  • DOM выступает промежуточным слоем, но не должен становиться источником истины

Ключевая задача интеграции — обеспечить односторонний поток данных и контролируемое обновление экземпляра Choices.js при изменениях состояния Vue.


Базовая инициализация в компоненте Vue

Наиболее прямолинейный способ интеграции — использование ref и инициализация в жизненном цикле компонента.

Пример на Options API

import Choices from "choices.js";

export default {
  name: "SelectField",
  props: {
    options: {
      type: Array,
      default: () => []
    },
    modelValue: [String, Array]
  },
  emits: ["update:modelValue"],
  mounted() {
    this.choices = new Choices(this.$refs.select, {
      searchEnabled: true,
      removeItemButton: true
    });

    this.syncFromProps();
  },
  watch: {
    modelValue() {
      this.syncFromProps();
    },
    options() {
      this.refreshOptions();
    }
  },
  methods: {
    syncFromProps() {
      this.choices.setChoiceByValue(this.modelValue || []);
    },
    refreshOptions() {
      this.choices.clearStore();
      this.choices.setChoices(this.options, "value", "label", true);
    }
  },
  beforeUnmount() {
    if (this.choices) {
      this.choices.destroy();
    }
  }
};

Связка с v-model

В Vue 3 модель данных стандартизируется через v-model, который фактически является синтаксическим сахаром над modelValue и update:modelValue.

Ключевой момент — Choices.js не знает о Vue-событиях, поэтому необходимо вручную пробрасывать изменения.

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

  this.$refs.select.addEventListener("change", (event) => {
    const value = this.choices.getValue(true);
    this.$emit("update:modelValue", value);
  });
}

При такой схеме:

  • Vue управляет входным значением
  • Choices.js уведомляет о пользовательских изменениях
  • Компонент выступает адаптером между системами

Реализация на Composition API

Composition API позволяет более явно контролировать жизненный цикл и побочные эффекты.

import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import Choices from "choices.js";

export default {
  props: {
    modelValue: [String, Array],
    options: Array
  },
  emits: ["update:modelValue"],
  setup(props, { emit }) {
    const selectRef = ref(null);
    let choicesInstance = null;

    const initChoices = () => {
      choicesInstance = new Choices(selectRef.value, {
        searchEnabled: true
      });

      selectRef.value.addEventListener("change", () => {
        emit("update:modelValue", choicesInstance.getValue(true));
      });
    };

    const setOptions = () => {
      if (!choicesInstance) return;
      choicesInstance.clearStore();
      choicesInstance.setChoices(props.options, "value", "label", true);
    };

    const setValue = () => {
      if (!choicesInstance) return;
      choicesInstance.setChoiceByValue(props.modelValue || []);
    };

    onMounted(() => {
      initChoices();
      setOptions();
      setValue();
    });

    watch(() => props.options, setOptions, { deep: true });
    watch(() => props.modelValue, setValue);

    onBeforeUnmount(() => {
      if (choicesInstance) choicesInstance.destroy();
    });

    return { selectRef };
  }
};

Управление списком опций

Choices.js оперирует внутренним хранилищем options, поэтому динамическое обновление требует полной или частичной перезагрузки состояния.

Основные стратегии:

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

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

choices.clearStore();
choices.setChoices(newOptions, "value", "label", true);

Инкрементальное обновление

Используется при частых изменениях:

choices.setChoices([newOption], "value", "label", false);

Параметр replaceChoices управляет перезаписью или добавлением.


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

Во Vue часто используется загрузка данных с сервера. В связке с Choices.js это требует контроля состояния загрузки.

watch(searchQuery, async (query) => {
  const data = await fetch(`/api/search?q=${query}`).then(r => r.json());

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

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

  • частоту запросов (рекомендуется debounce)
  • очистку состояния при смене запроса
  • предотвращение гонок ответов

Debounce при поиске

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

function debounce(fn, delay) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}

const fetchOptions = debounce(async (search) => {
  const res = await fetch(`/api?q=${search}`);
  const data = await res.json();
  choices.setChoices(data, "value", "label", true);
}, 300);

Работа с multiple sel ect и тегами

Choices.js поддерживает режим множественного выбора и создание тегов.

new Choices(select, {
  removeItemButton: true,
  duplicateItemsAllowed: false,
  maxItemCount: 5,
  addItems: true,
  editItems: false
});

Во Vue важно учитывать тип данных:

  • одиночный select → строка
  • multiple → массив

Синхронизация:

setValue() {
  const value = this.modelValue;
  this.choices.setChoiceByValue(
    Array.isArray(value) ? value : [value]
  );
}

Очистка и сброс состояния

Частая проблема — некорректный сброс состояния при смене формы.

Правильный подход:

reset() {
  this.choices.removeActiveItems();
  this.$emit("update:modelValue", []);
}

Либо полная синхронизация:

this.choices.setChoiceByValue([]);

Обработка событий Choices.js

Choices.js генерирует ряд событий:

  • addItem
  • removeItem
  • change
  • highlightItem

Во Vue их удобно пробрасывать через emit:

this.choices.passedElement.element.addEventListener(
  "addItem",
  (event) => {
    this.$emit("add", event.detail);
  }
);

Это позволяет расширять компонент без изменения внутренней логики.


SSR и особенности Nuxt-подобных окружений

При серверном рендеринге важно избегать инициализации Choices.js до монтирования DOM.

if (typeof window !== "undefined") {
  this.choices = new Choices(this.$refs.select);
}

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

onMounted(async () => {
  const Choices = (await import("choices.js")).default;
  choicesInstance = new Choices(selectRef.value);
});

Типизация (TypeScript)

При использовании TypeScript важно явно описывать экземпляр:

import Choices fr om "choices.js";

let choices: Choices | null = null;

И типизировать props:

interface Props {
  modelValue: string | string[];
  options: { value: string; label: string }[];
}

Частые ошибки интеграции

Потеря синхронизации состояния

Возникает при одновременном управлении DOM и Vue без разделения ответственности.

Повторная инициализация

Создание нового экземпляра Choices.js без destroy() приводит к утечкам памяти и дублированию DOM-узлов.

Несовместимость типов значений

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

Решение — нормализация:

normalize(value) {
  return Array.isArray(value)
    ? value.map(String)
    : String(value);
}

Компонентный паттерн-обертка

Наиболее устойчивый подход — создание универсального Vue-компонента-адаптера:

  • принимает options
  • управляет lifecycle Choices.js
  • синхронизирует v-model
  • не содержит бизнес-логики

Такой компонент становится инфраструктурным слоем, отделяющим UI-библиотеку от доменной логики приложения.