Формы и валидация: Constraint Validation API
Формы — это не просто набор полей ввода. Это контракт между пользователем, DOM и сервером: какие данные собирать, как их проверять и как сообщать об ошибках. В этой статье разбираем встроенную валидацию HTML, Constraint Validation API и псевдоклассы валидации — темы, которые реально спрашивают на собеседованиях.
Содержание
- Глубокий разбор
- Практические примеры
- Типичные ошибки и антипаттерны
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Глубокий разбор
Как форма собирает данные
Любой элемент формы, у которого есть атрибут name, участвует в отправке. Браузер собирает пары name=value и либо строит из них application/x-www-form-urlencoded тело, либо передаёт в FormData.
<form id="profile">
<input name="email" value="user@example.com">
<input type="checkbox" name="newsletter" checked>
<button type="submit">Отправить</button>
</form>
const form = document.getElementById('profile');
const data = new FormData(form);
for (const [key, value] of data) {
console.log(key, value); // email user@example.com, newsletter on
}
Важный нюанс: кнопка <button type="submit"> внутри формы отправляет её, но если внутри формы несколько submit-кнопок, каждая может иметь собственные name и value, которые тоже попадают в данные при активации.
Встроенная валидация HTML
Браузер умеет проверять поля до отправки без единой строчки JS:
required— поле не должно быть пустым;type="email",type="url",type="number"— проверка формата;min/max— для чисел и дат;minlength/maxlength— для строк;pattern— регулярное выражение;step— для чисел и дат.
<input
type="email"
name="email"
required
minlength="5"
placeholder="user@example.com"
>
Если валидация не проходит, браузер блокирует отправку и показывает всплывающую подсказку с сообщением об ошибке. Это поведение можно переопределить через Constraint Validation API.
Constraint Validation API
Каждый элемент формы реализует интерфейс ConstraintValidation. Его ключевые методы и свойства:
willValidate— будет ли элемент валидироваться;checkValidity()— проверяет поле, не показывая UI;reportValidity()— проверяет поле и показывает нативную подсказку;setCustomValidity(message)— устанавливает кастомную ошибку;validity— объектValidityStateс флагами:valueMissing,typeMismatch,patternMismatch,tooShort,tooLong,rangeUnderflow,rangeOverflow,stepMismatch,badInput,customError,valid.
const input = document.querySelector('input[name="password"]');
input.addEventListener('input', () => {
if (input.value.length < 8) {
input.setCustomValidity('Пароль должен быть не короче 8 символов');
} else {
input.setCustomValidity(''); // сброс кастомной ошибки
}
});
Если setCustomValidity('') не вызвать, поле останется невалидным даже при корректном значении, потому что флаг customError будет установлен.
Псевдоклассы валидации
Браузер применяет к элементам псевдоклассы:
:valid/:invalid— поле проходит или не проходит валидацию;:user-valid/:user-invalid— то же самое, но только после взаимодействия пользователя с полем;:required/:optional— по наличию атрибутаrequired;:in-range/:out-of-range— для числовых полей.
input:user-invalid {
border-color: #dc2626;
}
input:user-valid {
border-color: #16a34a;
}
:user-valid и :user-invalid удобнее :valid/:invalid, потому что не подсвечивают поля красным сразу при загрузке страницы.
Событие submit и formdata
При отправке формы происходит последовательность:
- Событие
submitна<form>. - Если обработчик не отменил событие и форма валидна, браузер инициирует навигацию или отправку.
- Перед отправкой генерируется событие
formdataна<form>, в обработчике которого можно модифицировать данные черезFormData.
form.addEventListener('submit', (event) => {
event.preventDefault();
const data = new FormData(form);
fetch('/api/profile', { method: 'POST', body: data });
});
form.addEventListener('formdata', (event) => {
event.formData.append('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
});
ElementInternals: собственный элемент внутри формы
ElementInternals — это API, которое позволяет custom element участвовать в жизни формы: передавать значение, валидироваться и реагировать на reset и disabled. Это нишевый сценарий для библиотек компонентов, но его стоит знать как минимум на уровне «что это и зачем».
Чтобы связать custom element с формой:
- Объявить статическое свойство
formAssociated = true. - Получить
ElementInternalsчерезthis.attachInternals(). - Устанавливать значение через
internals.setFormValue(value).
class RatingElement extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this._internals = this.attachInternals();
this._value = '0';
}
connectedCallback() {
this.addEventListener('click', this);
this._internals.setFormValue(this._value);
}
handleEvent(event) {
if (event.type === 'click') {
this._value = event.target.dataset.value ?? this._value;
this._internals.setFormValue(this._value);
}
}
}
customElements.define('star-rating', RatingElement);
<form>
<star-rating name="rating"></star-rating>
<button type="submit">Отправить</button>
</form>
Для валидации используется setValidity(flags, message, anchor), где flags — объект ValidityStateFlags, а anchor — элемент для фокуса при ошибке. Также есть колбэки formAssociatedCallback, formDisabledCallback, formResetCallback, formStateRestoreCallback. Без formAssociated = true форма не увидит значение элемента.
Практические примеры
Пример 1: валидация пароля
<form id="signup">
<label for="password">Пароль</label>
<input
id="password"
name="password"
type="password"
minlength="8"
pattern="^(?=.*[A-Za-z])(?=.*\d).+$"
required
>
<span class="error" id="password-error"></span>
<button type="submit">Зарегистрироваться</button>
</form>
const form = document.getElementById('signup');
const password = form.elements.password;
const error = document.getElementById('password-error');
password.addEventListener('input', () => {
password.setCustomValidity('');
if (password.validity.valueMissing) {
password.setCustomValidity('Введите пароль');
} else if (password.validity.tooShort) {
password.setCustomValidity(`Минимум ${password.minLength} символов`);
} else if (password.validity.patternMismatch) {
password.setCustomValidity('Пароль должен содержать буквы и цифры');
}
error.textContent = password.validationMessage;
});
form.addEventListener('submit', (event) => {
if (!form.reportValidity()) {
event.preventDefault();
}
});
Пример 2: модификация данных перед отправкой
const form = document.getElementById('checkout');
form.addEventListener('formdata', (event) => {
const data = event.formData;
// нормализуем телефон
const phone = data.get('phone').replace(/\D/g, '');
data.set('phone', phone);
// добавляем метаданные
data.append('submittedAt', new Date().toISOString());
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (!form.reportValidity()) return;
await fetch('/api/order', {
method: 'POST',
body: new FormData(form)
});
});
Типичные ошибки и антипаттерны
- Валидация только на сервере. Клиентская валидация не заменяет серверную, но улучшает UX и уменьшает нагрузку. Делайте и то, и другое.
- Сообщения об ошибках висят в HTML и не синхронизируются с
validationMessage. Дублируйте состояние из API, чтобы скринридеры и пользователи видели одно и то же. - Использование
:invalidдля стилизации сразу после загрузки. При загрузке формы все обязательные пустые поля будут:invalid. Используй:user-invalidили классы послеreportValidity(). - Забытый
setCustomValidity(''). Если один раз установить кастомную ошибку, она останется навсегда. Сбрасывайте её, когда условие исправлено. - Отмена
submitбезevent.preventDefault()и безreportValidity(). Проверяйтеform.reportValidity()до отправки, иначе браузер может отправить невалидную форму. - Custom element без
formAssociated = trueпытается участвовать в форме. Без этого свойстваattachInternals()вернётinternals, но форма не увидит значение элемента. - Передача в
setFormValueне строки. Метод принимаетFormData,Fileили строку. Если передать объект, получится непредсказуемое поведение.
Ключевые тезисы для интервью
- Встроенная HTML-валидация работает через атрибуты
required,pattern,min/max,minlength/maxlengthи блокирует отправку формы при ошибках — без JavaScript. - Constraint Validation API даёт полный контроль:
checkValidity()проверяет без UI,reportValidity()показывает ошибки,setCustomValidity()задаёт кастомное сообщение; объектValidityStateсодержит флаги (valueMissing,typeMismatch,patternMismatchи др.). :user-validи:user-invalidудобнее:valid/:invalid, потому что не срабатывают до взаимодействия пользователя — не пугают красным сразу при загрузке формы.ElementInternalsпозволяет custom element участвовать в форме черезformAssociated = trueиsetFormValue()/setValidity()— нишевый сценарий для библиотек компонентов.- Событие
formdataпозволяет модифицировать данные формы непосредственно перед отправкой — удобная точка для добавления скрытых полей или трансформации значений. - Клиентская валидация — это UX, а не безопасность. Сервер всегда должен проверять данные повторно, так как клиентскую проверку можно обойти.
Заключение
Формы в HTML — целостная система: атрибуты задают правила, Constraint Validation API управляет проверкой, а псевдоклассы :user-valid/:user-invalid позволяют стилизовать состояния без излишней навязчивости. Событие formdata — удобная точка для модификации данных перед отправкой. ElementInternals — отдельный нишевый механизм для интеграции custom elements в формы; его достаточно знать на уровне «что это», если вы не пишете библиотеки компонентов. Клиентская валидация улучшает UX, но никогда не заменяет серверную проверку.
Во Vue этот фундамент сочетается с v-model, defineModel, composables и schema-валидацией — паттерны конкретно для Vue собраны в статье Формы и валидация во Vue.