Визуальная индикация ошибок

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

Основная сложность при работе с кастомными select-компонентами заключается в том, что нативные механизмы браузера (:invalid, :valid) перестают применяться напрямую. DOM-элемент скрывается, а управление вводом переходит к JavaScript-слою, который полностью отвечает за состояние визуального компонента.


Базовая модель состояния поля

В Choices.js поле выбора можно рассматривать как совокупность трёх уровней состояния:

  • Состояние данных (выбранные значения)
  • Состояние интерфейса (открыт/закрыт список, фокус)
  • Состояние валидности (ошибка, предупреждение, корректность)

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


Подходы к реализации визуальных ошибок

Внешняя валидация через форму

Наиболее распространённый вариант — использование стандартной HTML-валидации или сторонних валидаторов (например, на уровне бизнес-логики), с последующим обновлением UI компонента.

Пример базовой интеграции:

const element = document.querySelector('#select');
const choices = new Choices(element, {
  shouldSort: false,
  removeItemButton: true
});

const form = document.querySelector('form');

form.addEventListener('submit', (e) => {
  const value = choices.getValue(true);

  if (!value || value.length === 0) {
    e.preventDefault();
    showError(element, 'Необходимо выбрать хотя бы один элемент');
  }
});

Функция визуального отображения ошибки обычно работает через модификацию классов и вставку текстового блока:

function showError(el, message) {
  const wrapper = el.closest('.choices');
  wrapper.classList.add('is-invalid');

  let error = wrapper.querySelector('.choices-error');

  if (!error) {
    error = document.createElement('div');
    error.className = 'choices-error';
    wrapper.appendChild(error);
  }

  error.textContent = message;
}

Использование CSS-состояний

Choices.js активно опирается на классы контейнера .choices, что позволяет расширять визуальные состояния без вмешательства в внутреннюю структуру.

Типовые состояния:

  • .is-focused — поле в фокусе
  • .is-open — список раскрыт
  • .is-disabled — отключённое состояние
  • пользовательские классы для ошибок

Пример стилизации ошибки:

.choices.is-invalid .choices__inner {
  border-color: #d9534f;
  box-shadow: 0 0 0 1px rgba(217, 83, 79, 0.25);
}

.choices-error {
  color: #d9534f;
  font-size: 12px;
  margin-top: 4px;
}

Такой подход отделяет визуальную логику от бизнес-логики и позволяет гибко управлять внешним видом без изменения JavaScript-кода библиотеки.


Связь с состоянием выбора

Важный аспект — синхронизация ошибки с изменением значения. Ошибка должна автоматически исчезать при корректном вводе или выборе.

element.addEventListener('change', () => {
  const wrapper = element.closest('.choices');

  if (wrapper.classList.contains('is-invalid')) {
    wrapper.classList.remove('is-invalid');

    const error = wrapper.querySelector('.choices-error');
    if (error) error.remove();
  }
});

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


Перехват событий библиотеки

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

  • addItem
  • removeItem
  • change
  • choice

Пример реактивной валидации:

choices.passedElement.element.addEventListener('addItem', () => {
  clearError(element);
});

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

  if (values.length === 0) {
    showError(element, 'Выбор не может быть пустым');
  }
});

Интеграция с асинхронной валидацией

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

async function validateSelection(value) {
  const response = await fetch('/validate', {
    method: 'POST',
    body: JSON.stringify({ value }),
    headers: { 'Content-Type': 'application/json' }
  });

  return response.json();
}

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

element.addEventListener('change', async () => {
  const value = choices.getValue(true);

  const result = await validateSelection(value);

  if (!result.valid) {
    showError(element, result.message);
  }
});

Управление множественными ошибками

При сложной форме возможны ситуации, когда одно поле должно отображать несколько типов ошибок одновременно:

  • обязательность выбора
  • ограничения по количеству
  • бизнес-правила
  • серверная ошибка

В таких случаях применяется приоритетизация сообщений:

function resolveError(errors) {
  if (errors.required) return errors.required;
  if (errors.limit) return errors.limit;
  if (errors.server) return errors.server;
  return null;
}

Отображается только одно активное сообщение, чтобы избежать перегрузки интерфейса.


Доступность и ARIA-атрибуты

Визуальная индикация должна сопровождаться семантическими изменениями для экранных читалок. Choices.js позволяет модифицировать aria-invalid и aria-describedby.

function setAriaError(el, message) {
  el.setAttribute('aria-invalid', 'true');

  const id = `${el.id}-error`;
  let error = document.getElementById(id);

  if (!error) {
    error = document.createElement('div');
    error.id = id;
    error.className = 'choices-error';
    el.parentNode.appendChild(error);
  }

  error.textContent = message;
  el.setAttribute('aria-describedby', id);
}

При очистке ошибки атрибуты также должны возвращаться в исходное состояние.


Обработка состояния disabled и ошибок

Особое внимание требуется при сочетании ошибок и отключённого состояния. Поле, находящееся в состоянии disabled, не должно отображать ошибку как активную, даже если валидатор возвращает негативный результат.

if (element.disabled) {
  clearError(element);
  return;
}

Это предотвращает визуальные противоречия в интерфейсе.


Кастомизация через шаблоны

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

Пример расширения контейнера:

const choices = new Choices(element, {
  callbackOnCreateTemplates: function (template) {
    return {
      containerOuter: (classNames, data, hasFocus) => {
        const div = template(`
          <div class="${classNames.containerOuter}">
            <div class="choices__inner"></div>
            <div class="choices-error" data-error></div>
          </div>
        `);

        return div;
      }
    };
  }
});

Синхронизация с состоянием формы

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

const formState = {
  country: null,
  tags: []
};

function validateForm(state) {
  return {
    country: state.country ? null : 'Страна обязательна',
    tags: state.tags.length > 3 ? 'Максимум 3 элемента' : null
  };
}

После пересчёта состояния ошибки распределяются по соответствующим экземплярам Choices.


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

При использовании динамической загрузки (setChoices, clearChoices) важно учитывать сброс состояния ошибок. Изменение набора данных может автоматически обнулять валидность выбора.

choices.setChoices([], 'value', 'label', true);
clearError(element);

Это предотвращает ситуации, когда ошибка остаётся после изменения контекста выбора.


Практическая модель жизненного цикла ошибки

Поведение визуальной индикации можно описать как цикл:

  1. Ввод или изменение значения
  2. Проверка локальных правил
  3. Проверка внешних правил (при необходимости)
  4. Формирование состояния ошибки
  5. Отображение через классы и DOM
  6. Сброс при изменении валидного состояния

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