Walidacja Formularzy

Poniższy artykuł jest tak naprawdę przerobioną na Vanilla JS kopią mojego innego artykułu traktującego o męczeniu formularzy za pomocą jQuery. Jeżeli interesuje Cię ten temat, lub zwyczajnie gubisz się w poniższym tekście, zapraszam do lektury tamtego wiekopomnego dzieła. Część kwestii jest tam dokładniej opisana.

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>Tekst</label>
        <input type="text" placeholder="Może być puste" />
    </div>
    
    <div>
        <label>Tekst</label>
        <input type="text" required placeholder="Wpisz jakiś tekst" />
    </div>

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

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

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

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

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

    <div>
        <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
    </div>

    <div>
        <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
    </div>

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


var validateForm = (function() {
    //prywatne właściwości
    var options = {};
    
    //metoda publiczna
    var 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;
        }
    }
    
    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:


var validateForm = (function() {
    ...

    var prepareElements = function() {
        var elements = options.form.querySelectorAll('input[required], textarea[required], select[required]');
    
        //przyjemniejsza forma for
        [].forEach.call(elements, function(element) {
            //usuwamy atrybut required - inaczej przy wysyłaniu wyskakiwały by domyślne błędy przeglądarki
            element.removeAttribute('required');
            //dodajemy klasę - po niej będziemy później sprawdzać pola
            element.className += ' required';

            //sprawdzamy typ pola
            if (element.nodeName.toUpperCase() == 'INPUT') {
                var 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)});
            }
        });
    };
    
    var 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:


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

    if (pattern != null) {                
        var 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ć:


var showFieldValidation = function(input, inputIsValid) {
    if (!inputIsValid) {
        if (!input.parentNode.className || input.parentNode.className.indexOf(options.classError)==-1) {
            input.parentNode.className += ' ' + options.classError
        }
    } else {
        var regError = new RegExp('(\\s|^)'+options.classError+'(\\s|$)');
        input.parentNode.className = input.parentNode.className.replace(regError, '');
    }
};

Zastosowana jest tutaj technika poznana w poprzednim rozdziale, z tym że dodajemy ją do elementu nadrzędnego. Czemu tak robimy? Dzięki temu w łatwy sposób możemy poprzez tą klasę ostylować samo pole, co i pokazać ukryty domyślnie komunikat błędu.

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:


var testInputEmail = function(input) {
    var fieldHasError = false;
    var 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:


var testInputURL = function(input) {
    var 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:


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


var testInputCheckbox = function(input) {
    var name = input.getAttribute('name');
    
    //pobieramy formularz metodą, którą poznaliśmy juz poprzednio...
    var group = input.form.querySelectorAll('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:


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

    var showFieldValidation = function(input, inputIsValid) {
        if (!inputIsValid) {
            if (!input.parentNode.className || input.parentNode.className.indexOf(options.classError)==-1) {
                input.parentNode.className += ' ' + options.classError
            }
        } else {
            var regError = new RegExp('(\\s|^)'+options.classError+'(\\s|$)');
            input.parentNode.className = input.parentNode.className.replace(regError, '');
        }
    };

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

        if (pattern != null) {                
            var 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;
        }    
    };
    
    var testInputEmail = function(input) {
        var fieldHasError = false;
        var 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;
        }    
    };
    
    var testInputURL = function(input) {        
        var urlReg = new RegExp('^http:\/\/.+', 'i');
        if (!urlReg.test(input.value)) {
            showFieldValidation(input, false);
            return false;
        } else {
            showFieldValidation(input, true);
            return true;
        }    
    };
    
    var 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;
        }
    };
    
    var testInputCheckbox = function(input) {
        var name = input.getAttribute('name');
        var group = input.form.querySelectorAll('input[name="'+name+'"]:checked');
        
        if (group.length) {
            showFieldValidation(input, true);
            return true;            
        } else {
            showFieldValidation(input, false);
            return false;
        }
    };

    var prepareElements = function() {
        var elements = options.form.querySelectorAll('input[required], textarea[required], select[required]');

        [].forEach.call(elements, function(element) {
            element.removeAttribute('required');
            element.className += ' required';

            if (element.nodeName.toUpperCase() == 'INPUT') {
                var 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)});
            }
        });        
    };

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


var formSubmit = function() {
    options.form.addEventListener('submit', function(e) {
        e.preventDefault();
    
        var validated = true;
    
        //pobieramy wszystkie pola, którym wcześniej ustawiliśmy klasę .required
        var elements = options.form.querySelectorAll('.required');

        //podobne działanie już robiliśmy przy przygotowywaniu pól
        [].forEach.call(elements, function(element) {
            if (element.nodeName.toUpperCase() == 'INPUT') {
                var 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;
        }
    });    
};
    
var init = function(_options) {
    //do naszego modulu bedziemy przekazywac opcje
    options = {
        form : _options.form || null,
        classError : _options.classError || 'error'
    }
    //jak nie przekazaliśmy formularza
    iif (options.form == null || options.form == undefined || options.form.length==0) {
            console.warn('validateForm: Źle przekazany formularz');
            return false;
        }
    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 :P)