Walidacja Formularzy

Poniższy artykuł jest tak naprawdę przerobioną na Vanilla JS kopią mojego innego artykułu traktującego o walidacji formularzy za pomocą jQuery. Jeżeli interesuje Cię ten temat, lub zwyczajnie gubisz się w poniższym tekście, zapraszam do lektury tamtego tekstu. Część kwestii jest tam dokładniej opisana. Polecam też przyjrzeć się mojemu innemu tekstowi o formularzach - czyli tworzeniu formularza kontaktowego.

Przyszedł najwyższy czas na sprawdzenie, czy nie chcą nas oszukać :)
Sprawdzanie danych, które wprowadza użytkownik to jedna z ważniejszych rzeczy o jaką musimy zadbać przy tworzeniu strony.

Do sprawdzania pól formularza wykorzystamy metody String, wyrażenia regularne + trochę kawy do picia.

Weryfikacja danych powinna przebiegać w 3 etapach:

  1. Pierwszy etap to prosta podpowiedź w czasie wprowadzania danych przez użytkownika. Wykorzystamy tutaj zdarzenia change, select, focus, blur...
  2. Drugi etap to sprawdzenie danych tuż przed wysłaniem. Jeżeli są poprawne to je wysyłamy na serwer, jeżeli nie, wyświetlamy stosowną informację i przesyłania nie wykonujemy.
  3. Ostatni - trzeci 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.

Trzeci etap sobie pominiemy, bo to zadanie dla działu programistów. Zajmiemy się 2 pierwszymi.

Html

Wykorzystajmy do naszych celów formularz, w którym umieścimy wszystkie zazwyczaj stosowane pola:


<form class="form">
    <div>
        <label for="formName">Tekst</label>
        <input type="text" id="formName" placeholder="Może być puste" />
    </div>

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

    <div>
        <label for="formUrl">URL</label>
        <input type="url" id="formUrl" required placeholder="http://" />
    </div>

    <div>
        <label for="formEmail" >Email</label>
        <input type="email"  id="formEmail" required placeholder="Email" />
    </div>

    <div>
        <label for="formMessage">Wpisz tylko abc</label>
        <textarea id="formMessage" required pattern="^abc$"></textarea>
    </div>

    <div>
        <label for="formThing">Wybierz</label>
        <select id="formThing" required>
            <option value="">Wybierz coś</option>
            <option value="1">Super opcja 1</option>
            <option value="2">Super opcja 2</option>
        </select>
    </div>

    <fieldset>
        <label>Zaznacz</label>
        <input type="radio" required name="test1" /> 1
        <input type="radio" required name="test1" /> 2
        <input type="radio" required name="test1" /> 3
        <input type="radio" required name="test1" /> 4
        <input type="radio" required name="test1" /> 5
    </fieldset>

    <fieldset>
        <label>Zaznacz</label>
        <input type="checkbox" required name="test2" /> 1
        <input type="checkbox" required name="test2" /> 2
        <input type="checkbox" required name="test2" /> 3
        <input type="checkbox" required name="test2" /> 4
        <input type="checkbox" required name="test2" /> 5
    </fieldset>

    <div>
        <input type="submit" value="Wyślij" />
    </div>
</form>

Do walidacji możemy skorzystać z metody checkValidity. Niestety metoda ta nie jest wspierana przez IE9 przez co dla wielu osób jest nie do użycia. Dlatego my skorzystamy z klasycznych metod walidacji, które zresztą już nie raz poznaliśmy. Czy wykorzystasz stare metody, czy nowe, poniżej nauczymy się kilku ciekawych technik.

Etap 1 - podpowiedzi dla użytkownika

Przechodzimy do pracy.

Nasz kod napiszemy w formie modułu. Dzięki temu nasz kod będzie zabezpieczony przed dłubaniem w jego zmiennych. Na zewnątrz naszego kodu wystawimy tylko metody i zmienne, które chcemy by były widoczne. W naszym przypadku będzie to tylko jedna metoda init, do której będziemy przekazywać tylko formularz i klasę błędu:


const validateForm = (function() {
    //prywatne właściwości
    let options = {};

    //metoda publiczna
    const init = function(_options) {
        //do naszego modułu będziemy przekazywać opcje
        //przekazane ustawimy w zmiennej options naszego modułu, lub ustawimy domyślne
        options = {
            form : _options.form || null,
            classError : _options.classError || 'error'
        }
        if (options.form == null || options.form == undefined || options.form.length==0) {
            console.warn('validateForm: Źle przekazany formularz');
            return false;
        }

        //ustawiamy dla form novalidate - dzięki temu nie będzie domyślnych dymków walidacji dla pól required
        options.form.setAttribute('novalidate', 'novalidate');
    }

    return {
        init : init
    }
})();

Przygotowanie pól do weryfikacji

Zacznijmy od pierwszego punktu naszego planu. Podczas wypełniania formularza, dynamicznie będziemy użytkownikowi pokazywać informacje, czy popełnia błędy:


const validateForm = (function() {
    ...

    const prepareElements = function() {
        const elements = options.form.querySelectorAll(':scope [required]');

        [].forEach.call(elements, function(element) {
            //sprawdzamy typ pola
            if (element.nodeName.toUpperCase() == 'INPUT') {
                const type = element.type.toUpperCase();

                //dla każdego pola dodajemy obsługę funkcji sprawdzającej
                if (type == 'TEXT') {
                    element.addEventListener('keyup', function() {testInputText(element)});
                    element.addEventListener('blur', function() {testInputText(element)});
                }
                if (type == 'EMAIL') {
                    element.addEventListener('keyup', function() {testInputEmail(element)});
                    element.addEventListener('blur', function() {testInputEmail(element)});
                }
                if (type == 'URL') {
                    element.addEventListener('keyup', function() {testInputURL(element)});
                    element.addEventListener('blur', function() {testInputURL(element)});
                }
                if (type == 'CHECKBOX') {
                    element.addEventListener('click', function() {testInputCheckbox(element)});
                }
                if (type == 'RADIO') {
                    element.addEventListener('click', function() {testInputCheckbox(element)});
                }
            }
            if (element.nodeName.toUpperCase() == 'TEXTAREA') {
                element.addEventListener('keyup', function() {testInputText(element)});
                element.addEventListener('blur', function() {testInputText(element)});
            }
            if (element.nodeName.toUpperCase() == 'SELECT') {
                element.addEventListener('change', function() {testInputSelect(element)});
            }
        });
    };

    const init = function(_options) {
        prepareElement();
    }
})();

Funkcja prepareElement robi pętlę po wszystkich polach z atrybutem required. Dla każdego takiego pola dodaje odpowiednie zdarzenie z obsługą funkcji sprawdzającej. Za chwilę napiszemy kod każdej takiej funkcji.

Sprawdzanie pól tekstowych

W powyższym kodzie do sprawdzania pól tekstowych wykorzystujemy funkcję testInputText(element). Napiszmy jej kod:


const testInputText = function(input) {
    let inputIsValid = true;
    const pattern = input.getAttribute('pattern');

    if (pattern != null) {
        const reg = new RegExp(pattern, 'gi');
        if (!reg.test(input.value)) {
            inputIsValid = false;
        }
    } else {
        if (input.value=='') {
            inputIsValid = false;
        }
    }

    if (inputIsValid) {
        showFieldValidation(input, true);
        return true;
    } else {
        showFieldValidation(input, false);
        return false;
    }
};

Każde pole tekstowe - input:text, textarea może zawierać atrybut pattern, który definiuje wzór teksty który musi się w tym polu pojawić. Dla przykładu dla pola z datą będzie to pattern ^\d{2}\.\d{2}\.\d{4}$ (spójrz na kod naszego formularza). Jeżeli takiego atrybutu pole nie posiada, a jest wymagane (a jest, bo przecież robimy pętlę tylko po polach required), to znaczy, że musi się w nim znaleźć jakaś wartość. W każdym z obydwu przypadków wyświetlamy błąd, lub go nie pokazujemy za pomocą metody showFieldValidation(input, czyPoleJestPoprawne). Ma ona postać:


const showFieldValidation = function(input, inputIsValid) {
    if (!inputIsValid) {
        input.parentNode.classList.add(options.classError);
    } else {
        input.parentNode.classList.remove(options.classError);
    }
};

Sprawdzanie pól typu email

Sprawdzanie pól email robiliśmy już w poprzednim rozdziale i w rozdziale o wyrażeniach regularnych, nie będzie to więc nic nowego. Tutaj tak samo jak w poprzednim teście wyświetlamy błędy za pomocą funkcji showFieldValidation:


const testInputEmail = function(input) {
    const mailReg = new RegExp('^[0-9a-zA-Z_.-]+@[0-9a-zA-Z.-]+\.[a-zA-Z]{2,3}$', 'gi');

    if (!mailReg.test(input.value)) {
        showFieldValidation(input, false);
        return false;
    } else {
        showFieldValidation(input, true);
        return true;
    }
};

Sprawdzanie pól typu url

Pola typu URL powinny zawierać http://, sam test jest więc bardzo prosty:


const testInputURL = function(input) {
    const urlReg = new RegExp('^http:\/\/.+', 'i');
    if (!urlReg.test(input.value)) {
        showFieldValidation(input, false);
        return false;
    } else {
        showFieldValidation(input, true);
        return true;
    }
};

Sprawdzanie selektów

Dla selektów sprawdzamy aktualnie wybraną pozycję. To także już robiliśmy :). Nic ciekawego:


const testInputSelect = function(select) {
    if (select.options[select.selectedIndex].value=='' || select.options[select.selectedIndex].value=='-1') {
        showFieldValidation(select, false);
        return false;
    } else {
        showFieldValidation(select, true);
        return true;
    }
};

Sprawdzanie radio i checkboxów

Do pól radio i checkbox musimy podejść nieco inaczej. Jak wiemy występują one w grupach, więc musimy testować całe grupy tych pól:


const testInputCheckbox = function(input) {
    const name = input.getAttribute('name');
    const group = input.form.querySelectorAll(':scope input[name="'+name+'"]:checked');

    if (group.length) {
        showFieldValidation(input, true);
        return true;
    } else {
        showFieldValidation(input, false);
        return false;
    }
};

Koniec etapu pierwszego

W tym momencie nasz kod wygląda następująco:


const validateForm = (function() {
    const options = {};
    const classError = 'error';

    const showFieldValidation = function(input, inputIsValid) {
        if (!inputIsValid) {
            input.parentNode.classList.add(options.classError);
        } else {
            input.parentNode.classList.remove(options.classError);
        }
    };

    const testInputText = function(input) {
        let inputIsValid = true;
        const pattern = input.getAttribute('pattern');

        if (pattern != null) {
            const reg = new RegExp(pattern, 'gi');
            if (!reg.test(input.value)) {
                inputIsValid = false;
            }
        } else {
            if (input.value=='') {
                inputIsValid = false;
            }
        }

        if (inputIsValid) {
            showFieldValidation(input, true);
            return true;
        } else {
            showFieldValidation(input, false);
            return false;
        }
    };

    const testInputEmail = function(input) {
        const mailReg = new RegExp('^[0-9a-zA-Z_.-]+@[0-9a-zA-Z.-]+\.[a-zA-Z]{2,3}$', 'gi');

        if (!mailReg.test(input.value)) {
            showFieldValidation(input, false);
            return false;
        } else {
            showFieldValidation(input, true);
            return true;
        }
    };

    const testInputURL = function(input) {
        const urlReg = new RegExp('^http:\/\/.+', 'i');
        if (!urlReg.test(input.value)) {
            showFieldValidation(input, false);
            return false;
        } else {
            showFieldValidation(input, true);
            return true;
        }
    };

    const testInputSelect = function(select) {
        if (select.options[select.selectedIndex].value=='' || select.options[select.selectedIndex].value=='-1') {
            showFieldValidation(select, false);
            return false;
        } else {
            showFieldValidation(select, true);
            return true;
        }
    };

    const testInputCheckbox = function(input) {
        const name = input.getAttribute('name');
        const group = input.form.querySelectorAll(':scope input[name="'+name+'"]:checked');

        if (group.length) {
            showFieldValidation(input, true);
            return true;
        } else {
            showFieldValidation(input, false);
            return false;
        }
    };

    const prepareElements = function() {
        const elements = options.form.querySelectorAll(':scope [required]');

        [].forEach.call(elements, function(element) {
            if (element.nodeName.toUpperCase() == 'INPUT') {
                const type = element.type.toUpperCase();

                if (type == 'TEXT') {
                    element.addEventListener('keyup', function() {testInputText(element)});
                    element.addEventListener('blur', function() {testInputText(element)});
                }
                if (type == 'EMAIL') {
                    element.addEventListener('keyup', function() {testInputEmail(element)});
                    element.addEventListener('blur', function() {testInputEmail(element)});
                }
                if (type == 'URL') {
                    element.addEventListener('keyup', function() {testInputURL(element)});
                    element.addEventListener('blur', function() {testInputURL(element)});
                }
                if (type == 'CHECKBOX') {
                    element.addEventListener('click', function() {testInputCheckbox(element)});
                }
                if (type == 'RADIO') {
                    element.addEventListener('click', function() {testInputCheckbox(element)});
                }
            }
            if (element.nodeName.toUpperCase() == 'TEXTAREA') {
                element.addEventListener('keyup', function() {testInputText(element)});
                element.addEventListener('blur', function() {testInputText(element)});
            }
            if (element.nodeName.toUpperCase() == 'SELECT') {
                element.addEventListener('change', function() {testInputSelect(element)});
            }
        });
    };

    const init = function(_options) {
        //do naszego modulu bedziemy przekazywac opcje
        options = {
            form : _options.form || null,
            classError : _options.classError || 'error'
        }
        if (options.form == null || options.form == undefined || options.form.length==0) {
            console.warn('validateForm: Źle przekazany formularz');
            return false;
        }

        //ustawiamy dla form novalidate - dzieki temu nie bedzie domyslnych dymkow walidacji dla pol required
        options.form.setAttribute('novalidate', 'novalidate');

        prepareElements();
    };

    return {
        init : init
    }
})();

Etap pierwszy mamy zakończony. W tym momencie wystarczy odpalić nasz moduł, by uzyskać dynamiczne sprawdzanie pól w formularzu:


document.addEventListener("DOMContentLoaded", function() {
    const form = document.querySelector('.form');
    validateForm.init({form : form})
});

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 formularza:


const formSubmit = function() {
    options.form.addEventListener('submit', function(e) {
        e.preventDefault();

        let validated = true;

        //pobieramy wszystkie pola, którym wcześniej ustawiliśmy klasę .required
        const elements = options.form.querySelectorAll(':scope [required]');

        //podobne działanie już robiliśmy przy przygotowywaniu pól
        [].forEach.call(elements, function(element) {
            if (element.nodeName.toUpperCase() == 'INPUT') {
                const type = element.type.toUpperCase();
                if (type == 'EMAIL') {
                    if (!testInputEmail(element)) validated = false;
                }
                if (type == 'URL') {
                    if (!testInputURL(element)) validated = false;
                }
                if (type == 'TEXT') {
                    if (!testInputText(element)) validated = false;
                }
                if (type == 'CHECKBOX') {
                    if (!testInputCheckbox(element)) validated = false;
                }
                if (type == 'RADIO') {
                    if (!testInputCheckbox(element)) validated = false;
                }
            }
            if (element.nodeName.toUpperCase() == 'TEXTAREA') {
                if (!testInputText(element)) validated = false;
            }
            if (element.nodeName.toUpperCase() == 'SELECT') {
                if (!testInputSelect(element)) validated = false;
            }
        });

        if (validated) {
            this.submit();
        } else {
            return false;
        }
    });
};

const init = function(_options) {
    //do naszego modułu będziemy przekazywać opcje
    options = {
        form : _options.form || null,
        classError : _options.classError || 'error'
    }
    //jak nie przekazaliśmy formularza
    if (options.form == null || options.form == undefined || options.form.length==0) {
        console.warn('validateForm: Źle przekazany formularz');
        return false;
    }

    //ustawiamy dla form novalidate - dzięki temu nie będzie domyślnych dymków walidacji dla pól required
    options.form.setAttribute('novalidate', 'novalidate');


    prepareElements();
    formSubmit();
}

Działanie jest tutaj identyczne co w funkcji prepareElements. Robimy pętlę po elementach z klasą .required (które zamienialiśmy w tamtej funkcji) i wykorzystując wcześniej przygotowane funkcje dokonujemy sprawdzenia, czy formularz możemy być wysłany.

Poniżej przykładowy formularz z poniższym bardzo prostym stylowaniem (+ jakieś małe, które używam w tym kursie):


.form .error {
    color:#DB083E;
}
.form .error input[type=text],
.form .error input[type="url"],
.form .error input[type="email"],
.form .error textarea,
.form .error select {
    color: #DB083E;
    border-color: #DB083E;
    background: #FFEDED url(/images/field-error.png) right center no-repeat;
    background-position: calc(100% - 10px) center;
    padding-right:40px;
}
.form .error textarea {
    background-position: calc(100% - 10px) 10px;
}
.form .error input[type="radio"],
.form .error input[type="checkbox"] {
    border-color: #DB083E;
    background: #FFEDED;
}
.form .error select {
    background-image: none;
    padding-right:4px;
}

Formularz testowy

1 2 3 4 5
1 2 3 4 5

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Posiłkując się artykułem wspomnianym na początku, dodaj do powyższego modułu walidację początkową, która będzie widoczna od razu po wejściu na stronę. Walidację tą pokazuj w zależności od dodatkowego parametru, który będziesz przekazywał w _options (ok, ok - zadanie to jest wykonane w tamtym artykule)