Formularze - klasa walidacji

W poniższym tekście zajmiemy się zbudowaniem przykładowej klasy, która posłuży nam do sprawdzania większych formularzy.

HTML i CSS

Do naszych celów stwórzmy prosty formularz z kilkoma rodzajami podstawowych pól oraz prostym stylowaniem.

Pokaż HTML

<form class="form" action="adres-do-wysylki" method="post">
    <div class="form-row">
        <label for="formName" class="label-block">Tekst</label>
        <input type="text" id="formName" placeholder="Może być puste" />
    </div>

    <div class="form-row">
        <label for="formDate" class="label-block">Data</label>
        <input type="text" id="formDate" pattern="^\d{2}\.\d{2}\.\d{4}$" required placeholder="dd.mm.rrrr" />
    </div>

    <div class="form-row">
        <label for="formUrl" class="label-block">Adres URL</label>
        <input type="url" id="formUrl" required placeholder="http://" />
    </div>

    <div class="form-row">
        <label for="formEmail" class="label-block">Email</label>
        <input type="email"  id="formEmail" required placeholder="Email" />
    </div>

    <div class="form-row">
        <label for="formMessage" class="label-block">Wiadomość</label>
        <textarea id="formMessage" required></textarea>
    </div>

    <div class="form-row">
        <label for="formThing" class="label-block">Wybierz super opcję</label>
        <select id="formThing" required>
            <option value="">Wybierz</option>
            <option value="1">Super opcja 1</option>
            <option value="2">Super opcja 2</option>
        </select>
    </div>

    <fieldset class="form-row">
        <legend>Wybierz płeć</legend>
        <div class="form-group">
            <label><input type="radio" name="gender" value="m" required  /> Mężczyzna</label>
            <label><input type="radio" name="gender" value="w" /> Kobieta</label>
            <label><input type="radio" name="gender" value="o" /> Inna</label>
        </div>
    </fieldset>

    <div class="form-row">
        <label><input type="checkbox" required name="spamLike" /> Zgadzam się na dostawanie spamu</label>
    </div>

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

Pokaż CSS

.form {
    max-width: 600px;
    margin: 10px auto;
    border-radius: 2px;
    padding: 1.5rem;
    box-shadow: 0 3px 10px 1px rgba(0, 0, 0, .05);
    background: #EEE;
    font-family: sans-serif;
}

.form fieldset {
    border: 0;
    padding: 0;
    margin:0;
}

.form legend {
    font-weight: bold;
    font-size: 0.9rem;
    display: block;
    margin: 5px 0;
}

.form label {
    font-size: 0.9rem;
    margin: 0 0 0.5rem;
    line-height: 1.5rem;
}

.form select,
.form input[type=text],
.form input[type=email],
.form input[type=url],
.form textarea {
    display: block;
    width: 100%;
    border-radius: 2px;
    line-height: 40px;
    font-family: sans-serif;
    box-sizing: border-box;
    padding: 0 10px;
    color: #333;
    border: 1px solid #DDD;
    background: #FFF;
}

.form textarea {
    height: 120px;
}

.form select {
    padding: 4px 4px 4px 10px;
}

.form select:not([multiple]) {
    height: 36px;
}

.form .form-row {
    padding-bottom: 20px;
}

.form .button {
    background: rgb(255, 99, 71);
    color: #FFF;
    cursor: pointer;
    padding: 15px 50px;
    border-radius: 5px;
    border: 0;
    box-shadow: 0 2px 10px rgba(255, 99, 71, 0.5)
}

/* błędy w formularzu */
.form .form-error {
    margin-left: -1.5rem;
    padding-left: 1.5rem;
}

.form .form-error label,
.form .form-error legend {
    color: tomato;
}

.form .form-error select,
.form .form-error input[type=text],
.form .form-error input[type=email],
.form .form-error input[type=url],
.form .form-error textarea {
    border-color: tomato;
}

.form .form-error select:focus,
.form .form-error input[type=text]:focus,
.form .form-error input[type=email]:focus,
.form .form-error input[type=url]:focus,
.form .form-error textarea:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(255, 99, 71, 0.3);
}
Przykładowy formularz

Jeżeli przeczytałeś poprzedni artykuł, powyższy kod nie powinien być dla ciebie zaskoczeniem. Każde z pól posiada HTMLową walidację, a to za sprawą zastosowanych atrybutów require i pattern.

Nową rzeczą z którą musimy się zmierzyć to grupy pól - radio.

Jeżeli chcemy by grupa inputów typu radio była wymagana, wtedy przynajmniej jeden z nich powinien mieć atrybut required (1).

Początek

Przechodzimy do pracy.
Sporo tutaj będzie się powtarzało z poprzednich działów. Walidację napiszmy w formie klasy. Zacznijmy od konstruktora:


class FormValidate {
    constructor(form, options) {
        const defaultOptions = { //domyślne opcje naszej walidacji
            classError : "error"
        };

        this.form = form; //właściwy formularz

        //tworzymy opcje dla naszego obiektu
        //scalając przekazane opcje z obiektem defaultOptions
        this.options = Object.assign({}, defaultOptions, options);

        //wyłączamy walidację HTML
        this.form.setAttribute("novalidate", "novalidate");
    }
};

W powyższym kodzie do zbudowania opcji dla całego formularza zastosowałem technikę wykorzystaną już w artykule o sliderze, a którą omawialiśmy tutaj.

Żeby w ogóle myśleć o naszej walidacji, musimy też wyłączyć HTMLową walidację (linia 14).

Etap 1 - podpowiedzi dla użytkownika

Zacznijmy od pierwszego punktu naszego planu. Podczas wypełniania formularza, dynamicznie będziemy użytkownikowi pokazywać informacje, czy popełnia błędy. Aby to zrobić do każdego pola podepniemy odpowiednie zdarzenie.

Pracę napiszmy od funkcji, która zwróci nam odpowiednie pola.


class FormValidate {
    ...

    getFields() {
        const inputs = [...this.form.querySelectorAll("input:not(:disabled), select:not(:disabled), textarea:not(:disabled)")];
        const result = [];

        for (const el of inputs) {
            if (el.willValidate) {
                result.push(el);
            }
        }

        return result;
    }
}

Raczej interesują nas pola które nie są :disabled, ponieważ takie domyślnie nie są wysyłane.

Dokumentacja mówi, że przy grupach radio nie musimy dawać require dla każdego pola. Tak więc zrobiliśmy. Z tego też powodu przy pobieraniu elementów nie mogliśmy użyć selektora *[required], ponieważ wtedy zwróciło by nam tylko elementy z tym atrybutem.

Zamiast tego pobraliśmy wszystkie pola, a następnie odsialiśmy te, które będą uczestniczyć w walidacji (willValidate) - czyli też radio bez required.

Kolejnym krokiem będzie napisanie funkcji, która podepnie pod te pola odpowiednie zdarzenia:


class FormValidate {
    ...

    prepareElements() {
        const elements = this.getFields();

        for (const el of elements) {
            const tag = el.tagName.toLowerCase();
            const type = el.type.toLowerCase();
            let eventName = "input";

            if (type === "checkbox" || type === "radio" || tag === "select") { //checkboxa i radio
                eventName = "change";
            }

            el.addEventListener(eventName, e => this.testInput(e.target));
        }
    }
}

I odpalamy ją w konstruktorze:


class FormValidate() {
    constructor(form, options) {
        const defaultOptions = {
            classError : "error"
        };

        this.form = form;
        this.options = Object.assign({}, defaultOptions, options);

        this.form.setAttribute("novalidate", "novalidate");

        this.prepareElements();
    }

    ...
};

Funkcja prepareElements() robi pętlę po wszystkich polach z atrybutem required. Dla każdego takiego pola odpalana jest funkcja spradzająca. Za chwilę napiszemy kod każdej takiej funkcji.

Sprawdzanie pól

Funkcja sprawdzająca dane pole wykorzysta do tego poznaną wcześniej checkValidity():


class FormValidate {
    ...

    testInput(input) {
        const valid = input.checkValidity();
        this.markFieldAsError(input, !valid);
        return valid;
    }
};

Jeżeli dane pole będzie błędnie wyświetlone, wtedy pokażemy błąd za pomocą funkcji markFieldAsError(input, czyPoleJestBledne) (pisaliśmy ją w tym artykule). W tym przypadku wydaje się, że łatwiej nam będzie dodawać błędy nie bezpośrednio do pola, co do .form-row w którym dane pole się znajduje. Dzięki temu łatwiej nam będzie stylować np. grupy radio.


class FormValidate {
    ...

    markFieldAsError(field, show) {
        if (show) {
            field.closest(".form-row").classList.add("form-error");
        } else {
            field.closest(".form-row").classList.remove("form-error");
        }
    }
}

Funkcja ta będzie dodawać klasę błędu do elementu nadrzędnego, a nie samego inputa. Dzięki temu będziemy mieli o wiele większe możliwości stylowania:


.form-error label {
    color: tomato;
}

.form-error select,
.form-error input[type=text],
.form-error input[type=email],
.form-error input[type=url],
.form-error textarea {
    border-color: tomato;
}

.form select.field-error:focus,
.form-error input[type=text]:focus,
.form-error input[type=email]:focus,
.form-error input[type=url]:focus,
.form-error textarea:focus {
    outline: none;
    box-shadow: 0 0 0 3px rgba(255, 99, 71, 0.3);
}

Etap pierwszy mamy zakończony. W tym momencie wystarczy stworzyć egzemplarz naszej klasy, by uzyskać dynamiczne sprawdzanie pól w formularzu:


document.addEventListener("DOMContentLoaded", () => {
    const cfg = {};
    const form = document.querySelector(".form");
    const fv = new FormValidate(form, cfg);
});
Sprawdź w działaniu.

Etap 2 - sprawdzanie danych przy wysyłaniu formularza

Pozostało nam do wykonania sprawdzenie danych przy wysyłce formularza. Jeżeli dane są błędne, wtedy powinniśmy pokazać odpowiednie błędy w formularzu, oraz przerwać jego wysyłanie. Jest to proste zadanie, ponieważ wszystkie niezbędne metody mamy już napisane. Wystarczy dodać obsługę zdarzenia submit dla formularza:


class FormValidate {
    ...

    bindSubmit() {
        this.form.addEventListener('submit', e => {
            e.preventDefault();

            const elements = this.getFields();

            for (const el of elements) {
                this.markFieldAsError(el, !el.checkValidity());
            }
        });
    }
}

I tak jak poprzednio odpalenie tej metody w konstruktorze:


class FormValidate {
    constructor(form, options) {
        const defaultOptions = {
            classError : "error"
        }

        this.form = form;
        this.options = Object.assign({}, defaultOptions, options);

        this.form.setAttribute("novalidate", "novalidate");

        this.prepareElements();
        this.bindSubmit();
    }
};
Zobacz działanie formularza

Etap 3 - sprawdzanie po stronie serwera

Nie zapomnijmy o najważniejszym etapie, czyli walidacji po stronie serwera. Tutaj niestety będę musiał zakończyć by się nie ośmieszyć przed swoimi znajomymi backendowcami. Mini podejście przedstawiłem ci w rozdziale gdzie tworzyliśmy formularz ajaxowy. Zapewne sporo kolegów backendowców będzie tutaj zgrzytać zębami, bo na co dzień używa się do takich rzeczy mechanizmów z Symfony czy Laravela. I dobrze - to ich działka, nie będziemy im tutaj wkraczać z butami...

Inne typy kontrolek

A teraz łyżka dziegciu w beczce miodu...

Jeżeli chcielibyśmy podobny testy zrobić dla niestandardowych kontrolek - chociażby dla grupy checkboxów (np. "wybierz minimum 2 owoce"), czy np. minimum kilku opcjach w select:multiple, to... za pomocą walidacji HTML nie jesteśmy w stanie tego zrobić. Nie mówiąc już o czymś bardziej skomplikowanym...

Stworzyłem jakiś czas temu wpis pod adresem http://domanart.pl/ucieczka-z-nowego-jorku/. Zamieściłem tam link do jednego z odcinków podcasta, w którym autorzy dyskutują na temat Gutenberga - nowego edytora w Wordpressie. Najbardziej pamiętliwy moment tego słuchowiska to 1:08:30, w którym autor - słusznie zresztą - wytyka duży problem dzisiejszego frontendu.

Okazuje się bowiem, że w sprawie dostępnych kontrolek (jak i wielu innych rzeczy) przespaliśmy ostatnie lata. Dla formularzy mamy kilka podstawowych elementów - inputy, radio, checkboxy, selekty i kilka mocno podstawowych. Ale jeżeli przyjrzeć się im bliżej, okaże się, że w zasadzie każdą z tych kontrolek spokojnie można by o wiele bardziej dopracować, a i wciąż brakuje kontrolek z prawdziwego zdarzenia.

Oczywiście możemy tutaj podziałać Javascriptem, ale w czystym HTML mamy naprawdę mocno ograniczone możliwości, które dodatkowo są różnie wyświetlane na przeglądarkach.

Podobnie jest właśnie z grupami checkboxów. Wątek na stackoverflow (pierwszy post) wskazuje na zgłoszone w tej sprawie issue, które zostało odrzucone.

Jak to rozwiązać? Albo napiszemy własne Javascriptowe rozwiązanie, albo zamiast checkboxów zastosujemy element select[multiple] (przy czym tutaj nie ma atrybutów na minimalną i maksymalną liczbę wybranych elementów). Można też pójść ciut dalej i zamieniać za pomocą Javascript select[multiple] na grupę checkboxów. Ten pomysł wychodzi poza ramy tego artykułu, ale jeżeli by cię coś takiego interesowało, możesz zobaczyć jeden z moich starych artykułów (1, 2) - przy czym idealnym rozwiązaniem wydaje się stworzenie własnego web komponentu?.

Osobiście raczej nie dodawał bym takich dodatków do głównej klasy, ponieważ po krótkim czasie rozrosła by się ona do monstrualnych rozmiarów. To raczej są indywidualne sytuacje, które wymagają zastosowania sytuacyjnego podejścia, które używaliśmy przy okazji omawiania kontrolek.

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