Пошук уроків, статей та іншого контенту
Створюватимете інтерактивні форми, перевірятимете значення полів і керуватимете їхнім станом.
Форма — це спосіб отримати дані від користувача: текст, email, пароль, вибраний пункт, прапорець або файл. У браузері форма складається з HTML-елементів, а JavaScript реагує на події та керує їхнім станом.
У цьому уроці ви:
обробите надсилання форми без перезавантаження сторінки;
реагуватимете на введення користувача;
перевірятимете значення полів;
показуватимете помилки та стан форми;
використовуватимете FormData;
навчитеся розрізняти події input, change і submit.
submitПодія submit виникає, коли користувач надсилає форму:
натискає кнопку з type="submit";
натискає Enter у текстовому полі.
Типова поведінка браузера — перейти за адресою з атрибута action або перезавантажити сторінку. Для обробки форми через JavaScript часто викликають event.preventDefault().
form.addEventListener("submit", (event) => {
event.preventDefault();
console.log("Форму надіслано без перезавантаження сторінки");
});preventDefault() скасовує стандартну дію браузера, але не зупиняє поширення події. Для цього існує інший метод — stopPropagation().
inputПодія input виникає щоразу, коли значення поля змінюється внаслідок введення користувача.
Вона підходить для:
перевірки поля під час введення;
відображення лічильника символів;
пошуку в реальному часі;
оновлення попереднього перегляду.
const usernameInput = document.querySelector("#username");
usernameInput.addEventListener("input", () => {
console.log(usernameInput.value);
});Подія input також виникає під час вставлення тексту або видалення символів.
changeПодія change виникає після зміни значення, коли користувач завершив взаємодію з елементом.
Для текстового поля це зазвичай означає:
користувач змінив текст;
перевів фокус на інший елемент.
Для <select>, прапорців і перемикачів подія виникає одразу після вибору нового значення.
const countrySelect = document.querySelector("#country");
countrySelect.addEventListener("change", () => {
console.log(`Обрано: ${countrySelect.value}`);
});focus і blurfocus виникає, коли елемент отримує фокус;
blur виникає, коли елемент втрачає фокус.
Вони корисні, коли потрібно:
показати підказку;
приховати підказку;
перевірити поле після завершення введення;
змінити візуальний стан поля.
const emailInput = document.querySelector("#email");
emailInput.addEventListener("focus", () => {
console.log("Користувач почав працювати з email");
});
emailInput.addEventListener("blur", () => {
console.log("Користувач залишив поле email");
});Значення більшості полів доступне через властивість value.
const form = document.querySelector("#profile-form");
const nameInput = form.elements.name;
console.log(nameInput.value);Колекція form.elements дає змогу отримувати елементи форми за атрибутом name.
<input name="name" type="text">const input = form.elements.name;Для прапорця потрібно використовувати властивість checked, а не value.
const termsCheckbox = form.elements.terms;
if (termsCheckbox.checked) {
console.log("Умови прийнято");
}Для перемикачів із однаковим name вибраний елемент можна знайти через :checked.
const selectedPlan = form.querySelector(
'input[name="plan"]:checked'
);
console.log(selectedPlan?.value);Оператор ?. захищає код від помилки, якщо жоден перемикач не вибрано.
HTML має вбудовані атрибути перевірки:
required — поле обов’язкове;
minlength і maxlength — обмеження довжини тексту;
min і max — діапазон числових значень;
type="email" — базова перевірка email;
pattern — перевірка за регулярним виразом.
<input
name="email"
type="email"
required
autocomplete="email"
>Якщо поле не проходить перевірку, браузер зазвичай не генерує submit і показує стандартне повідомлення.
Щоб перевірити форму вручну, можна використати метод checkValidity():
if (form.checkValidity()) {
console.log("Усі поля коректні");
} else {
console.log("Форма містить помилки");
}Для окремого поля доступна властивість validity:
if (emailInput.validity.typeMismatch) {
console.log("Значення не схоже на email");
}Корисні властивості validity:
valueMissing — не заповнено обов’язкове поле;
typeMismatch — значення не відповідає типу;
tooShort — значення закоротке;
tooLong — значення задовге;
patternMismatch — значення не відповідає шаблону;
valid — усі обмеження виконано.
Метод reportValidity() запускає стандартну перевірку та показує повідомлення браузера:
form.reportValidity();Для складніших сценаріїв зручно показувати власні повідомлення. Поширений підхід:
перевірити значення;
додати або прибрати клас помилки;
змінити текст повідомлення;
оновити атрибут aria-invalid.
function setFieldError(input, messageElement, message) {
input.classList.add("invalid");
input.setAttribute("aria-invalid", "true");
messageElement.textContent = message;
}
function clearFieldError(input, messageElement) {
input.classList.remove("invalid");
input.removeAttribute("aria-invalid");
messageElement.textContent = "";
}Атрибут aria-invalid="true" повідомляє допоміжним технологіям, що поле містить помилку.
Для зв’язку поля з повідомленням про помилку можна використати aria-describedby:
<input
id="email"
name="email"
aria-describedby="email-error"
>
<p id="email-error"></p>Нижче наведено повний приклад форми реєстрації. Він:
перевіряє ім’я під час введення;
перевіряє email після втрати фокусу;
порівнює пароль і його підтвердження;
оновлює лічильник символів;
блокує кнопку під час обробки;
створює об’єкт із даними форми через FormData.
Збережіть код у файл index.html і відкрийте його в браузері.
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Реєстрація</title>
<style>
:root {
font-family: Arial, sans-serif;
color: #202124;
background: #f4f6f8;
}
body {
max-width: 560px;
margin: 40px auto;
padding: 0 16px;
}
form {
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgb(0 0 0 / 10%);
}
.field {
margin-bottom: 18px;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 700;
}
input {
box-sizing: border-box;
width: 100%;
padding: 10px;
border: 1px solid #aeb4bb;
border-radius: 6px;
font: inherit;
}
input.invalid {
border-color: #c62828;
background: #fff5f5;
}
.error {
min-height: 20px;
margin: 5px 0 0;
color: #c62828;
font-size: 0.9rem;
}
.counter {
color: #5f6368;
font-size: 0.9rem;
}
button {
padding: 10px 18px;
border: 0;
border-radius: 6px;
color: white;
background: #1769aa;
font: inherit;
cursor: pointer;
}
button:disabled {
cursor: wait;
opacity: 0.6;
}
#result {
margin-top: 18px;
font-weight: 700;
}
</style>
</head>
<body>
<h1>Створення облікового запису</h1>
<form id="registration-form" novalidate>
<div class="field">
<label for="username">Ім’я користувача</label>
<input
id="username"
name="username"
type="text"
minlength="3"
maxlength="20"
required
autocomplete="username"
aria-describedby="username-error username-counter"
>
<p id="username-error" class="error" aria-live="polite"></p>
<p id="username-counter" class="counter">0/20</p>
</div>
<div class="field">
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
required
autocomplete="email"
aria-describedby="email-error"
>
<p id="email-error" class="error" aria-live="polite"></p>
</div>
<div class="field">
<label for="password">Пароль</label>
<input
id="password"
name="password"
type="password"
minlength="8"
required
autocomplete="new-password"
aria-describedby="password-error"
>
<p id="password-error" class="error" aria-live="polite"></p>
</div>
<div class="field">
<label for="password-confirmation">Підтвердження пароля</label>
<input
id="password-confirmation"
name="passwordConfirmation"
type="password"
required
autocomplete="new-password"
aria-describedby="password-confirmation-error"
>
<p
id="password-confirmation-error"
class="error"
aria-live="polite"
></p>
</div>
<label>
<input
name="terms"
type="checkbox"
required
>
Я погоджуюся з умовами використання
</label>
<br><br>
<button type="submit">Зареєструватися</button>
<p id="result" aria-live="polite"></p>
</form>
<script>
const form = document.querySelector("#registration-form");
const usernameInput = form.elements.username;
const emailInput = form.elements.email;
const passwordInput = form.elements.password;
const confirmationInput = form.elements.passwordConfirmation;
const submitButton = form.querySelector('button[type="submit"]');
const usernameError = document.querySelector("#username-error");
const emailError = document.querySelector("#email-error");
const passwordError = document.querySelector("#password-error");
const confirmationError = document.querySelector(
"#password-confirmation-error"
);
const usernameCounter = document.querySelector("#username-counter");
const result = document.querySelector("#result");
function showError(input, element, message) {
input.classList.add("invalid");
input.setAttribute("aria-invalid", "true");
element.textContent = message;
}
function clearError(input, element) {
input.classList.remove("invalid");
input.removeAttribute("aria-invalid");
element.textContent = "";
}
function validateUsername() {
const value = usernameInput.value.trim();
if (value.length < 3) {
showError(
usernameInput,
usernameError,
"Ім’я має містити щонайменше 3 символи."
);
return false;
}
clearError(usernameInput, usernameError);
return true;
}
function validateEmail() {
if (emailInput.validity.valueMissing) {
showError(emailInput, emailError, "Введіть email.");
return false;
}
if (emailInput.validity.typeMismatch) {
showError(emailInput, emailError, "Введіть коректний email.");
return false;
}
clearError(emailInput, emailError);
return true;
}
function validatePassword() {
if (passwordInput.value.length < 8) {
showError(
passwordInput,
passwordError,
"Пароль має містити щонайменше 8 символів."
);
return false;
}
clearError(passwordInput, passwordError);
return true;
}
function validateConfirmation() {
if (confirmationInput.value !== passwordInput.value) {
showError(
confirmationInput,
confirmationError,
"Паролі не збігаються."
);
return false;
}
clearError(confirmationInput, confirmationError);
return true;
}
usernameInput.addEventListener("input", () => {
usernameCounter.textContent =
`${usernameInput.value.length}/20`;
if (usernameInput.value.length > 0) {
validateUsername();
}
});
emailInput.addEventListener("blur", validateEmail);
passwordInput.addEventListener("blur", validatePassword);
confirmationInput.addEventListener("input", validateConfirmation);
passwordInput.addEventListener("input", () => {
if (confirmationInput.value.length > 0) {
validateConfirmation();
}
});
form.addEventListener("submit", async (event) => {
event.preventDefault();
const isValid =
validateUsername() &&
validateEmail() &&
validatePassword() &&
validateConfirmation() &&
form.checkValidity();
if (!isValid) {
result.textContent = "Виправте помилки у формі.";
result.style.color = "#c62828";
return;
}
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// Показуємо стан обробки, щоб запобігти повторному надсиланню.
submitButton.disabled = true;
submitButton.textContent = "Обробка...";
result.textContent = "";
try {
// Імітуємо запит до сервера.
await new Promise((resolve) => setTimeout(resolve, 800));
console.log(data);
result.textContent = `Користувача ${data.username} створено.`;
result.style.color = "#2e7d32";
form.reset();
usernameCounter.textContent = "0/20";
} finally {
submitButton.disabled = false;
submitButton.textContent = "Зареєструватися";
}
});
</script>
</body>
</html>novalidate вимикає стандартні підказки браузера. Це потрібно, щоб форма використовувала власні повідомлення. Водночас form.checkValidity() усе ще перевіряє HTML-обмеження.
Під час надсилання виконується кілька перевірок:
const isValid =
validateUsername() &&
validateEmail() &&
validatePassword() &&
validateConfirmation() &&
form.checkValidity();Оператор && зупиняє обчислення після першого false. Це дає змогу не виконувати наступні перевірки, якщо попередня вже завершилася помилкою.
FormDataFormData збирає дані форми у форматі пар ключ-значення.
const formData = new FormData(form);
console.log(formData.get("username"));
console.log(formData.get("email"));Щоб перетворити просту форму на звичайний об’єкт:
const data = Object.fromEntries(formData.entries());Результат може мати такий вигляд:
{
username: "olena",
email: "olena@example.com",
password: "secret123",
passwordConfirmation: "secret123",
terms: "on"
}Прапорець потрапляє до FormData лише тоді, коли він вибраний. Його значенням за замовчуванням є "on", якщо в HTML не вказано власний value.
Для перевірки стану прапорця краще використовувати:
const termsAccepted = form.elements.terms.checked;Не надсилайте пароль у консоль або повідомленнях користувачеві в реальному застосунку. У прикладі він виводиться лише для демонстрації структури даних.
Форма має не лише значення, а й стан:
поле порожнє;
поле має помилку;
поле перевіряється;
форма надсилається;
надсилання завершилося успішно;
виникла помилка сервера.
Стан можна представляти класами, атрибутами та окремими елементами інтерфейсу.
Після початку асинхронної операції кнопку надсилання варто заблокувати:
submitButton.disabled = true;
try {
await saveData();
} finally {
submitButton.disabled = false;
}Блок finally виконується і після успішного завершення, і після помилки. Тому кнопка не залишиться заблокованою назавжди.
Метод reset() повертає поля до початкових значень, заданих у HTML:
form.reset();Якщо інтерфейс містить власні повідомлення або лічильники, їх потрібно очистити окремо:
form.reset();
errorElement.textContent = "";
counter.textContent = "0";Значення поля можна змінити через value:
emailInput.value = "user@example.com";Важливо: програмна зміна value не генерує подію input автоматично. Якщо інша логіка залежить від цієї події, її потрібно викликати вручну або повторно запустити необхідну функцію.
emailInput.value = "user@example.com";
emailInput.dispatchEvent(new Event("input", { bubbles: true }));Якщо форма має багато полів, необов’язково додавати окремий обробник на кожне. Події введення поширюються від дочірнього елемента до батьківського, тому можна слухати подію на самій формі.
form.addEventListener("input", (event) => {
const input = event.target;
if (input.matches("input[name='username']")) {
console.log("Змінено ім’я користувача");
}
});event.target — елемент, на якому подія виникла.
event.currentTarget — елемент, на якому зареєстровано обробник. У цьому прикладі event.currentTarget буде самою формою.
Делегування особливо корисне для динамічно створених полів, оскільки не потрібно додавати нові обробники після кожної зміни DOM.
У реальному застосунку після перевірки дані часто надсилають на сервер за допомогою fetch.
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
try {
const response = await fetch("/api/register", {
method: "POST",
body: formData
});
if (!response.ok) {
throw new Error("Сервер не прийняв дані");
}
const result = await response.json();
console.log(result);
} catch (error) {
console.error(error);
}
});Під час надсилання FormData браузер сам встановлює правильний тип запиту для multipart-даних. Для JSON потрібно явно перетворити дані та встановити заголовок:
const data = Object.fromEntries(new FormData(form));
const response = await fetch("/api/register", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});Перевірка на клієнті покращує взаємодію з користувачем, але не замінює перевірку на сервері. Дані клієнта можна змінити до надсилання, тому сервер завжди має перевіряти їх повторно.
Якісна форма має бути зрозумілою для клавіатури та допоміжних технологій.
Рекомендації:
кожне поле повинно мати пов’язаний <label>;
використовуйте for у <label> та відповідний id у поля;
не замінюйте label лише текстом у placeholder;
помилки мають бути зрозумілими та конкретними;
додавайте aria-invalid="true" для поля з помилкою;
зв’язуйте поле з помилкою через aria-describedby;
повідомлення, що змінюються динамічно, можна позначити aria-live="polite";
не покладайтеся лише на колір для позначення помилки.
Приклад пов’язаних поля та повідомлення:
<label for="phone">Номер телефону</label>
<input
id="phone"
name="phone"
aria-describedby="phone-error"
>
<p id="phone-error" aria-live="polite"></p>click замість submitЯкщо слухати лише click кнопки, можна пропустити надсилання через Enter. Надійніше обробляти подію submit самої форми.
preventDefault()Без event.preventDefault() браузер виконає стандартне надсилання, і сторінка може перезавантажитися до завершення JavaScript-логіки.
Клієнтську валідацію можна обійти. Сервер повинен повторно перевіряти типи, довжину, формат і допустимість отриманих даних.
value для прапорцяДля прапорця value не показує, чи його вибрано. Використовуйте checked.
if (form.elements.terms.checked) {
// Прапорець вибрано.
}inputПеревірка кожного символу може бути надто нав’язливою. Часто краще:
оновлювати підказки на input;
показувати помилку після blur;
виконувати повну перевірку під час submit.
Якщо використовуються novalidate і власна система помилок, потрібно самостійно перевіряти всі важливі поля. Якщо власні повідомлення не потрібні, можна залишити стандартну HTML-валідацію.
Не записуйте паролі та інші секрети в консоль, URL або повідомлення інтерфейсу. Навіть під час налагодження такі дані легко випадково залишити в коді.
Користувач може натиснути кнопку кілька разів, поки триває запит. Блокуйте кнопку або використовуйте інший індикатор процесу.
Створіть форму з такими полями:
ім’я;
email;
вік;
вибір ролі;
прапорець згоди на обробку даних.
Реалізуйте:
перевірку обов’язкових полів;
перевірку віку від 18 до 120 років;
відображення помилок біля відповідних полів;
оновлення повідомлення після зміни ролі;
блокування кнопки під час імітації надсилання;
виведення успішного результату після завершення.
Додатково спробуйте реалізувати перевірку через делегування події input на рівні форми.
Подія submit використовується для обробки надсилання форми.
event.preventDefault() скасовує стандартне перезавантаження сторінки.
input виникає під час кожної зміни значення.
change виникає після завершення зміни або вибору нового значення.
Значення текстових полів читають через value, а стан прапорців — через checked.
HTML-атрибути required, minlength, type та інші забезпечують базову валідацію.
checkValidity() перевіряє форму, а reportValidity() додатково показує стандартні повідомлення.
FormData зручно використовувати для збору даних форми.
Власні помилки варто поєднувати з aria-invalid та aria-describedby.
Перевірка на клієнті покращує інтерфейс, але серверна перевірка залишається обов’язковою.