Работа с Choices.js внутри Vue требует понимания различий между
императивной и декларативной моделями. Vue управляет DOM через
реактивную систему, тогда как Choices.js напрямую модифицирует
DOM-элемент <select> или <input>,
создавая поверх него собственную структуру интерфейса.
Основная сложность заключается в синхронизации состояний:
Ключевая задача интеграции — обеспечить односторонний поток данных и контролируемое обновление экземпляра Choices.js при изменениях состояния Vue.
Наиболее прямолинейный способ интеграции — использование
ref и инициализация в жизненном цикле компонента.
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();
}
}
};
В 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);
});
}
При такой схеме:
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);
});
Важно учитывать:
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);
Choices.js поддерживает режим множественного выбора и создание тегов.
new Choices(select, {
removeItemButton: true,
duplicateItemsAllowed: false,
maxItemCount: 5,
addItems: true,
editItems: false
});
Во Vue важно учитывать тип данных:
Синхронизация:
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 генерирует ряд событий:
Во Vue их удобно пробрасывать через emit:
this.choices.passedElement.element.addEventListener(
"addItem",
(event) => {
this.$emit("add", event.detail);
}
);
Это позволяет расширять компонент без изменения внутренней логики.
При серверном рендеринге важно избегать инициализации 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 важно явно описывать экземпляр:
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-компонента-адаптера:
Такой компонент становится инфраструктурным слоем, отделяющим UI-библиотеку от доменной логики приложения.