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

//wyłączamy domyślną walidację by nie wyskakiwały nam przeglądarkowe komunikaty
form.setAttribute("novalidate", true);

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

    let formErrors = [];

    if (!inputName.checkValidity()) {
        formErrors.push("Wypełnij poprawnie pole z imieniem");
    }

    if (!inputEmail.checkValidity()) {
        formErrors.push("Wypełnij poprawnie pole z emailem");
    }

    //jeżeli nie ma błędów wysyłamy formularz
    if (!formErrors.length) {
        form.submit();
    } 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="form3" method="post" action="">
    <div class="form-row">
        <label for="form3-name">Imię (min. 3 znaki)*</label>
        <input type="text" name="name" required pattern=".{3,}" id="form3-name">
        <div class="form-error-text" aria-hidden="false">Wpisz poprawne imię</div>
    </div>
    <div class="form-row">
        <label for="form3-email">Email*</label>
        <input type="email" name="email" required id="form3-email">
        <div class="form-error-text" aria-hidden="false">Wpisz poprawny email</div>
    </div>
    <div class="form-row">
        <button type="submit" class="button submit-btn">
            Wyślij
        </button>
    </div>
</form>

W poniższym kodzie dodałem dwie funkcje. Funkcja toggleErrorField(field, show) służy pokazaniu komunikatu błędu za danym polem. Aby to zrobić sprawdzam wcześniej, czy taki komunikat w ogóle istnieje (linia 3).

Druga funkcja to markFieldAsError(field, hasError) posłuży nam do zaznaczania błędnego pola.


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

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

form.setAttribute("novalidate", true);

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

    let formErrors = false;

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

Powyższy kod zadziała tylko przy próbie wysłania formularza. Możemy też dodać dynamiczne reagowanie na to co wpisuje użytkownik:


...

form.setAttribute("novalidate", true);

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

form.addEventListener("submit", e => {
    ...
});
Wpisz poprawne imię
Wpisz poprawny email

Dynamicznie generowane błędy

Spróbujmy teraz - podobnie w poprzednim artykule - 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 komunikatu usuwam stary

    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-error-text każdego z pola:


<input type="text" required name="name" id="name" data-error-text="Wpisz poprawne imię">
<input type="email" required name="email" id="email" data-error-text="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, hasError) {
    if (hasError) {
        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;

    for (const el of inputs) {
        removeFieldError(el);
        el.classList.remove("field-error");

        if (!el.checkValidity()) {
            createFieldError(el, el.dataset.errorText);
            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 jakiejś wartości, a jej nie podano
typeMismatch Zwraca true gdy pole jest typu url/email a podana wartość jest błędna (np. do pola email wpisano wartość nie będącą emailem, a do url wpisano treść nie będącą adresem)
tooShort Zwraca true jeżeli wpisany tekst jest krótszy niż wartość atrybutu minLength
tooLong Zwraca true jeżeli wpisany tekst jest dłuższy niż wartość atrybutu maxLength
patternMismatch Zwraca true jeżeli pole posiada atrybut pattern, a wpisana wartość nie pasuje do niego
badInput Zwraca true jeżeli wpisana wartość nie może być skonwertowana na wymagany format. Dla przykładu gdy do pola number wpiszemy tekst nie będący 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, a dla url wpisano tekst nie będący urlem)
    if (validity.typeMismatch) {
        if (el.type === 'email') return 'Wpisz poprawny email';
        if (el.type === 'url') return 'Wpisz poprawny adres URL';
    }

    //jeżeli długość tekstu określana atrybutem minlength jest za krótka
    if (validity.tooShort) return 'Wpisana wartość jest za krótka';

    //jeżeli długość tekstu określana atrybutem maxlength jest za długa
    if (validity.tooLong) return 'Wpisana wartość jest za długa';

    //jeżeli wpisana wartość nie może być skonwertowana na odpowiedni format
    //np. gdy do pola number użytkownik wpisze tekst
    if (validity.badInput) return 'Wpisz liczbę';

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

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

    //jeżeli wartość pola jest mniejsza niż zakres określony atrybutem min
    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 naprawdę do zmiany mamy tylko jedną linijkę:


function getFieldError(el) {
    ...
}

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

function removeFieldError(field) {
    ...
}

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

function markFieldAsError(field, hasError) {
    ...
}

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;

    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.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.