HTML форми та вбудована валідація
HTML форми збирають введені дані через елементи <input>, <select>, <textarea> всередині <form>. Вбудована валідація (built-in validation) перевіряє ці дані за правилами на кшталт required або pattern перш ніж браузер відправляє запит.
Теорія
TL;DR
- Форма = контейнер з
action(куди надсилати) іmethod(POST/GET); inputs - це поля для введення даних - Вбудована валідація працює без JavaScript через атрибути
required,type="email",min/max,pattern - При помилці браузер блокує submit, показує підказку і активує псевдоклас
:invalid - Серверна перевірка обов'язкова - браузерну валідацію обходять через curl і прямі HTTP-запити
novalidateприбирає браузерний UI для помилок, алеcheckValidity()продовжує працювати в коді
Швидкий приклад
<form action="/submit" method="POST">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<!-- Порожній submit: "Будь ласка, заповніть це поле" -->
<!-- Неправильний формат: "Включіть '@' в адресу email" -->
<label for="age">Вік (від 18):</label>
<input type="number" id="age" name="age" min="18" required>
<!-- Вік 17: "Значення має бути >= 18" -->
<button type="submit">Надіслати</button>
</form>Два атрибути роблять усю роботу: type="email" повідомляє браузеру очікуваний формат, required блокує порожнє поле. JavaScript не потрібен.
Елементи форми
| Елемент | Призначення |
|---|---|
<input> | Текст, email, пароль, чекбокс, радіо, число, телефон, діапазон, дата |
<textarea> | Багаторядковий текст |
<select> | Випадний список |
<fieldset> | Групує пов'язані поля |
<label> | Прив'язує текст до поля (клік по label = фокус на input) |
<datalist> | Підказки автодоповнення для <input> |
Атрибути валідації
Кожен атрибут відповідає за конкретну перевірку браузера перед відправкою:
<input type="text" required> <!-- блокує порожнє значення -->
<input type="email"> <!-- перевіряє наявність @ та домену -->
<input type="number" min="1" max="100"> <!-- числовий діапазон -->
<input type="text" minlength="3" maxlength="50"> <!-- довжина рядка -->
<input type="text" pattern="[a-zA-Z0-9_-]+"
title="Лише літери, цифри, підкреслення, дефіс"> <!-- відповідність regex -->
<input type="tel" pattern="\d{4}-\d{4}-\d{4}-\d{4}"> <!-- формат картки -->Атрибут title задає текст підказки при невідповідності pattern. Без нього браузер показує загальне повідомлення.
Як валідація працює всередині
Браузер перетворює атрибути на об'єкт ValidityState для кожного поля. Його можна зчитати напряму:
const input = document.querySelector('#email');
console.log(input.validity.valueMissing); // true якщо порожнє + required
console.log(input.validity.typeMismatch); // true якщо неправильний формат
console.log(input.validity.valid); // true лише коли всі перевірки пройденоПри submit браузер викликає checkValidity() на кожному полі. Якщо хоча б одне повертає false, запит зупиняється, фокус переходить на перше проблемне поле, з'являється нативна підказка. Мережевого запиту немає. Псевдоклас :invalid активується в той самий момент.
CSS для станів валідації
input:valid { border-color: green; }
input:invalid { border-color: red; }
/* Не підсвічувати порожні поля червоним при завантаженні сторінки */
input:invalid:not(:focus):not(:placeholder-shown) {
border-color: red;
}Останній селектор вирішує поширену UX-проблему. Без нього всі required поля стають червоними одразу при завантаженні, ще до того як користувач щось заповнив.
novalidate та власний UI для помилок
Додай novalidate до форми, коли хочеш показувати власні повідомлення про помилки (toast, inline-текст), але все одно перевіряти валідність у коді:
<form id="payment" novalidate>
<input type="tel" name="card"
pattern="\d{4}-\d{4}-\d{4}-\d{4}" required>
<button type="submit" disabled>Оплатити</button>
</form>
<script>
const form = document.getElementById('payment');
const btn = form.querySelector('button');
form.addEventListener('input', () => {
btn.disabled = !form.checkValidity();
});
form.addEventListener('submit', (e) => {
if (!form.checkValidity()) {
e.preventDefault();
form.querySelectorAll(':invalid').forEach(field => {
// показати власний UI для помилок
});
}
});
</script>novalidate прибирає браузерний тултіп при submit. checkValidity() продовжує перевіряти дані в коді.
Типові помилки
Довіряти лише клієнтській перевірці
Браузерна валідація прозора для сервера. Бот може надіслати POST напряму через curl і оминути всі required атрибути. Завжди перевіряй на сервері.
// Express.js - дублюй правила HTML на сервері
if (!req.body.email.includes('@')) {
return res.status(400).json({ error: 'Невірний email' });
}required на прихованих полях
Приховані поля з required завжди провалюють валідацію, бо їхнє значення "". Це непомітно ламає багатокрокові форми.
<!-- Неправильно: завжди блокує submit -->
<input type="text" name="step" required style="display:none" value="">
<!-- Правильно: hidden input не потребує required -->
<input type="hidden" name="step" value="2">Забути атрибут name
Без name поле невидиме для FormData і ніколи не потрапляє на сервер. В code review це одна з найчастіших помилок, яку я бачив у джуніорів.
<!-- Неправильно: дані не надсилаються -->
<input id="username" required>
<!-- Правильно -->
<input id="username" name="username" required>Безумовний виклик setCustomValidity('')
Скидання кастомного повідомлення до перевірки стирає справжні помилки.
// Неправильно: стирає помилку на кожну input-подію
input.setCustomValidity('');
// Правильно: скидати лише коли поле дійсно валідне
input.setCustomValidity(input.validity.valid ? '' : 'Кастомне повідомлення про помилку');Де зустрічається в реальних проектах
- React Hook Form:
register('email', { required: true })безпосередньо маппиться на нативнийrequiredна input - Stripe: форми карток використовують
patternі:invalidдля відображення помилок в реальному часі - Поширений production-сетап:
novalidateна формі іexpress-validatorна сервері з тими самими правилами formnovalidateна кнопці "Зберегти чернетку" дозволяє зберігати незаповнені форми без спрацювання валідації
Питання на співбесіді
Q: Що відбувається при відправці форми з невалідними даними?
A: Браузер скасовує запит, активує псевдоклас :invalid, переводить фокус на перше проблемне поле і показує нативну підказку. form.reportValidity() запускає той самий процес вручну.
Q: Яка різниця між required і minlength="1"?
A: required блокує порожні значення, включно з рядками лише з пробілами. minlength="1" дозволяє порожній submit, але блокує короткий непорожній рядок.
Q: Чи можна запускати валідацію по blur, а не тільки по submit?
A: Так. Додай input.addEventListener('blur', () => input.reportValidity()), щоб показувати помилки одразу після того як користувач залишив поле.
Q: Що насправді робить novalidate?
A: Вимикає браузерний UI для тултіпів при submit. Не вимикає checkValidity() і псевдоклас :invalid. Senior-відповідь: використовують коли потрібен власний UI помилок (toast, modal), але програмна перевірка залишається.
Q: Як вбудована валідація взаємодіє з FormData?
A: FormData формується лише коли браузер вирішує відправити форму. Якщо валідація блокує submit, FormData не створюється і мережевого запиту немає.
Приклади
Базовий: форма реєстрації з вбудованими перевірками
<form action="/api/users" method="POST">
<label for="username">Username (3-20 символів):</label>
<input type="text" id="username" name="username"
required minlength="3" maxlength="20"
pattern="[a-zA-Z0-9_-]+"
title="Лише літери, цифри, підкреслення, дефіс">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<label for="password">Пароль (мін. 8 символів):</label>
<input type="password" id="password" name="password"
required minlength="8">
<button type="submit">Створити акаунт</button>
</form>
<style>
input:valid { border: 2px solid green; }
input:invalid { border: 2px solid red; }
</style>Username "ab " не відповідає pattern (пробіл не входить в клас символів) і показує "Лише літери, цифри, підкреслення, дефіс". Правильне значення дає зелений бордер, форма надсилає POST з усіма трьома полями через FormData.
Середній: форма у стилі GitHub з валідацією в реальному часі
<form id="signup" action="/api/register" method="POST" novalidate>
<fieldset>
<legend>Створи акаунт</legend>
<label for="user">Username:</label>
<input type="text" id="user" name="username"
required minlength="3" maxlength="39"
pattern="[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?">
<span class="error" id="user-error"></span>
<label for="mail">Email:</label>
<input type="email" id="mail" name="email" required>
<button type="submit">Зареєструватися</button>
</fieldset>
</form>
<script>
const form = document.getElementById('signup');
form.addEventListener('submit', (e) => {
if (!form.checkValidity()) {
e.preventDefault();
form.querySelectorAll(':invalid').forEach(field => {
const errorEl = document.getElementById(field.id + '-error');
if (errorEl) errorEl.textContent = field.validationMessage;
});
}
});
form.querySelectorAll('input').forEach(input => {
input.addEventListener('input', () => {
const errorEl = document.getElementById(input.id + '-error');
if (errorEl && input.validity.valid) errorEl.textContent = '';
});
});
</script>novalidate прибирає браузерний тултіп. field.validationMessage - це рядок від браузера на кшталт "Включіть '@' в адресу email". Тобто нативний текст помилки без написання власного.
Просунутий: форма оплати з кнопкою, заблокованою до валідності
<form id="checkout" novalidate>
<label for="card">Номер картки (XXXX-XXXX-XXXX-XXXX):</label>
<input type="tel" id="card" name="card"
pattern="\d{4}-\d{4}-\d{4}-\d{4}" required>
<label for="tip">Чайові (10-30%):</label>
<input type="range" id="tip" name="tip" min="10" max="30" value="15">
<button type="submit" id="pay-btn" disabled>Оплатити</button>
</form>
<script>
const form = document.getElementById('checkout');
const btn = document.getElementById('pay-btn');
function syncButton() {
btn.disabled = !form.checkValidity();
}
form.addEventListener('input', syncButton);
form.addEventListener('invalid', (e) => {
e.preventDefault();
e.target.setCustomValidity('Картка має бути у форматі XXXX-XXXX-XXXX-XXXX');
syncButton();
}, true); // фаза захоплення - спрацьовує на кожному невалідному полі
</script>Подія invalid захоплюється у фазі захоплення (true третім аргументом), тому спрацьовує на кожному полі, а не лише на формі. Кнопка залишається заблокованою поки form.checkValidity() не поверне true. Такий патерн використовують Stripe-подібні форми перед передачею даних у платіжний SDK.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.