Встроенная валидация и своя

формы≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Браузер умеет проверять формы сам: required, pattern, min — и половина работы сделана без единой строчки JavaScript. Разберёмся, как достать результат этой проверки из кода, как добавить к ней свои правила и почему сервер всё равно обязан проверить всё заново.

Разделы темы:·

Половина ограничений формы описывается не кодом, а атрибутами. required запрещает пустое значение, pattern сверяет строку с регулярным выражением, min и max ограничивают число или дату, step задаёт шаг, minlength и maxlength — длину. Браузер сам не пустит такую форму дальше и сам покажет подсказку на языке пользователя.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Два момента про pattern: он сверяется со строкой целиком, поэтому якоря ^ и $ писать не нужно, они подразумеваются. И у пустого поля pattern молчит — формат проверяется только у непустого значения, поэтому pattern почти всегда ходит в паре с required.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У поля нет единственного признака ошибки. Есть объект validity — набор булевых свойств, где каждое отвечает за свой вид нарушения, плюс итоговое valid. Рядом лежит validationMessage: тот самый текст, который браузер показал бы в пузыре.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
Что задали в разметкеКакой флаг загорится
requiredvalueMissing
type="email", type="url"typeMismatch
patternpatternMismatch
min / maxrangeUnderflow / rangeOverflow
stepstepMismatch
minlength / maxlengthtooShort / tooLong
setCustomValidity("текст")customError

Две тонкости. tooShort и tooLong загораются только от ввода человека — значение, присвоенное из скрипта, ограничение длины не нарушает. А поля disabled, readonly, кнопки и type="hidden" в проверке вообще не участвуют: у них willValidate равно false, и форма считает их валидными всегда.

Всё, чего браузер знать не может — совпадение паролей, сумма не больше остатка на счёте, дата выезда позже даты заезда, — подключается через setCustomValidity. Метод принимает строку: непустая делает поле невалидным и становится его validationMessage, пустая снимает ошибку. Ваше правило после этого живёт по общим законам: поле не пройдёт checkValidity, не даст отправить форму и попадёт под те же CSS-псевдоклассы.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

checkValidity() возвращает булево значение и попутно шлёт событие invalid каждому проблемному полю — тихо, без интерфейса. reportValidity() делает ровно то же самое, но вдобавок фокусирует первое нарушенное поле и показывает пузырь. У формы оба метода проверяют все её поля разом. Событие invalid не всплывает, поэтому слушать его на форме нужно в фазе перехвата — третьим аргументом true.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Именно reportValidity браузер вызывает сам, когда пользователь нажимает кнопку отправки. Пока форма невалидна, событие submit просто не наступает — и это видно без единого клика, если отправить форму из кода через requestSubmit().

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Стандартный пузырь нельзя ни подвинуть, ни перекрасить, и живёт он до первого клика мимо. Как только нужен свой текст под полем, форме ставят атрибут novalidate: браузер перестаёт блокировать отправку и показывать подсказки, но validity, checkValidity и setCustomValidity продолжают работать. Вы забираете себе только показ ошибок, а не сам механизм проверки — писать регулярки заново не приходится.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Текст ошибки видит зрячий пользователь — скринридеру о нём нужно сказать отдельно. Минимум: aria-invalid на поле, как в примере, и aria-describedby, связывающий поле с элементом сообщения.

CSS-псевдокласс :invalid срабатывает с первой секунды: пустое required-поле подсвечено ещё до того, как человек в него заглянул, — ощущается как обвинение авансом. Псевдокласс :user-invalid ждёт, пока пользователь попробует ввести значение или отправить форму — именно то поведение, которое раньше имитировали руками через классы вроде touched.

Всё, что описано выше, выполняется на машине пользователя. А значит, на машине пользователя же и отключается. Ниже — те самые две строки, которые любой человек введёт в консоли DevTools за десять секунд.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

И дело не только в злом умысле. Запрос к вашему API может прийти из мобильного приложения, из скрипта, из чужой интеграции — там никакого HTML не существует в принципе. Плюс целый класс правил браузеру просто недоступен: занят ли логин, действует ли промокод, остался ли товар на складе. Такие проверки приходят с сервера, а на клиенте лишь показываются — например, тем же setCustomValidity по ответу. Как пишут проверку на той стороне, где браузера нет и каждое присланное поле разбирают заново, показывает статья «Действие — это открытый эндпоинт»; собственный сервер на Node.js появится в части VI.

Браузерная валидация — про удобство, серверная — про доверие. Первая может ошибиться в вашу пользу, вторая не имеет права ошибиться никогда.

  • Атрибуты required, pattern, min, max, step, minlength и maxlength — это уже валидация: браузер сам блокирует отправку и показывает подсказку.
  • Объект validity разбирает ошибку на конкретные флаги — valueMissing, typeMismatch, patternMismatch, rangeOverflow и прочие, — а validationMessage хранит готовый текст.
  • setCustomValidity подключает собственные правила к тому же механизму; пустая строка снимает ошибку, и забыть про неё — самый частый баг.
  • checkValidity проверяет молча и шлёт невсплывающее событие invalid, reportValidity ещё и фокусирует поле с пузырём; novalidate отключает только показ, а не сам API.
  • Клиентская проверка снимается двумя строками в консоли и не существует для запросов мимо браузера, поэтому сервер обязан проверять данные заново.

1/3Поле <input required pattern="[0-9]{4}"> пустое. Какие флаги validity истинны?

2/3Что делает setCustomValidity("") на поле, которому раньше задали своё сообщение?

3/3Форме поставили атрибут novalidate. Что изменится?

Подтверждение пароля

Второе поле должно быть невалидным, пока его значение не совпадает с первым, и сообщение должно попадать в validationMessage. Допишите функцию sync так, чтобы оба console.log напечатали true.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

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

Список проблемных полей

Напишите функцию, которая возвращает массив имён полей формы, не проходящих проверку. Осторожно: form.elements содержит не только поля ввода.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Коллекцию form.elements превращаем в массив и фильтруем:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверка willValidate здесь не для красоты: в form.elements попадают кнопки и элемент output, которые в валидации не участвуют вовсе, — без фильтра в массив полезут элементы с пустым именем. Готовый список удобно использовать для своего блока ошибок над формой.