Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «HTML форми та вбудована валідація». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Валідація HTML форм** - `<form>` групує елементи введення і відправляє дані на сервер; вбудована валідація (built-in validation) перевіряє дані в браузері через `required`, `type="email"`, `pattern` до будь-якого мережевого запиту. ```html <input type="email" name="email" required> <!-- Блокує порожній submit і неправильний формат --> ``` **Ключове:** браузерна перевірка виконується на стороні клієнта. На сервері завжди валідуй знову.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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()` продовжує працювати в коді ### Швидкий приклад ```html <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>` | ### Атрибути валідації Кожен атрибут відповідає за конкретну перевірку браузера перед відправкою: ```html <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` для кожного поля. Його можна зчитати напряму: ```js 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 для станів валідації ```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-текст), але все одно перевіряти валідність у коді: ```html <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` атрибути. Завжди перевіряй на сервері. ```js // Express.js - дублюй правила HTML на сервері if (!req.body.email.includes('@')) { return res.status(400).json({ error: 'Невірний email' }); } ``` **`required` на прихованих полях** Приховані поля з `required` завжди провалюють валідацію, бо їхнє значення `""`. Це непомітно ламає багатокрокові форми. ```html <!-- Неправильно: завжди блокує 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 це одна з найчастіших помилок, яку я бачив у джуніорів. ```html <!-- Неправильно: дані не надсилаються --> <input id="username" required> <!-- Правильно --> <input id="username" name="username" required> ``` **Безумовний виклик `setCustomValidity('')`** Скидання кастомного повідомлення до перевірки стирає справжні помилки. ```js // Неправильно: стирає помилку на кожну 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` не створюється і мережевого запиту немає. ## Приклади ### Базовий: форма реєстрації з вбудованими перевірками ```html <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 з валідацією в реальному часі ```html <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". Тобто нативний текст помилки без написання власного. ### Просунутий: форма оплати з кнопкою, заблокованою до валідності ```html <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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.