Formularze - Constraint validation API

W poprzednich rozdziałach próbowaliśmy użyć walidacji udostępnianej przez HTML5.

Efekt nie był może idealny, ale w zasadzie za darmo dostawaliśmy kilka dobroci. Po pierwsze stosując odpowiednie pola i atrybuty nasz formularz tylko zyskał na użyteczności (np. na urządzeniach mobilnych pola numeryczne czy email mają inne klawiatury podczas wypełniania). Po drugie dostaliśmy prostą walidację dla użytkowników bez Javascript. Idealna może nie była, ale była.

Najważniejsze jednak, że Javascript udostępnia nam specjalny interfejs - zwany Constrains Validation API, który bardzo ułatwia walidację formularzy, a który opiera swoje działanie o omawiane w poprzednim rozdziale atrybuty i typy pól.

Wspomniany interfejs udostępnia nam kilka metod i właściwości:

Metody:

input.checkValidity() sprawdza czy pole jest poprawnie wypełnione. Zwraca true/false, a jeżeli pole jest błędnie wypełnione, odpala dodatkowo event invalid.
form.reportValidity() Wykonuje sprawdzenie takie jak przy próbie wysłania formularza który ma walidację w HTML po czym zwraca true/false. Dla użytkownika oznacza to, że przy pierwszym błędnie wypełnionym polu pojawi się domyślny dymek z komunikatem błędu.
input.setCustomValidity(message) Pozwala ustawić własny komunikat błędu, który pojawia się przy walidacji danego pola

Właściwości:

validity zawiera obiekt ValidityState, który jest zbiorem właściwości na temat stanu walidacji danego pola (opisane poniżej)
validationMessage Jeżeli pole podlega walidacji (posiada atrybut required), właściwość ta zawiera aktualną wiadomość, która pojawi się w dymku podczas walidacji. Dla niektórych pól (email, url) wiadomość taka może się zmienić podczas pisania
willValidate Zwraca true/false w zależności od tego, czy pole będzie sprawdzane

Wyłączenie domyślnej walidacji

W poprzednim rozdziale w czystym HTML za pomocą atrybutów i pól dodaliśmy do naszego formularza podstawową walidację.

Jeżeli chcemy wyłączyć jej działanie (tak by móc poprawić jej działanie za pomocą Javascript), powinniśmy do formularza dodać atrybut novalidate. Najlepiej zrobić to za pomocą Javascript, dzięki czemu osoba bez włączonych skryptów dostanie podstawową HTMLową walidację, a osoba ze skryptami dostanie jej udoskonaloną wersję.


form.setAttribute("novalidate", true);

Formularz bez atrybutu novalidate z działającą domyślną walidacją:

Formularz z novalidate:

Własna walidacja

Jeżeli teraz będziemy chcieli nadać własną walidację, możemy wykonać działanie mniej więcej podobne do tego z poprzedniego rozdziału. Zamiast jednak sztywnych testów, możemy wykorzystać metodę checkValidity() udostępnianą przez ten interfejs. Zwraca ona true/false w zależności od tego czy dane pole jest wypełnione poprawnie:


<form class="form" method="post">
    <div class="form-row">
        <label for="name">Imię (min. 3 znaki)*</label>
        <input type="text" required name="name" id="name">
    </div>
    <div class="form-row">
        <label for="email">Email*</label>
        <input type="email" required 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.setAttribute("novalidate", true);

form.addEventListener("submit", e => {
    e.preventDefault();

    let formErrors = [];

    //-------------------------
    //2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
    //-------------------------
    if (!inputName.checkValidity()) {
        formErrors.push("Wypełnij poprawnie pole z imieniem");
    }

    if (!inputEmail.checkValidity()) {
        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">Błędnie wypełniłeś pola:</h3>
            <ul class="form-error-list">
                ${formErrors.map(el => `<li>${el}</li>`).join("")}
            </ul>
        `;
    }
});

Błędy przy polach

Podobnie do poprzedniego rozdziału spróbujmy pokazać błędy przy polach. Działanie będzie podobne, z tą różnicą, że zamiast pisać nasze własne funkcje testujące pola, całą logikę przeniesiemy na atrybuty pól.

Zacznijmy od sposobu z wstawieniem błędów bezpośrednio do HTML. Po pierwsze musimy naszym polom dodać atrybut required (dla pierwszego także pattern), a po drugie wstawić za nimi komunikaty błędów - domyślnie ukryte przez CSS.


<form class="form form-error-inline" id="<?=$formID?>" method="post" action="">
    <div class="form-row">
        <label for="<?=$formID?>-name">Imię (min. 3 znaki)*</label>
        <input type="text" name="name" required pattern=".{3,}" id="<?=$formID?>-name">
        <div class="form-error-text">Wpisz poprawne imię</div>
    </div>
    <div class="form-row">
        <label for="<?=$formID?>-email">Email*</label>
        <input type="email" name="email" required id="<?=$formID?>-email">
        <div class="form-error-text">Wpisz poprawny email</div>
    </div>
    <div class="form-row">
        <button type="submit" class="button submit-btn">
            Wyślij
        </button>
    </div>
</form>

function toggleErrorField(field, show) {
    const errorText = field.nextElementSibling;
    if (errorText !== null) {
        if (errorText.classList.contains("form-error-text")) {
            errorText.style.display = show ? "block" : "none";
            errorText.setAttribute('aria-hidden', show);
        }
    }
};

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 inputs = [inputName, inputEmail];

form.setAttribute("novalidate", true);

//etap 1 : podpinam eventy
for (const el of inputs) {
    el.addEventListener("input", e => markFieldAsError(e.target, !e.target.checkValidity()));
}

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);
        toggleErrorField(el, false);

        if (!el.checkValidity()) {
            markFieldAsError(el, true);
            toggleErrorField(el, true);
            formErrors = true;
        }
    }

    if (!formErrors) {
        e.target.submit();
    }
});
Wpisz poprawne imię
Wpisz poprawny email

Dynamicznie generowane błędy

Spróbujmy teraz - podobnie jak poprzednio - wygenerować błędy dynamicznie.

Po pierwsze musimy ponownie stworzyć funkcje generują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();
        }
    }
};

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);
        }
    }
};

Tym razem komunikaty będziemy trzymać jako dodatkowy atrybut data-text-error każdego z pola:


<input type="text" required name="name" id="name" data-text-error="Wpisz poprawne imię">
<input type="email" required name="email" id="email" data-text-error="Wpisz poprawny email">

Dzięki temu całość będziemy mogli ograć za pomocą pętli:


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

function removeFieldError(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";
            errorText.setAttribute('aria-hidden', show);
        }
    }
};

function markFieldAsError(field, show) {
    if (show) {
        field.classList.add("field-error");
    } else {
        field.classList.remove("field-error");
        toggleErrorField(field, false);
    }
};

const form = document.querySelector("form");
const inputName = form.querySelector("input[name=name]");
const inputEmail = form.querySelector("input[name=email]");
const inputs = [inputName, inputEmail];

form.setAttribute("novalidate", true);

for (const el of inputs) {
    el.addEventListener("input", e => markFieldAsError(e.target, !e.target.checkValidity()));
}

form.addEventListener("submit", e => {
    e.preventDefault();

    let formHasErrors = false;

    //-------------------------
    //2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
    //-------------------------
    for (const el of inputs) {
        removeFieldError(el);
        el.classList.remove("field-error");

        if (!el.checkValidity()) {
            createFieldError(el, el.dataset.textError);
            el.classList.add("field-error");
            formHasErrors = true;
        }
    }

    if (!formHasErrors) { //jeżeli nie ma błędów wysyłamy formularz
        e.target.submit();
    }
});

Validity State

Każde z pól formularza ma właściwość validity, która wskazuje na obiekt z kilkoma cennymi właściwościami:

valid Zwraca true/false w zależności czy pole przeszło walidację
valueMissing zwraca true gdy pole wymaga wartości, a jej nie podano
typeMismatch Zwraca true gdy pole jest typu url/email a podana wartość jest błędna
tooShort Zwraca true jeżeli pole posiada atrybut minLength a podana wartość jest za krótka
tooLong wraca true jeżeli pole posiada atrybut maxLength a podana wartość jest za długa
patternMismatch Zwraca true jeżeli pole posiada atrybut pattern, a wpisana wartość nie pasuje do niego
badInput Zwraca true jeżeli pole jest typu number, a wpisana wartość nie jest liczbą
stepMismatch Zwraca true jeżeli pole ma atrybut step, a wartość pola nie pasuje do tego skoku
rangeOverflow Zwraca true jeżeli pole posiada atrybut max, a podana wartość go przekroczyła
rangeUnderflow Zwraca true jeżeli pole posiada atrybut min, a podana wartość jest od niego mniejsza

const form = document.querySelector("form");
const input = form.querySelector("input");

form.setAttribute("novalidate", true);

form.addEventListener("submit", e => {
    e.preventDefault();
    console.log(input.validity);
});

Dzięki tej właściwości możemy pokusić się o pominięcie używania dodatkowych atrybutów do przechowywania komunikatów błędów, a zamiast nich odpowiednie sprawdzanie powyższych właściwości. Dzięki temu nasze komunikaty mogą być bardziej "inteligentne".

Napiszmy funkcję, która za pomocą składowych tej właściwości zwróci nam odpowiedni komunikat błędu:


function getFieldError(el) {
    const validity = el.validity;

    //jeżeli poprawne to zwróć true
    if (validity.valid) return true;

    //Jeżeli pole jest puste
    if (validity.valueMissing) return 'Wypełnij pole';

    //Jeżeli pole ma wpisaną błędną wartość (np. w pole email wpisano zły adres)
    if (validity.typeMismatch) {
        if (el.type === 'email') return 'Wpisz poprawny email';
        if (el.type === 'url') return 'Wpisz poprawny adres URL';
    }

    //Długość tekstu jest za krótka
    if (validity.tooShort) return 'Wpisana wartość jest za krótka';

    //Długość tekstu jest za długa
    if (validity.tooLong) return 'Wpisana wartość jest za długa';

    //Wpisana wartość nie jest liczbą
    if (validity.badInput) return 'Wpisz liczbę';

    //Jeżeli wpisana liczba nie pasuje do skoku
    if (validity.stepMismatch) return 'Wybierz poprawną wartość';

    //Jeżeli wartość pola jest większa niż zakres
    if (validity.rangeOverflow) return 'Wybierz mniejszą wartość';

    //Jeżeli wartość pola jest mniejsza niż zakres
    if (validity.rangeUnderflow) return 'Wybierz większą wartość';

    //Jeżeli wpisana wartość nie pasuje do wzorca
    if (validity.patternMismatch) return 'Wpisana wartość nie spełnia wymagań';

    //Jeżeli żaden z powyższych
    return 'Wypełnij poprawnie pole';
};

I zastosujmy ją w naszym poprzednim kodzie. Tak napawdę do zmiany mamy tylko jedną liniję:


function getFieldError(el) {
    ...
};

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

function removeFieldError(field) {
    ...
};

function toggleErrorField(field, show) {
    ...
};

function markFieldAsError(field, show) {
    ...
};

const form = document.querySelector("form");
const inputName = form.querySelector("input[name=name]");
const inputEmail = form.querySelector("input[name=email]");
const inputs = [inputName, inputEmail];

form.setAttribute("novalidate", true);

for (const el of inputs) {
    el.addEventListener("input", e => markFieldAsError(e.target, !e.target.checkValidity()));
}

form.addEventListener("submit", e => {
    e.preventDefault();

    let formHasErrors = false;

    //-------------------------
    //2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
    //-------------------------
    for (const el of inputs) {
        removeFieldError(el);
        el.classList.remove("field-error");

        if (!el.checkValidity()) {
            createFieldError(el, getFieldError(el));
            el.classList.add("field-error");
            formHasErrors = true;
        }
    }

    if (!formHasErrors) { //jeżeli nie ma błędów wysyłamy formularz
        e.target.submit();
    }
});

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.

Menu