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 formularza

Budowa formularza

Zanim przejdziemy do walidacji, zajmijmy się na chwilę kodem naszego formularza.


    <form class="form" method="post" action="...gdzie-wysylamy...">
        <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="email">Email*</label>
            <input type="email" name="email" id="email">
        </div>
        <div class="form-row">
            <button type="submit" class="button submit-btn">
                Wyślij
            </button>
        </div>
    </form>
    

Powyższy kod nie jest żadnym wyznacznikiem jakości. Formularze można budować na wiele sposób, a które dość często nie będą od nas zależeć, bo dla przykładu będą budowane przez jakieś pluginy (dość częsta praktyka w takim Wordpressie).

Jeżeli jednak tworzysz taki kod samemu, warto byś zwrócił uwagę na kilka rzeczy.

Po pierwsze każde pole powinno mieć swój label. Dzięki temu zwiększamy dostępność takich pól. Nawet jeżeli na layoucie takich labeli nie ma, nie znaczy to, że nie powinny znaleźć się w kodzie (można je ukryć dowolną techniką CSS). Żeby oba elementy - input i jego label - stanowiły zamkniętą całość, okryłem je dodatkowymi divami .form-row. Dzięki temu o wiele prościej będzie nam nadawać odstępy między elementami. Dodatkowo można by także pokusić się o dodanie dodatkowych klas dla labeli (.form-label) i samych inputów (.form-control), dzięki czemu w przyszłości nasze stylowanie było by jeszcze prostsze. W poniższych kodach tego unikam by za bardzo nie zaśmiecać kodu.

Dość często widziałem, jak niektórzy robili formularze w strukturze o wiele prostszej:


    <form class="form" method="post" action="...gdzie-wysylamy...">
        <label for="name">Imię (min. 3 znaki)*</label>
        <input type="text" name="name" id="name">

        <label for="email">Email*</label>
        <input type="email" name="email" id="email">

        <button type="submit" class="button submit-btn">
            Wyślij
        </button>
    </form>
    

Unikają dzięki temu zbędnego kodu, ale równocześnie tracą możliwości. Jak zobaczysz później, do każdego pola będziemy chcieli dodać dodatkowe opisy z błędami.

W strukturze takiej jak powyżej było by to dla nas bardzo problematyczne do stylowania. Jeżeli mamy dla każdego inputa rodzica w postaci .form-row, taki błąd możemy stylować w dowolny sposób. Może to być prosty tekst pod polem (jak w artykule poniżej), ale i spokojnie możemy z niego zrobić mała ikonkę pozycjonowaną absolutnie względem .form-row. A nawet jak będziemy chcieli do naszego formularza dodać nowy element z informacjami o wysyłce, nie będzie z tym żadnego problemu. Ogólnie więc nie warto oszczędzać na kilku dodatkowych divach.

Jak przeprowadzić walidację?

  1. 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.
  2. 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ę.
  3. 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ą tworzone przez jakieś podejrzane pluginy, nie mają odpowiednich pól (dość często spotykaną manierą jest tworzenie pól typu email jako tekstowe), a i nie są stosowane w nich odpowiednie atrybuty. I nie zawsze jest to wina leniwych programistów, ponieważ dość często formularze budowane są na bazie bardziej rozbudowane logiki, która swoje działanie opiera na swoich własnych mechanizmach, a nie tym co zostało wpisane w HTML. Dodatkowo taka domyślna walidacja wcale nie jest pozbawiona pewnych wad.

Tak czy siak 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) {
        e.target.submit();
    } else {
        //jeżeli nie to pokazujemy jakieś błędy
        alert("Kolego wypełniłeś błędnie nasz super formularz");
    }
})

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>
    <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 Przed wysłaniem proszę poprawić błędy:</h3>
            <ul class="form-error-list">
                ${formErrors.map(el => `<li>${el}</li>`).join("")}
            </ul>
        `;
    }
});

W powyższych przypadkach 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, show) {
    if (show) {
        field.classList.add("field-error");
    } else {
        field.classList.remove("field-error");
    }
};

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, show) {
    ...
};

//------------------------
//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)));
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]) {
        markErrorField(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 gdy pole stawało by się błędne.

Technika dość przyjemna, szczególnie, gdy kod naszego formularza jest generowany przez backend (np. przez Symfony czy podobne backendowe frameworki), a backendowcy w prosty sposób mogą nam wygenerować należyty komunikat błędu w kodzie HTML.


<!-- przykladowy kod w Symfony i Twig -->

{{ form_start(form) }}
    <div class="form-row">
        {{ form_widget(form.subject) }}
        {{ form_errors(form.subject) }} <!-- !!!! ten komunikat -->
    </div>

    <button type="submit" class="button submit-btn">
        Wyślij
    </button>
{{ form_end(form) }}

Od strony kodu Javascript nie powinno to być jakieś trudne. Gdy użytkownik będzie wypełniał pole, zaznaczamy je jako błędne. Komunikaty błędów pokażemy dopiero w przypadku próby wysłania błędnego formularza. Jeżeli przy pokazaniu takich komunikatów użytkownik poprawi dane pole, ukryjemy dla tego pola komunikat:


//funkcje testujące
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 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, show) {
    if (show) {
        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();
    }
});
Wpisz poprawne imię
Wpisz poprawny email

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) {
    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 removeFieldError(field) {
    ...
};

function createFieldError(field, text) {
    ...
};

function markFieldAsError(field, show) {
    if (show) {
        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 => markErrorField(e.target, !testText(e.target, 3)));
inputEmail.addEventListener("input", e => markErrorField(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
    for (const el of [inputName, inputEmail]) {
        markFieldAsError(el, false);
        removeFieldError(el);
    }

    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 zboostować powyższą walidację.

W tym momencie pole robi się czerwone od razu gdy użytkownik zacznie wpisywać. Według badań przeprowadzonych przez Christiana Holsta i Luka Wroblewskiego najlepszym sposobem informowania użytkownika o błędach jest moment, kiedy opuszcza on dane pole. Faktycznie coś w tym jest. My zaczynamy wpisywać, a od razu mamy błąd...
Z drugiej strony gdy użytkownik dostanie już błąd, wróci do pola i zacznie je poprawiać, fajnie by było gdyby widział na bieżąco, że wpisana wartość jest już poprawna. Ok - zróbmy to.

Linie 35-36 zamieniamy na:


    ...

    inputName.addEventListener("blur", e => {
        const testResult = !testText(e.target, 3) && e.target.value !== "";
        markFieldAsError(e.target, testResult);
    });
    inputName.addEventListener("input", e => {
        if (e.target.classList.contains("field-error") && testText(e.target, 3)) {
             markFieldAsError(e.target, false);
        }
    });

    inputEmail.addEventListener("blur", e => {
        const testResult = !testEmail(e.target) && e.target.value !== "";
        markFieldAsError(e.target, testResult);
    });
    inputEmail.addEventListener("input", e => {
        if (e.target.classList.contains("field-error") && testEmail(e.target)) {
             markFieldAsError(e.target, false);
        }
    });

    ...
    

Dla polepszenia dostępności poza dodawaniem klasy dla błędnych pól moglibyśmy pokusić się o dodanie do błędnych pól atrybutów aria-invalid=true oraz aria-describedBy="describedBy". Ten pierwszy oznacza pole niepoprawnie wypełnione. Ten drugi wskazuje na element z tekstem błędu.

Oznacza to, że musimy zmodyfikować nasze funkcje tworzące i usuwające komunikat błędu:


    function removeFieldError(field) {
        const errorText = field.nextElementSibling;
        if (errorText !== null) {
            if (errorText.classList.contains("form-error-text")) {
                errorText.remove();
            }
        }

        field.removeAttribute("aria-invalid");
        field.removeAttribute("aria-describedBy")
    };

    function createFieldError(field, text) {
        removeFieldError(field); //przed stworzeniem usuwam by zawsze był najnowszy komunikat

        field.setAttribute("aria-invalid", true);
        field.setAttribute("aria-describedBy", field.id + "_errorText")

        const div = document.createElement("div");
        div.classList.add("form-error-text");
        div.innerText = text;
        div.id = field.id + "_errorText";

        if (field.nextElementSibling === null) {
            field.parentElement.appendChild(div);
        } else {
            if (!field.nextElementSibling.classList.contains("form-error-text")) {
                field.parentElement.insertBefore(div, field.nextElementSibling);
            }
        }
    };
    

Formularze z większą ilością pól

Powyższe podejście sprawdzi się raczej w stosunkowo krótkich sytuacjach. Nie jest ono złe ale też nie jest żadnym wyznacznikiem jakości. Jeżeli do tej pory sumiennie czytałeś wcześniejsze rozdziały (zwłaszcza ten traktujący o DOM), powinieneś bez problemu stworzyć podobny, a pewnie i lepszy kod.

Problem z powyższym kodem jest taki, że testy kolejnych pól przy większych formularzach będą nam się niemożliwie mnożyć. Musielibyśmy więc w jakiś sposób zautomatyzować nasze podejście.

Jak to zrobić? Najłatwiej było by pobrać wszystkie pola, a następnie robiąc po nich pola wykonać odpowiednie testy. Przydało by się też coś, co da nam jakieś punkty zaczepienia jak dane pole mamy sprawdzać. Niektóre pola mogą wymagać konkretnej liczby znaków, pola z emailem wymagać powinny odpowiedniej wartości, niektóre wymagać będą podania numerów... A dodatkowo nie wszystkie pola muszą być wymagane...

Walidacja w czystym HTML

Nie jest to niestety kursu o HTML/CSS 😥, ale aby dobrze zrozumieć kolejny temat dobrze jest zacząć właśnie od 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 ciekawy koncept, 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
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
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.

Walidacja HTML

Na chwilę obecną jego wygląd możemy zmienić tylko w przeglądarkach Chrome za pomocą poniższego kodu:


::-webkit-validation-bubble {}
::-webkit-validation-bubble-message {}
::-webkit-validation-bubble-arrow {}
::-webkit-validation-bubble-arrow-clipper {}

Ale też możemy za pomocą Javascript zmienić jego treść.

Zauważ, że dla pola #name zastosowałem dodatkowy atrybut patter .{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, w którym podajemy wyrażenie regularne. Nasz wzór idealny nie jest, ponieważ pozwala wpisać dowolny ciąg nawet     (trzy spacje). Żeby realnie sprawdzać imię, trzeba by zastosować bardziej skomplikowany wzór (co wcale proste nie jest), lub celować w grupy znaków unicode (wraz z dodatkami, bo w nazwach mogą być ').

Można tutaj oczywiście walczyć na siłę, ale osobiście znowu zalecam stosować zasadę "im mniej walidacji tym lepiej". Można to przyrównać do captchy w formularzu. Jeżeli zastosujemy zbyt mocną captchę (np. tą od Googla), tylko zniechęcimy użytkowników przed wypełnieniem formularza. Zamiast tego odsiewanie spamu zaleca się robić po stronie serwera...

Wróćmy do formularza.

Dymki z podpowiedzią pokazują się dopiero w naszym 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. Dwa podstawowe selektory to:

  • :valid - czy pole jest poprawnie wypełnione
  • :invalid - czy pole jest niepoprawnie wypełnione

input:invalid {
    border-color: red;
    outline: none;
    box-shadow: 0 0 2px red;
}
input:required:valid {
    border-color: #4dcc23;
    outline: none;
    box-shadow: 0 0 2px #4dcc23;
}

<input type="text" name="numbers" required pattern="[0-9]{3,}">

Niestety na co dzień te dwa selektory te nie są wystarczające, dlatego dość szybko musimy sięgnąć po dodatkowe:


:required - czy pole jest wymagane
:placeholder-shown - czy dane pole pokazuje w danym momencie placeholder
:focus - czy pole jest aktualnie wybrane

/* plus te same wraz z zaprzeczeniem */
:not(:placeholder-shown)
:not(:valid)
:not(:invalid)
:not(:focus)

Spróbujmy je więc zastosować na powyższym formularzu kontaktowym:


input:invalid,
textarea:invalid {
    border-color: red;
    outline: none;
    box-shadow: 0 0 2px red;
}

input:required:valid,
textarea:required:valid {
    border-color: #4dcc23;
    outline: none;
    box-shadow: 0 0 2px #4dcc23;
}

Udało się? Niezupełnie. błędy w formularzu nie powinny być pokazywane od początku, a dopiero od momentu gdy użytkownik opuści błędnie wypełnione pole (ewentualnie błędy możemy zacząć pokazywać podczas wypełniania pola i po opuszczeniu).

W naszym Javascriptowym rozwiązaniu używaliśmy według potrzeby zdarzeń blur lub input, natomiast w czystym CSS na chwilę obecną w zasadzie nie mamy jakiś wielkich możliwości.

Teoretycznie istnieje selektor :placeholder-shown, który moglibyśmy tutaj wykorzystać:


/* jeżeli pole jest błędne ale i nie pokazuje placeholdera */
input:not(:placeholder-shown):invalid,
textarea:not(:placeholder-shown):invalid {
    border-color: red;
    outline: none;
    box-shadow: 0 0 2px red;
}

input:not(:placeholder-shown):required:valid,
textarea:not(:placeholder-shown):required:valid {
    border-color: #4dcc23;
    outline: none;
    box-shadow: 0 0 2px #4dcc23;
}

Ale jak widzisz rozwiązanie nie jest idealne. Po pierwsze żeby w ogóle je zastosować każde z pól musi mieć swój placeholder, a przecież nie zawsze tak będzie (zobacz jak powyższy formularz niepotrzebnie dubluje informacje). Moglibyśmy tutaj zastosować trik z ustawieniem placeholdera na tekst ze spacją, przy czym wydaje się to znowu hakiem na niedopracowaną rzecz...

Po drugie wsparcie tego selektora nie jest idealne.

Po trzecie jeżeli chcemy wysłać błędnie wypełniony formularz, pojawiają się pokazane powyżej tooltipy z komunikatem, ale dla każdego pola z osobna. Jeżeli mielibyśmy bardziej rozbudowany formularz, musielibyśmy kilkakrotnie próbować wysłać formularz, poprawiać dane i znowu próbować wysłać. To nie jest dobre UX.

Po czwarte - i najważniejsze - nie jesteśmy w stanie poprawić dostępności naszego rozwiązania. Aby nasz formularz był bardziej przyjazny dla osób korzystających z urządzeń asystujących, powinniśmy do naszych pól i błędów dodać dodatkowe atrybuty aria.

W powyższych rozwiązaniach z Javascript umyślnie ominąłem te rzeczy by dodatkowo nie mieszać kodu. Dodanie takiej funkcjonalności do naszego kodu to dosłownie 2-3 linijki.


    //za pomocą JS powinniśmy nie tylko dodawać klasę do pola i pokazywać komunikat błędu
    //ale i dodatkowe atrybuty aria które wskazują na odpowiedni element
    <input type="text" name="name" id="name" aria-invalid="true" aria-describeBy="nameErrorText">
    <div class="form-error-text" id="nameErrorText">Wpisz poprawne imię</div>

W CSS nie ma możliwości zarządzać atrybutami...


W Internecie znajdziesz wiele prób stworzenia walidacji w czystym HTML (np. to), ale większość z nich to pójście na kompromis. Czyżby kolejny punkt do listy wstydu CSS?

I znowu - może tobie to wystarczy w danej sytuacji? Widziałem podobne rozwiązania na wielu stronach i spełniało to całkiem swoje zadanie.

Ale powiedzmy, że nie jesteśmy zadowoleni z powyższego rezultatu. Na szczęście możemy tutaj popracować jeszcze za pomocą Javascript. Zrobimy to jednak w następnym rozdziale.

W naszym Javascriptowym przykładzie pokazywaliśmy dodatkowy tekst pod polami z błędami. To samo można - w dość ubogiej wersji zrobić za pomocą samego CSS. Wystarczy w HTML w formularzu stworzyć komunikaty błędów:

        <input type="email" name="email" placeholder="Wpisz email" required>
        <div class="form-error-text">Wpisany email jest niepoprawny</div>
    

A następnie odpowiednio ostylować:


    input {}
    .form-error-text {
        display: none;
    }
    input:not(:placeholder-shown):invalid {
        border-color: tomato;
        outline: none;
        box-shadow: 0 0 2px tomato;
    }
    input:not(:placeholder-shown):invalid ~ .form-error-text {
        display: block;
    }
    input:not(:placeholder-shown):required:valid {
        border-color: #4dcc23;
        outline: none;
        box-shadow: 0 0 2px #4dcc23;
    }
    input:not(:placeholder-shown):required:valid ~ .form-error-text {
        display: none;
    }
    

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.