Задача
СкопированоКто из нас не знаком с той неприятной ситуацией, когда усердно заполняешь форму, вводишь данные, а потом, с надеждой нажимая на кнопку «Отправить», обнаруживаешь, что что-то пошло не так и все усилия пропали даром? Для этого есть решение — мгновенная валидация при помощи JavaScript!
Отличный пользовательский опыт — ключ к успеху. Валидация форм с помощью HTML не может предоставить того уровня UX (пользовательского опыта), который требуется стандартами веб-разработки. Ему на помощь приходит валидация через JavaScript. Она обеспечивает мгновенную обратную связь при заполнении формы и аккуратно подсказывает, что нужно исправить прежде чем форма отправится.
Готовое решение
СкопированоПример стандартной HTML-разметки формы:
<form class="form" name="form" novalidate> <div class="form__field-container"> <label class="form__field"> <span class="form__label">Имя:</span> <input type="text" id="input__name" class="form__type-input" placeholder="Иван" pattern="^[a-zA-Zа-яА-ЯЁё\s\-]+$" data-error-message="Разрешены символы латиницы, кириллицы, знаки дефиса и пробелы." aria-describedby="name-error" required > </label> <span class="form__error input__name-error" id="name-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__field"> <span class="form__label">Фамилия:</span> <input type="text" id="input__surname" class="form__type-input" placeholder="Васильевич" pattern="^[a-zA-Zа-яА-ЯЁё\s\-]+$" data-error-message="Разрешены символы латиницы, кириллицы, знаки дефиса и пробелы." aria-describedby="surname-error" required > </label> <span class="form__error input__surname-error" id="surname-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__field"> <span class="form__label">Почта:</span> <input type="email" id="input__e-mail" class="form__type-input" placeholder="menyaet.professiyu@ivan.com" aria-describedby="email-error" required > </label> <span class="form__error input__e-mail-error" id="email-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__field"> <span class="form__label">Возраст:</span> <input type="number" id="input__age" class="form__type-input" placeholder="40" min="18" max="100" aria-describedby="age-error" required > </label> <span class="form__error input__age-error" id="age-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__checkbox-label"> <input type="checkbox" id="input__checkbox" class="form__type-input form__type-checkbox" checked aria-describedby="checkbox-error" required /> <span class="form__type-checkbox-title"> Я согласен быть царём </span> </label> <span class="form__error input__checkbox-error" id="checkbox-error" aria-live="polite"></span> </div> <button type="submit" class="button" aria-describedby="empty-error" > Отправить </button> <span class="form__empty-error" id="empty-error" aria-live="assertive" > </span></form>
<form class="form" name="form" novalidate > <div class="form__field-container"> <label class="form__field"> <span class="form__label">Имя:</span> <input type="text" id="input__name" class="form__type-input" placeholder="Иван" pattern="^[a-zA-Zа-яА-ЯЁё\s\-]+$" data-error-message="Разрешены символы латиницы, кириллицы, знаки дефиса и пробелы." aria-describedby="name-error" required > </label> <span class="form__error input__name-error" id="name-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__field"> <span class="form__label">Фамилия:</span> <input type="text" id="input__surname" class="form__type-input" placeholder="Васильевич" pattern="^[a-zA-Zа-яА-ЯЁё\s\-]+$" data-error-message="Разрешены символы латиницы, кириллицы, знаки дефиса и пробелы." aria-describedby="surname-error" required > </label> <span class="form__error input__surname-error" id="surname-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__field"> <span class="form__label">Почта:</span> <input type="email" id="input__e-mail" class="form__type-input" placeholder="menyaet.professiyu@ivan.com" aria-describedby="email-error" required > </label> <span class="form__error input__e-mail-error" id="email-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__field"> <span class="form__label">Возраст:</span> <input type="number" id="input__age" class="form__type-input" placeholder="40" min="18" max="100" aria-describedby="age-error" required > </label> <span class="form__error input__age-error" id="age-error" aria-live="polite" > </span> </div> <div class="form__field-container"> <label class="form__checkbox-label"> <input type="checkbox" id="input__checkbox" class="form__type-input form__type-checkbox" checked aria-describedby="checkbox-error" required /> <span class="form__type-checkbox-title"> Я согласен быть царём </span> </label> <span class="form__error input__checkbox-error" id="checkbox-error" aria-live="polite"></span> </div> <button type="submit" class="button" aria-describedby="empty-error" > Отправить </button> <span class="form__empty-error" id="empty-error" aria-live="assertive" > </span> </form>
Код JavaScript для валидации всех полей формы:
const form = document.querySelector('.form')const inputList = Array.from(form.querySelectorAll('.form__type-input'))const buttonElement = form.querySelector('.button')const formErrorElement = form.querySelector('.form__empty-error')startValidation()function startValidation() { toggleButton() form.addEventListener('submit', (event) => { event.preventDefault() if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) })}function checkInputValidity(inputElement) { if (inputElement.validity.patternMismatch) { inputElement.setCustomValidity(inputElement.dataset.errorMessage) } else { inputElement.setCustomValidity(checkLengthMismatch(inputElement)) } if (!inputElement.validity.valid) { toggleErrorSpan(inputElement, inputElement.validationMessage) } else { toggleErrorSpan(inputElement) }}function checkLengthMismatch(inputElement) { if (inputElement.type !== 'text') { return '' } const valueLength = inputElement.value.trim().length if (valueLength < inputElement.minLength) { return `Минимальное количество символов: ${inputElement.minLength}` } return ''}function hasInvalidInput() { return inputList.some((inputElement) => { return !inputElement.validity.valid })}function toggleErrorSpan(inputElement, errorMessage){ const errorElement = document.querySelector(`.${inputElement.id}-error`) if (errorMessage) { inputElement.classList.add('form__type-input-error') errorElement.textContent = errorMessage errorElement.classList.add('form__error-active') } else { inputElement.classList.remove('form__type-input-error') errorElement.textContent = '' errorElement.classList.remove('form__error-active') }}function toggleButton() { if (hasInvalidInput()) { buttonElement.classList.add('button-inactive') buttonElement.setAttribute('aria-disabled', 'true') } else { buttonElement.classList.remove('button-inactive') buttonElement.setAttribute('aria-disabled', 'false') formErrorElement.textContent = '' }}function formError() { const errorMessage = 'Заполните все поля для отправки формы.' formErrorElement.textContent = errorMessage}
const form = document.querySelector('.form') const inputList = Array.from(form.querySelectorAll('.form__type-input')) const buttonElement = form.querySelector('.button') const formErrorElement = form.querySelector('.form__empty-error') startValidation() function startValidation() { toggleButton() form.addEventListener('submit', (event) => { event.preventDefault() if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) }) } function checkInputValidity(inputElement) { if (inputElement.validity.patternMismatch) { inputElement.setCustomValidity(inputElement.dataset.errorMessage) } else { inputElement.setCustomValidity(checkLengthMismatch(inputElement)) } if (!inputElement.validity.valid) { toggleErrorSpan(inputElement, inputElement.validationMessage) } else { toggleErrorSpan(inputElement) } } function checkLengthMismatch(inputElement) { if (inputElement.type !== 'text') { return '' } const valueLength = inputElement.value.trim().length if (valueLength < inputElement.minLength) { return `Минимальное количество символов: ${inputElement.minLength}` } return '' } function hasInvalidInput() { return inputList.some((inputElement) => { return !inputElement.validity.valid }) } function toggleErrorSpan(inputElement, errorMessage){ const errorElement = document.querySelector(`.${inputElement.id}-error`) if (errorMessage) { inputElement.classList.add('form__type-input-error') errorElement.textContent = errorMessage errorElement.classList.add('form__error-active') } else { inputElement.classList.remove('form__type-input-error') errorElement.textContent = '' errorElement.classList.remove('form__error-active') } } function toggleButton() { if (hasInvalidInput()) { buttonElement.classList.add('button-inactive') buttonElement.setAttribute('aria-disabled', 'true') } else { buttonElement.classList.remove('button-inactive') buttonElement.setAttribute('aria-disabled', 'false') formErrorElement.textContent = '' } } function formError() { const errorMessage = 'Заполните все поля для отправки формы.' formErrorElement.textContent = errorMessage }
CSS-стили, которые будут использоваться при валидации:
/* Для изменения цвета обводки элемента формы при валидации */.form__type-input-error { border: 1px solid #FF8630; background-color: rgb(255 134 48 / 0.1);}/* Для отображения span-элемента с ошибкой */.form__error-active { display: block;}/* Для блокировки кнопки submit */.button-inactive { cursor: default; background-color: rgb(211 211 211 / 0.6);}
/* Для изменения цвета обводки элемента формы при валидации */ .form__type-input-error { border: 1px solid #FF8630; background-color: rgb(255 134 48 / 0.1); } /* Для отображения span-элемента с ошибкой */ .form__error-active { display: block; } /* Для блокировки кнопки submit */ .button-inactive { cursor: default; background-color: rgb(211 211 211 / 0.6); }
Разбор решения
СкопированоСначала сообщаем браузеру, что он не должен валидировать форму стандартным способом, добавляя атрибут novalidate
к тегу <form>
.
<form class="form__field" novalidate> <!-- Содержимое формы --></form>
<form class="form__field" novalidate> <!-- Содержимое формы --> </form>
Разметка
СкопированоВзгляните на пример разметки элемента формы, чтобы лучше понять, как осуществляется валидация.
Добавим атрибуты:
type
— определяет ожидаемый тип данных в поле.placeholder
— предоставляет подсказку пользователю о том, какие данные нужно ввести.required
— указывает на обязательность заполнения поля.
Свяжем поле ввода и <span>
с ошибкой с помощью идентификаторов и классов CSS. Задаём идентификатор для <input>
и присваиваем для <span>
аналогичный класс, добавляя '-error' в конце. Это позволит найти <span>
в DOM
по такой схеме: document
. Чтобы эта связь между полем и ошибкой к нему была понятна и пользователям вспомогательных технологий, свяжем их атрибутом aria
у поля и кнопки и id
с таким же значением у <span>
. Чтобы вспомогательные технологии рассказывали о них автоматически, добавим ещё другой ARIA-атрибут aria
.
Настроим параметры валидации. Тут можно использовать как стандартные атрибуты — maxlength
/minlength
, так и нестандартные атрибуты типа pattern
с регулярными выражениями. Последний позволяет настроить более точные и специфические правила для полей ввода.
Подробнее о pattern
: хотя в большинстве случаев стандартных сообщений валидации достаточно, иногда возникает необходимость в более специфических требованиях к полям ввода. Возьмём, к примеру, ситуацию, когда требуется ввод только букв латиницы и кириллицы, дефисов и пробелов. Такой набор символов не предусмотрен стандартной валидацией, что делает необходимым использование кастомной валидации. Для этого используем регулярное выражение и записываем кастомное сообщение об ошибке в специально созданный data-атрибут — 'data-error-message'. Подробнее о data-атрибутах можно прочитать в доке про атрибуты data
.
<label class="form__field"> <span class="form__label">Имя:</span> <input type="text" id="input__name" class="form__type-input" placeholder="Иван" pattern="^[a-zA-Zа-яА-ЯЁё\s\-]+$" data-error-message="Разрешены символы латиницы, кириллицы, знаки дефиса и пробелы." aria-describedby="name-error" required ></label><span class="form__error input__name-error" id="name-error" aria-live="polite"></span>
<label class="form__field"> <span class="form__label">Имя:</span> <input type="text" id="input__name" class="form__type-input" placeholder="Иван" pattern="^[a-zA-Zа-яА-ЯЁё\s\-]+$" data-error-message="Разрешены символы латиницы, кириллицы, знаки дефиса и пробелы." aria-describedby="name-error" required > </label> <span class="form__error input__name-error" id="name-error" aria-live="polite" > </span>
JavaScript
СкопированоСначала собираем все необходимые DOM-элементы для валидации:
const form = document.querySelector('.form')const inputList = Array.from(form.querySelectorAll('.form__type-input'))const buttonElement = form.querySelector('.button')const formErrorElement = form.querySelector('.form__empty-error')
const form = document.querySelector('.form') const inputList = Array.from(form.querySelectorAll('.form__type-input')) const buttonElement = form.querySelector('.button') const formErrorElement = form.querySelector('.form__empty-error')
Функция start
инициирует процесс валидации. Она добавляет обработчик событий для всей формы на событие submit
, где используется event
для предотвращения стандартного поведения формы при отправке. Для дополнительной информации читайте «Работа с формами».
На каждый элемент формы назначаются обработчики события input
. Они активируют функции check
и toggle
при любых изменениях в полях ввода. Их мы напишем далее.
function startValidation() { form.addEventListener('submit', (event) => { event.preventDefault() if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) })}
function startValidation() { form.addEventListener('submit', (event) => { event.preventDefault() if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) }) }
Функция check
использует JavaScript-объект Validity
для проверки каждого поля ввода. Если поле не валидно, показывается сообщение об ошибке.
Объект validity
можно увидеть, если обратиться к ключу validity
элемента input
(input
). Он выглядит вот так:
{ badInput: false, customError: false, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, valid: true, valueMissing: false,}
{ badInput: false, customError: false, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, valid: true, valueMissing: false, }
Чтобы поле ввода считалось валидным, его свойство input
должно быть равно true
. Это свойство становится true
, когда все остальные свойства в объекте validity
равны false
. Подробнее о validity
и значении каждого ключа в этом объекте можно узнать здесь: MDN Web Docs — ValidityState.
В функции check
как раз используется объект validity
и его ключи pattern
для кастомных ошибок и valid
для стандартной проверки.
Сначала проверяется задан ли для поля ввода определённый паттерн и установлена ли минимальная длина. Если паттерн задан и не совпадает с введёнными данными, то с помощью функции set
передаётся кастомное сообщение об ошибке, хранящееся в атрибуте data
. В случае соответствия введённых данных паттерну, с помощью функции check
также проверяется длина введённых данных, очищенная от пробелов. Если сообщение больше установленного количества символов и не пустое, то сообщение об ошибке не передаётся, в ином случае — пользователь получает сообщение с минимально необходимым количеством символов.
Затем проводится стандартная проверка: если свойство input
равно false
, выводится сообщение об ошибке, а если true
— ошибка убирается.
function checkInputValidity(inputElement) { if (inputElement.validity.patternMismatch) { inputElement.setCustomValidity(inputElement.dataset.errorMessage) } else { inputElement.setCustomValidity(checkLengthMismatch(inputElement)) } if (!inputElement.validity.valid) { toggleErrorSpan(inputElement, inputElement.validationMessage) } else { toggleErrorSpan(inputElement) }}function checkLengthMismatch(inputElement) { if (inputElement.type !== 'text') { return '' } const valueLength = inputElement.value.trim().length if (valueLength < inputElement.minLength) { return `Минимальное количество символов: ${inputElement.minLength}` } return ''}
function checkInputValidity(inputElement) { if (inputElement.validity.patternMismatch) { inputElement.setCustomValidity(inputElement.dataset.errorMessage) } else { inputElement.setCustomValidity(checkLengthMismatch(inputElement)) } if (!inputElement.validity.valid) { toggleErrorSpan(inputElement, inputElement.validationMessage) } else { toggleErrorSpan(inputElement) } } function checkLengthMismatch(inputElement) { if (inputElement.type !== 'text') { return '' } const valueLength = inputElement.value.trim().length if (valueLength < inputElement.minLength) { return `Минимальное количество символов: ${inputElement.minLength}` } return '' }
Функция toggle
устроена просто: она блокирует кнопку, когда находит невалидные поля, и вновь активирует её, если все поля заполнены корректно. Функция has
проверяет поля ввода на наличие ошибок и возвращает true
или false
, основываясь на том, обнаружены ли невалидные данные.
Блокировка кнопки отправки формы — рискованный приём. Важно учитывать различные варианты поведения пользователя. Чтобы избежать ситуаций, когда пользователь не понимает причину блокировки кнопки, мы принимаем следующие меры:
- Применяем класс
button
, который изменяет цвет кнопки на менее яркий, подсказывая пользователю, что нажатие невозможно.- inactive - Добавляем через этот класс свойства
cursor
, которое меняет форму курсора на символ запрета.: not - allowed; - При клике по кнопке отправки формы или при нажатии на неё с клавиатуры показываем ошибку, которая объясняет причину блокировки. Реализуем это с помощью JavaScript.
- В случае ввода невалидных данных в одно из полей, пользователь моментально получает обратную связь о допущенной ошибке.
- Чтобы заблокированная кнопка была заметна пользователям, перемещающимся по сайту с помощью клавиши Tab, мы добавляем к кнопке атрибут
aria
.- disabled
Так же напоминаем, что при блокировке кнопки отправки формы важно удостовериться, что требования к заполнению формы разумны и могут быть выполнены всеми пользователями. Следует избегать установления чрезмерно строгих условий для данных, вводимых пользователем. К примеру, пользователь может столкнуться с тем, что его имя слишком длинное для установленного в форме ограничения в 10 символов, что сделает невозможным отправку формы и ограничит доступ к вашему продукту.
function toggleButton() { if (hasInvalidInput()) { buttonElement.classList.add('button-inactive') buttonElement.setAttribute('aria-disabled', 'true') } else { buttonElement.classList.remove('button-inactive') buttonElement.setAttribute('aria-disabled', 'false') formErrorElement.textContent = '' }}function hasInvalidInput() { return inputList.some((inputElement) => { return !inputElement.validity.valid })}function formError() { const errorMessage = 'Заполните все поля для отправки формы.' formErrorElement.textContent = errorMessage}
function toggleButton() { if (hasInvalidInput()) { buttonElement.classList.add('button-inactive') buttonElement.setAttribute('aria-disabled', 'true') } else { buttonElement.classList.remove('button-inactive') buttonElement.setAttribute('aria-disabled', 'false') formErrorElement.textContent = '' } } function hasInvalidInput() { return inputList.some((inputElement) => { return !inputElement.validity.valid }) } function formError() { const errorMessage = 'Заполните все поля для отправки формы.' formErrorElement.textContent = errorMessage }
.button-inactive { cursor: not-allowed; background-color: rgb(211 211 211 / 0.6);}.button-inactive:hover { background-color: rgb(211 211 211 / 0.2); border: 2px solid transparent;}.form__empty-error { padding: 10px 0; font-size: 18px; color: #FF8630;}
.button-inactive { cursor: not-allowed; background-color: rgb(211 211 211 / 0.6); } .button-inactive:hover { background-color: rgb(211 211 211 / 0.2); border: 2px solid transparent; } .form__empty-error { padding: 10px 0; font-size: 18px; color: #FF8630; }
Осталось самое лёгкое — сделать активными элементы с ошибками. Если поле ввода оказалось невалидным, то скрипт показывает заранее подготовленный элемент с сообщением об ошибке. Если поле становится валидным, то сообщение исчезает. Именно в этой функции нам пригодился трюк, где мы создавали класс ошибки по следующему шаблону: id input
function toggleErrorSpan(inputElement, errorMessage){ const errorElement = document.querySelector(`.${inputElement.id}-error`) if (errorMessage) { inputElement.classList.add('form__type-input-error') errorElement.textContent = errorMessage errorElement.classList.add('form__error-active') } else { inputElement.classList.remove('form__type-input-error') errorElement.textContent = '' errorElement.classList.remove('form__error-active') }}
function toggleErrorSpan(inputElement, errorMessage){ const errorElement = document.querySelector(`.${inputElement.id}-error`) if (errorMessage) { inputElement.classList.add('form__type-input-error') errorElement.textContent = errorMessage errorElement.classList.add('form__error-active') } else { inputElement.classList.remove('form__type-input-error') errorElement.textContent = '' errorElement.classList.remove('form__error-active') } }
Также позаботимся об ошибке про пустую форму при клике или нажатии с клавиатуры на кнопку.
const formErrorElement = form.querySelector('.form__empty-error')function startValidation() { toggleButton() form.addEventListener('submit', (event) => { event.preventDefault() // Показываем ошибку if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) })}function toggleButton() { if (hasInvalidInput()) { buttonElement.classList.add('button-inactive') buttonElement.setAttribute('aria-disabled', 'true') } else { buttonElement.classList.remove('button-inactive') buttonElement.setAttribute('aria-disabled', 'false') // Удаляем текст ошибки formErrorElement.textContent = '' }}// Здесь храним и добавляем текст к нужному контейнеруfunction formError() { const errorMessage = 'Заполните все поля для отправки формы.' formErrorElement.textContent = errorMessage}
const formErrorElement = form.querySelector('.form__empty-error') function startValidation() { toggleButton() form.addEventListener('submit', (event) => { event.preventDefault() // Показываем ошибку if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) }) } function toggleButton() { if (hasInvalidInput()) { buttonElement.classList.add('button-inactive') buttonElement.setAttribute('aria-disabled', 'true') } else { buttonElement.classList.remove('button-inactive') buttonElement.setAttribute('aria-disabled', 'false') // Удаляем текст ошибки formErrorElement.textContent = '' } } // Здесь храним и добавляем текст к нужному контейнеру function formError() { const errorMessage = 'Заполните все поля для отправки формы.' formErrorElement.textContent = errorMessage }
Наш код готов! Не забудьте:
- Добавить в самое начало функции
start
функциюValidation ( ) toggle
, чтобы ещё до ввода символов кнопка была заблокирована.Button ( ) - Вызвать функцию
start
.Validation ( )
// Вызываем функциюstartValidation()function startValidation() { toggleButton() form.addEventListener('submit', (event) => { event.preventDefault() if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) })}
// Вызываем функцию startValidation() function startValidation() { toggleButton() form.addEventListener('submit', (event) => { event.preventDefault() if (hasInvalidInput()) { formError() } }) inputList.forEach((inputElement) => { inputElement.addEventListener('input', () => { checkInputValidity(inputElement) toggleButton() }) }) }