Formularze - walidacja
Sprawdzanie danych, które wprowadza użytkownik to jedna z ważniejszych rzeczy o jaką musimy zadbać, ale równocześnie też nie zawsze prosta do wykonania. Dane możemy sprawdzać na wiele, wiele sposobów, które często będzie uzależnione od budowy formularza i zawartych w nim mechanizmów.
Kilka informacji na temat struktury HTML formularzaJak przeprowadzić walidację?
- Pierwszy etap to dynamiczna podpowiedź w czasie wprowadzania danych przez użytkownika. Wykorzystamy tutaj zdarzenia change, focus, blur, keyup, keydown, keypress lub input - wszystko zależnie od sytuacji. Dodanie takich dynamicznych podpowiedzi znacząco może poprawić użyteczność naszego formularza, ale nie zawsze jest stosowane. Niektóre z takich testów robiliśmy w poprzednim rozdziale.
- Drugi etap to sprawdzenie danych tuż przed wysłaniem. Jeżeli są poprawne to je prześlemy na serwer. Jeżeli nie, wyświetlamy stosowną informację (ewentualnie wskazujemy błędne pola) i blokujemy wysyłkę.
- Ostatni - najważniejszy etap - to sprawdzenie przesłanych danych po stronie serwera. Jeżeli dane są błędne, wówczas wracamy do formularza wyświetlając informację i czekamy na wykonanie punktu drugiego.
I nie - nie jest to jedyny przepis na sprawdzanie formularzy.
W idealnym świecie do powyższych kroków doszedł by jeszcze krok 0. Od kilku lat w przeglądarkach istnieje bowiem walidacja po stronie HTML. Jeżeli dobrze zbudujemy nasz formularz (dodając do niego odpowiednie atrybuty, stosując odpowiednie typy pól itp) wtedy możemy mieć podstawową walidację bez napisania nawet kawałka Javascript.
Jest to fajna sprawa, ponieważ idealnie wkomponowuje się w progressive enhancement. Jeżeli na stronę użytkownik wejdzie bez Javascript, dostanie podstawową walidację, która może nie jest idealna, ale jest. Na to my jako programiści robimy nakładkę w postaci naszej walidacji Javascript, która - znowu - w idealnym świecie - przeprowadziła by użytkownika przez wszystkie powyższe kroki. Jest to też o tyle fajne, ponieważ w Javascript mamy specjalny interfejs do sprawdzania formularzy (porozmawiamy o nim w kolejnym rozdziale), który swoje działanie opiera na walidacji HTML. Jeżeli więc dobrze stworzymy nasz początkowy kod HTML, nasze zadanie będzie o wiele prostsze.
Problem jest niestety taki, że nie żyjemy w idealnym świecie. Dość często formularze które musimy obsłużyć są tak zaawansowanymi tworami, że standardowa HTMLowa walidacja nie wystarczy.
Walidacji za pomocą Javascript raczej nie unikniemy.
Tak naprawdę ciężko mówić o pełnoprawnej walidacji danych po stronie przeglądarki (pierwszy i drugi krok). Możemy dane sprawdzać i pokazywać użytkownikowi podpowiedzi, ale opieranie się w całości na walidacji danych tylko po stronie przeglądarki jest mocno naiwne. Spreparowanie odpowiedniego formularza, czy nagięcie działania skryptów nie jest zbyt ciężką rzeczą. Szczególnie jeżeli mamy dostęp do narzędzi developerskich (1, 2).
Prawdziwa walidacja danych powinna zawsze się odbywać po stronie serwera. Skrypty w przeglądarce traktuj tylko jako poprawę użyteczności formularza i podpowiedzi dla użytkownika. Nic więcej.
Prosta walidacja
Zacznijmy od podstaw. Domyślną akcją każdego formularza jest wysłanie danych, co powoduje przeładowanie danej strony, lub przeniesienie na inną. Aby wykonać jakąkolwiek walidację, musimy tą akcję przerwać.
Najlepszym sposobem na to jest podpięcie się pod zdarzenie submit
formularza, a następnie użycie preventDefault()
, po którym możemy w spokoju przeprowadzać sprawdzanie pól:
const form = document.querySelector("form");
const input = form.querySelector("input");
form.addEventListener("submit", e => {
e.preventDefault();
//jeżeli wszystko ok to wysyłamy
if (input.value.length >= 3) {
form.submit();
} else {
//jeżeli nie to pokazujemy jakieś błędy
alert("Kolego wypełniłeś błędnie nasz super formularz");
}
})
Niektórzy zamiast pod zdarzenie submit podpinają się pod zdarzenie click dla przycisków Submit. I takie działanie także zadziała, nawet gdy użytkownik wyśle formularz enterem. W moim odczuciu jest to jednak mało eleganckie rozwiązanie.
Ważniejsze jest jednak coś innego. Gdy formularz wysyła użytkownik np. klikając w przycisk Wyślij, wpierw odpalana jest HTMLowa walidacja (którą możemy wyłączyć atrybutem novalidate dla formularza), a następnie zdarzenie "submit", do którego się powyżej podpięliśmy.
Wywołanie metody form.submit()
powoduje wysłanie formularza, jednak ani HTMLowa walidacja, ani zdarzenie "submit" nie zostanie aktywowane. Możemy to obejść na trzy sposoby. Po pierwsze zamiast metody form.submit()
możemy za pomocą kodu kliknąć w przycisk submit w danym formularzu np. button.click()
. Drugim sposobem może być stworzenie własnego zdarzenia "submit" (za pomocą new Event()) , które poślemy do danego formularza:
const event = new Event("submit");
form.dispatchEvent(event);
Od jakiegoś czasu mamy też dostęp do metody form.requestSubmit()
, która zarówno aktywuje HTMLową walidację jak i uruchamia zdarzenie "submit". Trzeba mieć tylko na uwadze, że nie zadziała w starszych przeglądarkach (IE11).
Gdy pojawi się w naszym formularzu kilka pól, trzeba jakoś zebrać wyniki. Rozwiązań jest wiele. Jednym z nich jest ręczne sprawdzanie kolejnych pól, gdzie wyniki testów możemy trzymać w oddzielnej tablicy.
Równocześnie posługiwanie się okienkiem alert nie jest zalecanym rozwiązaniem. Okienko takie jest zbyt "intensywne" dla użytkownika końcowego, a poza tym nie mamy w ogóle możliwości zmiany jego wyglądu. Przyda się w prostych treningowych rozwiązaniach, ale my chcemy więcej. Pokażmy błędy jako informacja wkomponowana w formularz.
<form class="form" method="post" id="form">
<div class="form-row">
<label for="name">Imię (min. 3 znaki)*</label>
<input type="text" name="name" id="name">
</div>
<div class="form-row">
<label for="name">Email*</label>
<input type="email" name="email" id="email">
</div>
<div class="form-message"></div> <!-- tu trafią błędy -->
<div class="form-row">
<button type="submit" class="button submit-btn">
Wyślij
</button>
</div>
</form>
const form = document.querySelector("form");
const inputName = form.querySelector("input[name=name]");
const inputEmail = form.querySelector("input[name=email]");
const formMessage = form.querySelector(".form-message");
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = [];
//-------------------------
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
//-------------------------
if (inputName.value.length <= 3) {
formErrors.push("Wypełnij poprawnie pole z imieniem");
}
//wyrażenie testujące maila omawiane w rozdziale o wyrażeniach regularnych
const reg = /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$/;
if (!reg.test(inputEmail.value)) {
formErrors.push("Wypełnij poprawnie pole z emailem");
}
if (!formErrors.length) { //jeżeli nie ma błędów wysyłamy formularz
e.target.submit();
//...lub dynamicznie wysyłamy dane za pomocą Ajax
//równocześnie reagując na odpowiedź z serwera
} else {
//jeżeli jednak są jakieś błędy...
formMessage.innerHTML = `
<h3 class="form-error-title">Przed wysłaniem proszę poprawić błędy:</h3>
<ul class="form-error-list">
${formErrors.map(el => `<li>${el}</li>`).join("")}
</ul>
`;
}
});
W powyższym kodzie pominęliśmy etap pierwszy, czyli podpowiedzi dla użytkownika w trakcie pisania.
Żeby nie powtarzać warunków testów stwórzmy oddzielne funkcje do testowania i dodawania klasy:
function testText(field, lng) {
return field.value.length >= lng;
}
function testEmail(field) {
const reg = /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*(\.\w{2,})+$/;
return reg.test(field.value);
}
function markFieldAsError(field, hasError) {
field.classList.toggle("field-error", hasError);
}
A następnie wykorzystajmy je w naszym kodzie podpinając je pod zdarzenia input
lub blur
po wysyłce formularza.
function testText(field, lng) {
...
}
function testEmail(field) {
...
}
function markFieldAsError(field, hasError) {
...
}
//------------------------
//pobieram elementy
//------------------------
const form = document.querySelector("form");
const inputName = form.querySelector("input[name=name]");
const inputEmail = form.querySelector("input[name=email]");
const formMessage = form.querySelector(".form-message");
//------------------------
//etap 1 : podpinam zdarzenia
//------------------------
inputName.addEventListener("input", e => markFieldAsError(e.target, !testText(e.target, 3)));
inputEmail.addEventListener("input", e => markFieldAsError(e.target, !testEmail(e.target)));
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = [];
//------------------------
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
//------------------------
//chowam błędy
for (const el of [inputName, inputEmail]) {
markFieldAsError(el, false);
}
//i testuję w razie czego zaznaczając pola
if (!testText(inputName, 3)) {
markFieldAsError(inputName, true);
formErrors.push("Wypełnij poprawnie pole z imieniem");
}
if (!testEmail(inputEmail)) {
markFieldAsError(inputEmail, true);
formErrors.push("Wypełnij poprawnie pole z emailem");
}
if (!formErrors.length) { //jeżeli nie ma błędów wysyłamy formularz
form.submit();
//...lub dynamicznie wysyłamy dane za pomocą Ajax
//równocześnie reagując na odpowiedź z serwera
} else {
//jeżeli jednak są jakieś błędy...
formMessage.innerHTML = `
<h3 class="form-error-title">Przed wysłaniem formularza proszę poprawić błędy:</h3>
<ul class="form-error-list">
${formErrors.map(el => `<li>${el}</li>`).join("")}
</ul>
`;
}
});
Błędy przy polach
Możemy też pokusić się o pokazywanie błędów przy błędnych polach. Rozwiązań jest tutaj kilka - lepszych i gorszych - a na pewno różnych.
Najprostsze z nich może polegać na ręcznym wstawieniu komunikatów do HTML za danym polem.
...
<div class="form-row">
<label for="name2A">Imię (min. 3 znaki)*</label>
<input type="text" name="name" id="name">
<div class="form-error-text">Wpisz poprawne imię</div>
</div>
...
Domyślnie takie komunikaty powinny by ukryte (display: none
), a pokazywalibyśmy je w momencie, gdy dane pole będzie błędnie wypełnione.
//funkcje testujące
function testText(field, lng) {
...
}
function testEmail(field) {
...
}
function toggleErrorField(field, show) {
const errorText = field.nextElementSibling;
if (errorText !== null) {
if (errorText.classList.contains("form-error-text")) {
errorText.style.display = show ? "block" : "none";
}
}
}
function markFieldAsError(field, hasError) {
if (hasError) {
field.classList.add("field-error");
} else {
field.classList.remove("field-error");
toggleErrorField(field, false);
}
}
//pobieram elementy
const form = document.querySelector("form");
const inputName = form.querySelector("input[name=name]");
const inputEmail = form.querySelector("input[name=email]");
const formMessage = form.querySelector(".form-message");
//etap 1 : podpinam zdarzenia
inputName.addEventListener("input", e => markFieldAsError(e.target, !testText(e.target, 3)));
inputEmail.addEventListener("input", e => markFieldAsError(e.target, !testEmail(e.target)));
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
//chowam błędy by zaraz w razie czego je pokazać
for (const el of [inputName, inputEmail]) {
markFieldAsError(el, false);
toggleErrorField(el, false);
}
if (!testText(inputName, 5)) {
markFieldAsError(inputName, true);
toggleErrorField(inputName, true);
formErrors = true;
}
if (!testEmail(inputEmail)) {
markFieldAsError(inputEmail, true);
toggleErrorField(inputEmail, true);
formErrors = true;
}
if (!formErrors) {
e.target.submit();
}
});
Dynamicznie tworzone komunikaty
Jeżeli nie mamy wpływu na to jaki będzie HTML, komunikaty o błędach możemy też robić za pomocą samego Javascript. Domyślnie nie będzie ich w HTML, a będziemy je do niego wstawiać dopiero gdy pokażemy dany błąd.
function removeFieldError(field) {
const errorText = field.nextElementSibling;
if (errorText !== null) {
if (errorText.classList.contains("form-error-text")) {
errorText.remove();
}
}
};
function createFieldError(field, text) {
removeFieldError(field); //przed stworzeniem usuwam by zawsze był najnowszy komunikat
const div = document.createElement("div");
div.classList.add("form-error-text");
div.innerText = text;
if (field.nextElementSibling === null) {
field.parentElement.appendChild(div);
} else {
if (!field.nextElementSibling.classList.contains("form-error-text")) {
field.parentElement.insertBefore(div, field.nextElementSibling);
}
}
};
Dodatkowo lekko przerobimy wcześniejszy kod:
//funkcje testujące
function testText(field, lng) {
...
}
function testEmail(field) {
...
}
function removeFieldError(field) {
...
}
function createFieldError(field, text) {
...
}
function markFieldAsError(field, hasError) {
if (hasError) {
field.classList.add("field-error");
} else {
field.classList.remove("field-error");
removeFieldError(field);
}
}
//pobieram elementy
const form = document.querySelector("form");
const inputName = form.querySelector("input[name=name]");
const inputEmail = form.querySelector("input[name=email]");
const formMessage = form.querySelector(".form-message");
//etap 1 : podpinam eventy
inputName.addEventListener("input", e => markFieldAsError(e.target, !testText(e.target, 3)));
inputEmail.addEventListener("input", e => markFieldAsError(e.target, !testEmail(e.target)));
form.addEventListener("submit", e => {
e.preventDefault();
let formErrors = false;
//2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
//wpierw usuwamy błędy
for (const el of [inputName, inputEmail]) {
markFieldAsError(el, false);
removeFieldError(el);
}
//następnie sprawdzamy każde pole
if (!testText(inputName, 3)) {
markFieldAsError(inputName, true);
createFieldError(inputName, "Wpisana wartość jest niepoprawna");
formErrors = true;
}
if (!testEmail(inputEmail)) {
markFieldAsError(inputEmail, true);
createFieldError(inputEmail, "Wpisany email jest niepoprawny");
formErrors = true;
}
if (!formErrors) {
e.target.submit();
}
});
Opcjonalnie moglibyśmy jeszcze tutaj nieco usprawnić powyższą walidację.
Walidacja w czystym HTML
Od kilku lat w HTML5 dostępna jest wbudowana walidacja, która swoje działanie opiera o zastosowanie odpowiednich typów pól formularza i atrybutów dla nich.
Jest to bardzo ciekawe pomysł, który pozwala wprowadzać walidację bez pisania kawałka kodu. Dzięki odpowiedniemu stworzeniu kodu formularza tylko zyskujemy. Dostajemy "darmową" walidację, która działa nawet, gdy użytkownik ma wyłączony Javascript. Po drugie dzięki zastosowaniu odpowiednich typów pól (np. type="email"
dla pola z emailem, type="number"
dla liczb) zyskujemy dodatkowe poprawienie użyteczności naszego formularza, bo np. na urządzeniach mobilnych wyświetlane są odpowiednie klawiatury. Same plusy.
Listę dostępnych typów pól najlepiej zobaczyć na stronie https://developer.mozilla.org/pl/docs/Web/HTML/Element/Input. Warto mieć jednak na uwadze, że nowe rodzaje pól nie w każdej przeglądarce będą się wyświetlać tak samo.
Jeżeli chodzi o atrybuty, to mamy kilka do wykorzystania:
required | określa czy dane pole ma być wypełnione |
---|---|
minlength, maxlength | atrybuty określające minimalna i maksymalną długość wpisywanego tekstu |
min, max | określa minimalną i maksymalną liczbę w polach numerycznych lub dla pól range |
type | określa typ pola. Niektóre z pól mają swoją własną domyślną walidację. I tak np. pola typu email wymagają wpisania emaila, a pola url wpisania odpowiedniego adresu url |
pattern | pozwalają nam podać własny wzór (w formacie regexp) który będzie używany do testu poprawności pola |
Spróbujmy to wykorzystać z prostym formularzu kontaktowym:
<form class="form" id="formTest1" method="post">
<div class="form-row">
<label for="name">Imię*</label>
<input type="text" name="name" id="name" pattern=".{3,}" required>
</div>
<div class="form-row">
<label for="email">Email*</label>
<input type="email" name="email" id="email" required>
</div>
<div class="form-row">
<label for="message">Wiadomość*</label>
<textarea name="message" id="message" required></textarea>
</div>
<div class="form-row">
<button type="submit" class="submit-btn">
Wyślij
</button>
</div>
</form>
Spróbuj teraz wysłać taki formularz. Zobaczysz, że przeglądarka sama w sobie będzie pokazywać odpowiedni komunikat z błędem danego pola.

Za pomocą Javascript możemy zmienić treść takiego dymka, ale wyglądu nie zmienimy.
Dla pola Imię zastosowałem dodatkowy atrybut pattern .{3,}
, który oznacza minimum 3 znaki dowolnego typu. Atrybut
required
oznacza, że dane pole musi być wypełnione, co dla pól tekstowych oznacza, że trzeba cokolwiek do takiego pola wpisać.
Jeżeli chcemy zmienić ten warunek, musimy używać atrybutu pattern
(4 linia), w którym podajemy wyrażenie regularne.
Dymki z podpowiedzią pokazują się dopiero w drugim kroku, czyli przy próbie wysłania formularza. Jeżeli chcielibyśmy do naszego formularza dodać dynamiczną podpowiedź w czasie wpisywania, możemy to zrobić za pomocą CSS oraz odpowiedni pseudoklas:
:valid | czy pole jest poprawnie wypełnione |
---|---|
:invalid | czy pole jest źle wypełnione |
:user-valid | czy pole jest poprawnie wypełnione, ale po pierwszej interakcji użytkownika z polem |
:user-invalid | czy pole jest źle wypełnione, ale po pierwszej interakcji użytkownika z polem |
input:user-invalid {
border-color: red;
outline: none;
box-shadow: 0 0 2px red;
}
input:user-valid {
border-color: #4dcc23;
outline: none;
box-shadow: 0 0 2px #4dcc23;
}
<input type="text" name="numbers" required pattern="[0-9]{3,}">
W powyższym kodzie zastosowałem selektory :user-valid
i :user-invalid
(https://developer.chrome.com/blog/css-wrapped-2023#user-states). Jak zauważysz, ich działanie rozpoczyna się w momencie, gdy użytkownik opuści wypełniane pole (czyli zdarzenie :blur). Są to nowe selektory, dlatego zastanów się, czy aby na pewno będą dobrym wyborem dla twojego projektu (jakie przeglądarki wspiera twój projekt).
Do tej pory przy takiej walidacji mogliśmy korzystać z selektorów :valid
i :invalid
. Pokazywały one błędy jeszcze przed interakcją użytkownika z polem. Oznaczało to, że użytkownik po wejściu na stronę od razu widział "czerwone pola". Mogliśmy do obejść np. poprzez dodanie do pola placeholdera np. ze spacją i użycia selektora :placeholder-shown:
input:invalid { background: red; }
input::placeholder-shown { background: white; }
<input type="text" name="name" required pattern=".{3,}" placeholder=" ">