При работе с форматированными полями ввода, где используется Cleave.js, ключевая сложность заключается в разделении двух уровней состояния: визуального форматирования и фактической валидности данных. Библиотека отвечает за преобразование пользовательского ввода (маски, разделители, группы цифр, даты), но не выполняет полноценную бизнес-валидацию. Это создаёт необходимость построения независимого слоя индикации ошибок, который корректно взаимодействует с отформатированными значениями.
Основная задача системы ошибок в таком контексте — не конфликтовать с форматированием, обеспечивать мгновенную обратную связь и сохранять исходные данные в пригодном для проверки виде.
Cleave.js изменяет представление значения в DOM, но не изменяет бизнес-смысл введённого текста. Например, номер карты:
4111111111111111 → 4111 1111 1111 1111
С точки зрения визуального слоя значение выглядит структурированным, однако для проверки Luhn-алгоритма требуется «сырое» значение без пробелов.
Поэтому архитектурно выделяются два уровня:
В Cleave.js raw-значение можно получить через:
const cleave = new Cleave(input, {
creditCard: true
});
const rawValue = cleave.getRawValue();
Именно raw-значение становится источником истины для системы ошибок.
Индикация ошибок строится вокруг событий изменения ввода. Cleave.js не заменяет стандартные DOM-события, а дополняет их, поэтому используются:
inputchangeТипичная схема обработки:
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 [];
}
При каждом символе 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 требуют разных стратегий:
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;
}
Система индикации ошибок должна учитывать три состояния:
Ошибки не должны отображаться в состоянии частичного ввода, если это не критическая проверка формата.
Пример логики:
function shouldShowError(raw, touched) {
if (!touched) return false;
if (raw.length === 0) return false;
return true;
}
При использовании библиотек форм (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;
}
После получения массива ошибок они распределяются по полям и отображаются независимо.