https://visualhunt.com/photo/192682/

Obsługa formularzy w Play Framework

Końcówka tego tygodnia jest wybitnie świąteczna, w związku z czym domowe obowiązki skutecznie pochłonęły czas na blogowanie i pchanie projektu do przodu. Jednak byłem na to przygotowany! Miałem w zanadrzu przygotowany kawałek kodu w projekcie Sterownika domowego. Etapem, na którym się obecnie znajduję jest przygotowanie całej otoczki, niezbędnej do zapewnienia podstawowej funkcjonalności, czyli obsługi sensorów. Mam tu na myśli dodanie możliwości konfiguracji systemu, dodania sensorów, ustawienia ich parametrów itp. W jednym z poprzednich wpisów przygotowałem API umożliwiające operacje na bazie danych, które tym razem chciałbym wykorzystać. Operacje na formularzu przedstawię na podstawie modelu pomieszczenia, czyli klasie Room, jako tej najprostszej, nie posiadającej zależności do innych encji. Przygotowałem formularz umożliwiający edycję oraz dodawanie pomieszczenia do systemu oraz wykonałem zmiany w widoku wyświetlającym listę pomieszczeń (dodany link umożliwiający kasowanie pomieszczenia).

Formularz dodawania/edycji pokoju znajduje się w pliku addEditRoomForm.scala.html. Struktura szablonu na chwilę obecną wygląda całkiem prosto (dla uproszczenia celowo wyciąłem implementację funkcji @displayForm()). Z totalnych dla mnie nowości jest konstrukcja @helper.form(), która jest tzw. helperem (pomocnikiem?) upraszczającym tworzenie formularza.

@(roomForm: Form[Room])(option: String)(editedRoomId: Integer)(errors: List[String])
@import views.html.formElementTemplates._
@import java.lang.String; var roomFormHeader=""
 
@main("Home Controller") {
 
    @if(option.equals("edit")){
        @{roomFormHeader = "Edycja pomieszczenia"}
            @helper.form(action = routes.RoomController.editRoom(editedRoomId)) {
                @displayForm()
            }
    } else {
        @{roomFormHeader = "Dodaj nowe pomieszczenie"}
            @helper.form(action = routes.RoomController.addRoom()) {
                @displayForm()
            }
    }
 
}

Do widoku przekazywane są parametry z kontrolera: obiekt formularza (roomForm) parametryzowany klasą Room, opcja funkcyjna (option) określająca czy formularz będzie służył do edycji czy do dodawania nowego pomieszczenia (zastanawiam się jak można to zrobić w bardziej elegancki sposób), numer identyfikacyjny pomieszczenia (editedRoomId) w przypadku chęci wprowadzenia zmian oraz ewentualna lista błędów z walidatora (errors).

Testując stan przekazanego parametru option, wyświetlam odpowiedni nagłówek oraz ustawiam ścieżkę do metody kontrolera obsługującej zdarzenie submit. Cała magia tworzenia formularza siedzi wewnątrz funkcji @displayForm().

@displayForm() = {
<div class="row col-lg-offset-4">
    <h3>@roomFormHeader</h3>
</div>
<fieldset>
    <div class="row col-lg-offset-2">
        <!-- Name -->
        @inputFieldTemplate(roomForm("name"),
        label = "Nazwa pomieszczenia",
        placeholder = "Wprowadż nazwę pomieszczenia (wymagane)")
    </div>
 
    <div class="row col-lg-offset-2">
        <!-- Description -->
        @textareaTemplate(roomForm("description"),
        label = "Dodatkowy opis pomieszczenia",
        placeholder = "Wprowadż dodatkowy opis pomieszczenia (opcjonalne)")
    </div>
        @if(errors != null) {
            <div class="alert alert-danger row col-lg-offset-2 col-lg-8">
                <ul class="form-error-list">
                    @for(error <- errors) {
                      <li>@error</li>
                    }
                </ul>
            </div>
        }
        @if(flash.containsKey("success")) {
        <div class="alert alert-success row col-lg-offset-2 col-lg-8">
            @flash.get("success")Przejdź do <a href="@routes.RoomController.list()">listy pomieszczeń</a>
        </div>
        }
    <div class="row col-lg-offset-2">
        <div class="form-group">
            <div class="col-lg-offset-7 col-lg-1">
                <button type="submit" id="submit" class="btn btn-primary" value="Zapisz">
                    Zapisz <span class="glyphicon glyphicon-hand-right"></span> 
                </button>
            </div>
        </div>
    </div>
</fieldset>
}

Wewnątrz znacznika agregującego elementy formularza (<fieldset>) znajdują się dwa pola umożliwiające wprowadzenie nazwy pomieszczenia (name) oraz dodatkowego opisu (description).

W przypadku wystąpienia błędów walidacji (@if(errors != null)), wyświetlane są odpowiednie komunikaty informujące użytkownika o nieprawidłowościach. Sprawdzeniu podlegają wszystkie pola, zgodnie z ograniczeniami określonymi dla każdego z nich (oczywiście komunikaty są indywidualne dla każdego rodzaju błędu). Natomiast jeśli serwer nie wykryje żadnych nieprawidłowości, użytkownik jest informowany o powodzeniu operacji (@if(flash.containsKey(„success”))).

Słowa komentarza wymaga sposób dodania do formularza poszczególnych pól, które są zdefiniowane w szablonach (np. inputFieldTemplate.scala.html). Główna korzyść z tego sposobu to ujednolicony wygląd wszystkich pól w całej aplikacji. Na chwilę obecną, takie schematy wykorzystują standardowe znaczniki, np. <input>, ale planuję zmienić implementację na taką, która wykorzysta helpery.

Po stronie kontrolera z tym formularzem związane są trzy metody – wyświetlająca (GET) oraz dwie typu POST realizujące dodawanie oraz edycję pomieszczenia.

public Result addEditRoomShowForm(String option, int roomId) {
        roomForm = formFactory.form(Room.class);
        Room room;
        if ("edit".equals(option)) {
            room = roomService.getRoomById(roomId);
            roomForm = roomForm.fill(room);
        } else { // add new room
            room = new Room();
        }
        return ok(views.html.rooms.addEditRoomForm.render(roomForm, option, room.id, null));
    }

Metoda addEditRoomShowForm() realizuje logikę odpowiadającą za renderowanie widoku z formularzem. W liście argumentów metody render() znajdują się wszystkie parametry, o których pisałem. W przypadku gdy parametr żądania option przyjmuje wartość edit, pobierane jest z bazy danych pomieszczenie o identyfikatorze roomId, którego wartości atrybutów wypełniają formularz (roomForm.fill(room)). Ostatni z argumentów metody renderującej ma wartość null, gdyż na tą chwilę, lista błędów jest pusta. Ewentualne błędy pojawią się dopiero po wykonaniu akcji submit, czyli po wysłaniu formularza do serwera.

  public Result addRoom() {
        roomForm = roomForm.bindFromRequest();
        if (roomForm.hasErrors()) {
            List<String> errorMessages = prepareErrorMessagesList();
            Room roomFromForm = getInvalidRoomFromForm();
            return badRequest(views.html.rooms.addEditRoomForm.render(roomForm.fill(roomFromForm), "new", null, errorMessages));
        } else {
            Room room = roomForm.get();
            roomService.addRoom(room);
            flash("success", "Pomieszczenie zostało dodane poprawnie. ");
            return redirect("/rooms/addEditRoomShowForm?option=new");
        }
    }
 
private List<String> prepareErrorMessagesList() {
        List<String> errorMessages = new ArrayList<String>();
        Map<String, List<ValidationError>> errors = roomForm.errors();
        errors.forEach((k, v) -> {
            v.forEach((t) -> {
                errorMessages.add(t.message());
            });
        });
        return errorMessages;
    }

Metoda addRoom() to jedna z dwóch metod realizowanych w momencie wysłania formularza na serwer. Pierwszym realizowanym w niej krokiem jest powiązanie danych z żądania z obiektem typu Form parametryzowanym typem Room (roomForm = roomForm.bindFromRequest()). W dalszej kolejności następuje weryfikacja czy wystąpiły błędy walidacji (roomForm.hasErrors()) oraz ich obsługa. W przypadku nieprawidłowości, ponownie renderowany jest formularz, tym razem z uzupełnionymi wcześniej przez użytkownika polami, opatrzony odpowiednim komunikatem błędu. Metoda prepareErrorMessagesList() konwertuje mapę z błędami, na ArrayList sparametryzowaną typem String. Do poinformowania użytkownika o poprawnym dodaniu nowego pomieszczenia, wykorzystałem flash scope.

 public Result editRoom(int roomId) {
        roomForm = roomForm.bindFromRequest();
        if (roomForm.hasErrors()) {
            List<String> errorMessages = prepareErrorMessagesList();
            Room roomFromForm = getInvalidRoomFromForm();
            return badRequest(views.html.rooms.addEditRoomForm.render(roomForm.fill(roomFromForm), "edit", roomId, errorMessages));
        } else {
            Room room = roomForm.get();
            room.id = roomId;
            roomService.updateRoom(room);
            flash("success", "Pomieszczenie zostało zmienione poprawnie. ");
            return redirect("/rooms/addEditRoomShowForm?option=new");
        }
    }

Metoda editRoom() jest totalnie analogiczna do tej dodającej nowe pomieszczenie. Zasadniczą różnicą jest wywołanie roomService.updateRoom(room) zamiast addRoom(room). Poza tym w przypadku wystąpienia błędów walidacji, parametr option przyjmuje wartość edit, a do formularza przekazywany jest identyfikator edytowanego pomieszczenia roomId.

Nie był to skomplikowany przykład, ale od takich dobrze się zaczyna. W kolejnym kroku rozpocząłem pracę nad formularzem dodawania do systemu nowego sensora, co okazało się delikatnie bardziej skomplikowane, gdyż musiałem usunąć dwa błędy, które napotkałem. Ale to już temat, który pojawi się w kolejnym wpisie na moim blogu. Serdecznie zapraszam!

Literatura obowiązkowa

Handling form submission
Form template helpers

Dodaj komentarz

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