Индикация ошибок для пользователей

При работе с форматированными полями ввода, где используется Cleave.js, ключевая сложность заключается в разделении двух уровней состояния: визуального форматирования и фактической валидности данных. Библиотека отвечает за преобразование пользовательского ввода (маски, разделители, группы цифр, даты), но не выполняет полноценную бизнес-валидацию. Это создаёт необходимость построения независимого слоя индикации ошибок, который корректно взаимодействует с отформатированными значениями.

Основная задача системы ошибок в таком контексте — не конфликтовать с форматированием, обеспечивать мгновенную обратную связь и сохранять исходные данные в пригодном для проверки виде.


Разделение форматирования и валидации

Cleave.js изменяет представление значения в DOM, но не изменяет бизнес-смысл введённого текста. Например, номер карты:

4111111111111111 → 4111 1111 1111 1111

С точки зрения визуального слоя значение выглядит структурированным, однако для проверки Luhn-алгоритма требуется «сырое» значение без пробелов.

Поэтому архитектурно выделяются два уровня:

  • Formatted value — отображаемое значение input
  • Raw value — очищенное значение, используемое для проверки

В Cleave.js raw-значение можно получить через:

const cleave = new Cleave(input, {
  creditCard: true
});

const rawValue = cleave.getRawValue();

Именно raw-значение становится источником истины для системы ошибок.


Событийная модель контроля ошибок

Индикация ошибок строится вокруг событий изменения ввода. Cleave.js не заменяет стандартные DOM-события, а дополняет их, поэтому используются:

  • input
  • change
  • пользовательские обработчики Cleave.js

Типичная схема обработки:

input.addEventListener('input', () => {
  validate(input, cleave.getRawValue());
});

Ключевой принцип: валидация должна выполняться после того, как Cleave.js завершил форматирование текущего символа.


Базовая структура валидатора

Слой валидации отделяется от Cleave.js и работает исключительно с чистыми данными:

function validate(input, value) {
  const errors = [];

  if (value.length === 0) {
    errors.push('EMPTY');
  }

  if (value.length < 16) {
    errors.push('TOO_SHORT');
  }

  setErrorState(input, errors);
}

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


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

После вычисления состояния ошибок требуется синхронизация с DOM. Обычно используются CSS-классы и ARIA-атрибуты:

function setErrorState(input, errors) {
  const hasError = errors.length > 0;

  input.classList.toggle('input-error', hasError);
  input.setAttribute('aria-invalid', hasError);

  const errorBox = document.getElementById('error-box');
  errorBox.textContent = hasError ? getErrorMessage(errors[0]) : '';
}

CSS-слой:

.input-error {
  border-color: #e74c3c;
  background-color: #fff5f5;
}

Важный аспект: Cleave.js может перерисовывать значение, но не должен влиять на классы состояния.


Конфликты форматирования и ошибок

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

Например:

1234 56__

С точки зрения UI это может восприниматься как заполненное поле, хотя raw-значение неполное.

Решение заключается в опоре на raw value:

const raw = cleave.getRawValue();

const isIncomplete = raw.length !== expectedLength;

Форматирование не используется как критерий валидности.


Ошибки в масках дат

При использовании режимов даты Cleave.js:

new Cleave(input, {
  date: true,
  datePattern: ['d', 'm', 'Y']
});

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

Пример стратегии:

function validateDate(raw) {
  if (raw.length < 8) {
    return ['INCOMPLETE_DATE'];
  }

  const [day, month, year] = parseDate(raw);

  if (month > 12) return ['INVALID_MONTH'];
  if (day > 31) return ['INVALID_DAY'];

  return [];
}

Debounce и предотвращение «мигания» ошибок

При каждом символе Cleave.js инициирует перерасчёт значения. Без сглаживания это приводит к частому переключению состояния ошибки.

Используется debounce:

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

const validateDebounced = debounce(validate, 150);

input.addEventListener('input', () => {
  validateDebounced(input, cleave.getRawValue());
});

Это особенно важно для сложных проверок (IBAN, карты, даты).


Кастомные ошибки и кодовая модель

Для масштабируемости вводится система кодов ошибок вместо строк:

const ERROR_CODES = {
  EMPTY: 'EMPTY',
  TOO_SHORT: 'TOO_SHORT',
  INVALID_FORMAT: 'INVALID_FORMAT',
  CHECKSUM_FAILED: 'CHECKSUM_FAILED'
};

Маппинг на сообщения выполняется отдельно:

function getErrorMessage(code) {
  switch (code) {
    case ERROR_CODES.EMPTY:
      return 'Поле не заполнено';
    case ERROR_CODES.TOO_SHORT:
      return 'Слишком короткое значение';
    case ERROR_CODES.CHECKSUM_FAILED:
      return 'Контрольная сумма не совпадает';
  }
}

Такой подход позволяет менять локализацию без изменения логики.


Валидация с учётом типов Cleave.js

Разные режимы Cleave.js требуют разных стратегий:

Телефонные номера

new Cleave(input, {
  phone: true,
  phoneRegionCode: 'US'
});

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

Кредитные карты

new Cleave(input, {
  creditCard: true
});

Дополнительно применяется Luhn-check:

function luhnCheck(number) {
  let sum = 0;
  let alt = false;

  for (let i = number.length - 1; i >= 0; i--) {
    let n = parseInt(number[i], 10);

    if (alt) {
      n *= 2;
      if (n > 9) n -= 9;
    }

    sum += n;
    alt = !alt;
  }

  return sum % 10 === 0;
}

Состояния ошибки и UX-переходы

Система индикации ошибок должна учитывать три состояния:

  • отсутствие ввода
  • частичный ввод
  • завершённый ввод

Ошибки не должны отображаться в состоянии частичного ввода, если это не критическая проверка формата.

Пример логики:

function shouldShowError(raw, touched) {
  if (!touched) return false;
  if (raw.length === 0) return false;
  return true;
}

Синхронизация Cleave.js с внешними формами

При использовании библиотек форм (React-style или vanilla form handlers) Cleave.js выступает как слой представления.

При отправке формы важно извлекать raw value:

form.addEventListener('submit', (e) => {
  e.preventDefault();

  const value = cleave.getRawValue();
  const errors = validate(value);

  if (errors.length) {
    setErrorState(input, errors);
    return;
  }

  submit(value);
});

Ошибки при динамическом изменении масок

Cleave.js позволяет изменять конфигурацию после инициализации. Это приводит к изменению структуры данных:

cleave.setRawValue('');
cleave.destroy();
cleave = new Cleave(input, newConfig);

После таких операций состояние ошибок должно быть сброшено или пересчитано, иначе возникает рассинхронизация UI и данных.


Комбинированные поля и агрегированная валидация

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

function validateForm(state) {
  const errors = [];

  if (!luhnCheck(state.cardNumber)) {
    errors.push({ field: 'cardNumber', code: 'CHECKSUM_FAILED' });
  }

  if (state.expiryMonth > 12) {
    errors.push({ field: 'expiryMonth', code: 'INVALID_MONTH' });
  }

  return errors;
}

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