https://www.pexels.com/photo/black-and-white-business-code-coding-79290/

Walidacja pól formularza (JS)

Cześć! Wracam po przerwie z postem związanym z tematem Sterownika Domowego, który co prawda małymi kroczkami, ale jednak posuwa się do przodu. W kolejnej odsłonie postanowiłem popracować nad walidacją  po stronie przeglądarki dla formularza dodawania/edycji pomieszczenia. Poniżej krótkie podsumowanie, co i jak udało mi się zrobić.

Moje pierwsze działanie podyktowane było koniecznością uporządkowania plików *.js i *.css, które do tej pory były wczytywane w głównym szablonie main.scala.html i były dostępne w każdym widoku. Oczywiście nie jest to pożądane i było tylko tymczasowe. Od tej pory w dotychczasowym miejscu będą podpięte tylko ogólne arkusze stylów i skrypty, wykorzystywane w każdym widoku, natomiast w miejscu dynamicznych wyrażeń @css oraz @scripts będę wstawiał elementy specyficzne dla podstron. Poniższy listing jest przykładem takiej sytuacji, gdzie do widoku z formularzem dodaję pliki ze skryptami walidacyjnymi.

@scripts = {
    <script src="@routes.Assets.versioned("javascripts/validation.js")" type="text/javascript"></script>
    <script src="@routes.Assets.versioned("javascripts/addEditRoom.js")" type="text/javascript"></script>
}
 
@main("Home Controller", scripts) {
@if(option.equals("edit")){
        @{roomFormHeader = "Edycja pomieszczenia"}
            @helper.form(action = routes.RoomController.editRoom(editedRoomId) , 'onSubmit -> "return validate();") {
                @displayForm()
            }
    } else {
        @{roomFormHeader = "Dodaj nowe pomieszczenie"}
            @helper.form(action = routes.RoomController.addRoom() , 'onSubmit -> "return validate();") {
                @displayForm()
            }
    }
}

Drugą nowością w tym pliku jest zmiana w helperze tworzącym formę, gdzie dodałem reakcję na zdarzenie onSubmit, które wywołuje funkcję JS sprawdzającą treść w formularzu (‚onSubmit -> „return validate();”). Implementacja funkcji validate() znajdująca się w pliku addEditRoom.js, jest powtórnie wykorzystywana w „nasłuchiwaczach” zdarzeń typu change i click pola typu input, w którym użytkownik wpisuje nazwę pomieszczenia.

$(document).ready(function () {
    const field = document.getElementById("name");
    field.addEventListener("change", function () {
        validate();
    });
    field.addEventListener("click", function () {
        validate();
    });
 
});

Główna logika walidacji formularza dodawania/edycji pomieszczenia znajduje się w funkcji _prepareNameFieldErrorSpan(). Sprawdzane warunki w pewnej części powielają te wykonywane w procesie weryfikacji danych po stronie serwera (Bean Validation), ale również pojawiają się nowe ograniczenia. Walidacja w backendzie zabezpiecza aplikację przed celową próbą uszkodzenia systemu poprzez wprowadzenie niebezpiecznych lub niespójnych danych, natomiast we frontendzie ma służyć pomocą użytkownikowi, który w szybki sposób (bez przeładowania strony) dowiaduje się o popełnionym błędzie oraz o dozwolonych treściach i formatach danych. W utworzonym przeze mnie się kodzie znajdują się nazwy rozpoczynające się od znaku podkreślenia (_). Przyjąłem taką konwencję nazewniczą dla funkcji, które wykorzystywane są wewnątrz danego pliku, a powstały najczęściej w wyniku refaktoryzacji kodu.

function _prepareNameFieldErrorSpan(field) {
    clearError(field);
    let fieldHasErrors = false;
    if (isFieldEmpty(field)) {
        printError(field, "Pole nie może być puste.");
        fieldHasErrors = true;
    } else {
        _ajaxCheckRoomExists(field.value);
        _checkIsNumericalContent(field);
        _checkHasMinLength(field);
        _checkHasMaxLength(field);
    }
    return fieldHasErrors;
}
 
function _checkIsNumericalContent(field) {
    if (!hasFieldNumericalContent(field)) {
        printError(field, "Wartości liczbowe nie są dozwolone w tym miejscu");
        fieldHasErrors = true;
    }
}

W omawianym formularzu polem weryfikowanym będzie nazwa pomieszczenia, przy czym ograniczam się do sprawdzania poniższych warunków.

  1. Czy pole nie jest puste?
  2. Czy pole nie zawiera liczby? (dopuszcza się wpisanie ciągu będącego połączeniem liter i cyfr)
  3. Czy długość ciągu znaków jest większa lub równa 2?
  4. Czy długość ciągu znaków jest mniejsza niż 20?
  5. Czy pomieszczenie o podanej nazwie nie istnieje już w bazie danych?

Pierwsze cztery z powyższych korzystają z prostych operacji testujących zawartość pola formularza. Implementacje funkcji takich jak isFieldEmpty(), hasFieldMaxLength() czy hasFieldNumericalContent() umieściłem w pliku validation.js, w którym to znajduje się również inna logika ogólnego przeznaczenia do operacji walidacji formularzy. Przykłady to printError() oraz clearError(), których odpowiedzialność związana jest z prezentacją komunikatu błędu, a polegająca na wyświetleniu odpowiedniej notki w elemencie <span class=”form-error”>.

function printError(element, errorMessage) {
    _markInputAsInvalid(element);
    _showErrorMessages(element, errorMessage);
}
function _showErrorMessages(element, errorMessage) {
    const parentElement = element.parentNode;
    const errorsField = parentElement.getElementsByClassName("form-error");
    if (_wasErrorsBefore(errorsField)) {
        _addNextError(errorsField, errorMessage);
    } else {
        _addFirstError(parentElement, errorMessage);
    }
}
 
function _wasErrorsBefore(errorsField) {
    return errorsField.length > 0;
}
 
function _addFirstError(parentElement, errorMessage) {
    const errorSpan = document.createElement("span");
    errorSpan.className = "form-error";
    errorSpan.textContent = errorMessage;
    parentElement.appendChild(errorSpan);
}
 
function _addNextError(errorsField, errorMessage) {
    const errorElement = errorsField[0];
    const oldMsg = errorElement.textContent;
    _appendErrorMsg(errorElement, "<br/>" + errorMessage);
}
 
function clearError(element) {
    _markInputAsValid(element);
    _removeAllErrorMessages(element);
}
 
function _removeAllErrorMessages(element) {
    var parent = element.parentNode;
    var errorMsgs = parent.getElementsByClassName("form-error");
    for (var i = 0; i < errorMsgs.length; i++) {
        parent.removeChild(errorMsgs[i]);
    }
}

Zdecydowanie ciekawszy jest sposób weryfikacji czy w bazie danych nie istnieje już pomieszczenie o wprowadzanej nazwie, który opiera się o wysłanie zapytania asynchronicznego (AJAX) do serwera. Zrealizowałem to z wykorzystaniem biblioteki jQuery oraz funkcji get(), która przyjmuje następujące parametry:

  • „roomExists” – ścieżka do metody kontrolera obsługującego zapytanie
  • data – dane przekazane do serwera
  • _handleAjaxRoomExistsResponse – uchwyt do funkcji wykonywanej po odebraniu od serwera informacji zwrotnej
function _ajaxCheckRoomExists(name) {
    const data = {
        name: name
    };
    $.get("roomExists", data, _handleAjaxRoomExistsResponse);
}

Wspomniana metoda roomExist() kontrolera RoomControler wykorzystuje warstwę serwisu RoomServiceImpl, gdzie zawarta jest implementacja sprawdzenia wykorzystująca konwersję listy na strumień i inne cwane elementy z Java 8. Zarówno wartość otrzymaną z widoku, jak i tą z bazy danych zamieniam na ciąg złożony z małych liter (toLowerCase).

@Override
    public boolean roomExists(String roomName) {
        List<String> roomNames = getAllRooms().stream().
                map((room) -> room.name).
                map(String::toLowerCase).
                collect(Collectors.toList());
        return roomNames.contains(roomName.toLowerCase());
    }

Na koniec została interpretacja informacji uzyskanej z serwera i ewentualne wyświetlenie komunikatu błędu.

function handleAjaxRoomExistsResponse(response) {
    if (response !== "") {
        const field = document.getElementById("name");
        if (response === "true") {
            printError(field, "Pomieszczenie o podanej nazwie już istnieje.");
        }
    }
}

Aby było bardziej kolorowo, na koniec wrzucam kilka screenów z działania walidatora formularza. Moje plany na najbliższy czas dotyczące Sterownika Domowego związane są z dodaniem funkcjonalności pozycjonowania sensorów na rzucie mieszkania. Serdecznie zapraszam gdzieś w trakcie weekendu majowego! 🙂

Nieprawidłowa wartość liczbowa oraz za krótka nazwa pomieszczenia
Nieprawidłowa wartość liczbowa oraz za krótka nazwa pomieszczenia
Pomieszczenie o podanej nazwie już istnieje
Pomieszczenie o podanej nazwie już istnieje
Za długa nazwa pomieszczenia
Za długa nazwa pomieszczenia

Literatura obowiązkowa

Common template use cases
jQuery.get()

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *