Валидация перед отправкой формы

В интерфейсах, где используется Choices.js, форма перестаёт быть простым HTML-компонентом и превращается в управляемую модель данных. Значения формируются динамически, часто через асинхронную подгрузку, пользовательские теги и множественный выбор. В таких условиях стандартная HTML-валидация оказывается недостаточной, поскольку не учитывает внутреннее состояние компонента, ограничения на количество элементов и бизнес-правила.

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


Синхронизация состояния Choices.js с формой

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 для предвалидации

Choices.js предоставляет события изменения состояния:

  • addItem
  • removeItem
  • change
choices.passedElement.element.addEventListener(
  'change',
  () => {
    const values = choices.getValue(true);
  }
);

События позволяют поддерживать актуальную валидацию без ожидания submit, формируя интерактивную проверку состояния.


Ограничения через callback-фильтрацию

Фильтрация вводимых значений может использоваться как первая линия защиты:

const choices = new Choices('#tags', {
  addItems: true,
  duplicateItemsAllowed: false,
  callbackFilter: (value) => {
    return /^[a-zA-Z0-9_-]+$/.test(value);
  }
});

Такой механизм предотвращает попадание некорректных данных ещё на этапе ввода.


Интеграция с HTML5 Constraint Validation API

Несмотря на кастомность компонента, стандартный 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-ограничениями.


Управление состоянием submit-кнопки

Кнопка отправки часто синхронизируется с текущим состоянием выбора:

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-invalid
  • is-valid
function 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();
});

Такой механизм фиксирует состояние транзакции до завершения всех проверок.