В интерфейсах, где используется Choices.js, форма перестаёт быть простым HTML-компонентом и превращается в управляемую модель данных. Значения формируются динамически, часто через асинхронную подгрузку, пользовательские теги и множественный выбор. В таких условиях стандартная HTML-валидация оказывается недостаточной, поскольку не учитывает внутреннее состояние компонента, ограничения на количество элементов и бизнес-правила.
Валидация перед отправкой формы становится отдельным этапом, в котором проверяется не только наличие значения, но и согласованность всей структуры данных, сформированной Choices-инстансом.
Choices.js хранит выбранные значения внутри собственного состояния и
синхронизирует их с <select> или
<input> только частично. Перед отправкой формы
критически важно опираться на актуальное состояние экземпляра.
Типовой механизм получения данных:
const choices = new Choices('#tags', {
removeItemButton: true,
maxItemCount: 5
});
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
const values = choices.getValue(true);
});
Метод getValue(true) возвращает массив примитивных
значений, что упрощает проверку ограничений до сериализации формы.
Одним из ключевых сценариев является контроль минимального и максимального количества выбранных элементов.
function validateSelectionCount(values) {
const MIN = 1;
const MAX = 5;
if (values.length < MIN) {
return { valid: false, error: 'Недостаточно элементов' };
}
if (values.length > MAX) {
return { valid: false, error: 'Превышено допустимое количество' };
}
return { valid: true };
}
Интеграция с Choices:
form.addEventListener('submit', (e) => {
const values = choices.getValue(true);
const result = validateSelectionCount(values);
if (!result.valid) {
e.preventDefault();
}
});
Дополнительно в самой библиотеке доступны параметры:
new Choices('#tags', {
maxItemCount: 5,
shouldSort: false,
duplicateItemsAllowed: false
});
Однако эти ограничения не покрывают бизнес-валидацию, связанную с контекстом формы.
В отличие от стандартного <select required>,
компонент Choices не всегда корректно отражает состояние required в
визуальном интерфейсе, особенно при множественном выборе.
function validateRequired(values) {
return values.length > 0;
}
Связка с UI-ошибкой:
form.addEventListener('submit', (e) => {
const values = choices.getValue(true);
if (values.length === 0) {
e.preventDefault();
choices.containerOuter.element.classList.add('is-invalid');
}
});
Состояние ошибки обычно привязывается к контейнеру
containerOuter, поскольку именно он отвечает за визуальное
представление поля.
Хотя Choices.js поддерживает
duplicateItemsAllowed: false, серверная и бизнес-валидация
могут требовать дополнительных проверок.
function validateUnique(values) {
const set = new Set(values);
return set.size === values.length;
}
В случаях кастомных объектов (например,
{ value, label }) требуется проверка по
value:
function validateUniqueObjects(values) {
const set = new Set(values.map(v => v.value));
return set.size === values.length;
}
При интеграции с сервером часто требуется проверка доступности выбранных значений: существование тегов, лимиты пользователя, права доступа.
async function validateServerSide(values) {
const response = await fetch('/validate-tags', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values })
});
return response.json();
}
Использование перед submit:
form.addEventListener('submit', async (e) => {
e.preventDefault();
const values = choices.getValue(true);
const result = await validateServerSide(values);
if (!result.valid) {
choices.containerOuter.element.classList.add('is-invalid');
return;
}
form.submit();
});
Асинхронная модель требует блокировки повторной отправки, чтобы избежать гонок состояний.
При частой смене значений пользователем проверка может выполняться слишком часто. Валидация оптимизируется через debounce.
function debounce(fn, delay) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), delay);
};
}
const validateLive = debounce(async () => {
const values = choices.getValue(true);
await validateServerSide(values);
}, 300);
Choices.js предоставляет события изменения состояния:
addItemremoveItemchangechoices.passedElement.element.addEventListener(
'change',
() => {
const values = choices.getValue(true);
}
);
События позволяют поддерживать актуальную валидацию без ожидания submit, формируя интерактивную проверку состояния.
Фильтрация вводимых значений может использоваться как первая линия защиты:
const choices = new Choices('#tags', {
addItems: true,
duplicateItemsAllowed: false,
callbackFilter: (value) => {
return /^[a-zA-Z0-9_-]+$/.test(value);
}
});
Такой механизм предотвращает попадание некорректных данных ещё на этапе ввода.
Несмотря на кастомность компонента, стандартный API валидации может использоваться через скрытое поле:
const hiddenInput = document.querySelector('input[type="hidden"]');
form.addEventListener('submit', (e) => {
const values = choices.getValue(true);
hiddenInput.value = JSON.stringify(values);
if (!hiddenInput.checkValidity()) {
e.preventDefault();
}
});
Таким образом сохраняется совместимость с required,
pattern и другими HTML-ограничениями.
Кнопка отправки часто синхронизируется с текущим состоянием выбора:
function updateSubmitState() {
const values = choices.getValue(true);
const isValid = values.length > 0 && values.length <= 5;
document.querySelector('button[type="submit"]').disabled = !isValid;
}
Связка с событиями:
choices.passedElement.element.addEventListener('change', updateSubmitState);
Валидация перед отправкой формируется как последовательность независимых проверок:
async function validateForm(values) {
const checks = [
validateRequired(values),
validateSelectionCount(values),
validateUnique(values),
await validateServerSide(values)
];
return checks.every(Boolean);
}
Такой подход позволяет изолировать бизнес-логику от UI-слоя и сохранять единый контракт проверки данных.
Состояния ошибок обычно выражаются через классы контейнера Choices:
is-invalidis-validfunction setErrorState(instance) {
instance.containerOuter.element.classList.add('is-invalid');
}
function clearErrorState(instance) {
instance.containerOuter.element.classList.remove('is-invalid');
}
Согласованность визуального состояния с результатами валидации предотвращает рассинхронизацию интерфейса и данных.
При асинхронной проверке возможны повторные сабмиты. Контроль выполняется через флаг состояния:
let isSubmitting = false;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (isSubmitting) return;
isSubmitting = true;
const values = choices.getValue(true);
const valid = await validateForm(values);
if (!valid) {
isSubmitting = false;
return;
}
form.submit();
});
Такой механизм фиксирует состояние транзакции до завершения всех проверок.