{#========================================== Docs : "Forms" ==========================================#}

Forms

This section is about HTML Forms, as used on traditional websites. If you use a SPA client-side, you in general don't use such POSTed forms, you rather use javascript to send and receive Json objects. Both approaches are supported out of the box by Spincast but this specific section is about traditional HTML forms and their validation!

We're going to learn :

A form always has a backing model to represent its data. This form model is sometimes called "form backing object", "form backing bean" or "command object". It's the object used to transfer the values of a form from the server to the client (to populate the form's fields) and vice versa.

On the server-side, this form model is represented using the Form class. A Form object is simply a JsonObject with extra validation features! You can manipulate a Form object exactly as a JsonObject and even cast it as one.

The validation pattern

The validation pattern shows how you create a form to be displayed, validate the form when it is submitted, and redisplay it again, with validation messages, if it is invalid...

First, let's start with the GET handler, which is the one called to display a form for the first time :


// GET handler
public void myHandlerGet(AppRequestContext context) {

    JsonObject form = context.response().getModel().getJsonObject("userForm");
    if (form == null) {
        form = context.json().create();
        context.response().getModel().put("userForm", form);
        
        User user = getUser(...);
        form.put("name", user.getName());
    }
    
    context.response().sendTemplateHtml("/templates/userEdit.html");
}

Explanation :

When the form is submitted, we retrieve its data inside a POST handler:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form form = context.request().getForm("userForm");
    context.response().addForm(form);
    
    validateForm(form);
    
    if (!form.isValid()) {
        myHandlerGet(context);
        return;
    } else {

        processForm(form);
        
        context.response().redirect("/success",
                                    FlashMessageLevel.SUCCESS,
                                    "The user has been processed!");
    }
}

Explanation :

The important part to understand is how the GET handler first checks in the response model to see if the form already exists in it... Indeed, this handler may be called by the POST handler if a posted form is invalid... When it's the case, you do not want to populate the form with some default/initial values, you want to keep the submitted values!

Displaying the Form

By using a dynamic JsonObject/Form object as the form model, a benefit is that you don't have to create in advance all the elements required to match the fields of the HTML form. Simply by using a valid JsonPath as the "name" attribute of a field, the element will automatically be created on the form model.

As an example, let's again use a form dedicated to editing a user. This form will display two fields : one for a username and one for an email. Our initial form model doesn't have to specify those two elements when it is first created :


// GET handler
public void myHandlerGet(AppRequestContext context) {

    JsonObject userForm = context.response().getModel().getJsonObject("userForm");
    if (userForm == null) {
    
        // Empty model! 
        // No username and no email elements are specified.
        userForm = context.json().create();
        
        context.response().getModel().put("userForm", userForm);
    }
    
    context.response().sendTemplateHtml("/templates/userEdit.html");
}

Here's what the HTML for that form may look like (we are using the syntax for the default Templating Engine, Pebble):

{% verbatim %}

<form method="post">
    <div class="form-group">
        <input type="text" 
               class="form-control" 
               name="userForm.username"
               value="{{userForm.username | default('')}}" />
    </div>
    <div class="form-group">
        <input type="text" 
               class="form-control" 
               name="userForm.email"
               value="{{userForm.email | default('')}}" />
    </div>
    <input type="submit" />
</form>

{% endverbatim %} Notice that even if the form model doesn't contain any "username" or "email" elements, we still bind them to the HTML elements using their JsonPaths [6] and here [12]. This is possible in part because we use the default('') filter : this filter tells Pebble to use an empty string if the element doesn't exist.

The "name" attributes of the HTML elements are very important : they represent the JsonPaths that Spincast is going to use to dynamically create the Form object, when the page is submitted.

So let's say this form is submitted. You would then access the values of the fields like so, in your POST handler:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getForm("userForm");
    context.response().addForm(userForm);
    
    // The "username" and "email" elements have been
    // automatically created to represent the submitted
    // fields.
    String username = userForm.getString("username");
    String email = userForm.getString("email");  
    
    //...
}

As you can see, Spincast uses the "name" attribute of an HTML element as a JsonPath to dynamically create an associated model element. This gives you a lot of flexibility client-side since you can dynamically generate new fields or even entire forms, using javascript.

Text based fields

Text based fields, such as text, password, email and textarea are very easy to manipulate :

Quick example :
{% verbatim %}

<input type="text" 
       name="userForm.email"
       value="{{userForm.email | default('')}}" />

{% endverbatim %}

Text based field groups

Sometimes we want multiple text fields to be grouped together. For example, let's say we want various "tags" to be associated with an "articleForm" object. Each of those "tags" will have its own dedicated field on the form, but we want all the "tags" to be available as a single array when they are submitted. To achieve that :

What we are doing, again, is to use the JsonPath to target each element! For example :
{% verbatim %}

<form method="post">
    <input type="text" class="form-control" name="articleForm.tags[0]"
           value="{{articleForm.tags[0] | default('')}}" />
    
    <input type="text" class="form-control" name="articleForm.tags[1]"
           value="{{articleForm.tags[1] | default('')}}">
    
    <input type="text" class="form-control" name="articleForm.tags[2]"
           value="{{articleForm.tags[2] | default('')}}">
    <input type="submit" />
</form>

{% endverbatim %}

When this form is submitted, you have access to the three "tags" as a single JsonArray :

public void manageArticle(AppRequestContext context) {

    Form form = context.request().getForm("articleForm");
    context.response().addForm(form);

    // Get all the tags of the article, as an array
    JsonArray tags = form.getJsonArray("tags");
    
    // You could also access one of the tag directly, using
    // its full JsonPath
    String thirdTag = form.getString("tags[2]");
    
    //...
}

Select fields

The select fields come in two flavors : single value or multiple values. To use them :

Here's an example for a single value select field :
{% verbatim %}


<select name="userForm.favDrink" class="form-control">
    <option value="tea" {{userForm.favDrink | selected("tea")}}>Tea</option>
    <option value="coffee" {{userForm.favDrink | selected("coffee")}}>Coffee</option>
    <option value="beer" {{userForm.favDrink | selected("beer")}}>WBeer</option>
</select>

{% endverbatim %}

In this example, the values of the option elements are hardcoded, they were known in advance : "tea", "coffee" and "beer". Here's a version where the option elements are dynamically generated :

{% verbatim %}

<select name="userForm.favDrink" class="form-control">
    {% for drink in allDrinks %}
        <option value="{{drink.id}}" {{userForm.favDrink | selected(drink.id)}}>{{drink.name}}</option>
    {% endfor %}
</select>

{% endverbatim %}

In this example, the selected(...) filter compares the current favorite drink of the user ("userForm.favDrink") to the value of every option element and outputs the "selected" attribute if there is a match.

Displaying a multiple values select field is similar, but :

For example :
{% verbatim %}


<select multiple name="userForm.favDrinks[]" class="form-control">
    <option value="tea" {{userForm.favDrinks | selected("tea")}}>Tea</option>
    <option value="coffee" {{userForm.favDrinks | selected("coffee")}}>Coffee</option>
    <option value="beer" {{userForm.favDrinks | selected("beer")}}>WBeer</option>
</select>

{% endverbatim %}

Radio Buttons

To display a radio buttons group :

Let's first have a look at an example where the values of the radio buttons are hardcoded :

{% verbatim %}

<div class="form-group">
    <label for="drinkTea">
        <input type="radio" 
               id="drinkTea" 
               name="userForm.favDrink"
               {{userForm.favDrink | checked("tea")}}
               value="tea"/> Tea</label>
    
    <label for="drinkCoffee">
        <input type="radio" 
               id="drinkCoffee" 
               name="userForm.favDrink"
               {{userForm.favDrink | checked("coffee")}}
               value="coffee"> Coffee</label>
    
    <label for="drinkBeer">
        <input type="radio" 
               id="drinkBeer" 
               name="userForm.favDrink"
               {{userForm.favDrink | checked("beer")}}
               value="beer"> Beer</label>
</div>

{% endverbatim %}

Let's focus on the first radio button of that group. First, its "name" attribute :

{% verbatim %}

<label for="drinkTea">
    <input type="radio" 
           id="drinkTea" 
           name="userForm.favDrink"
           {{userForm.favDrink | checked("tea")}}
           value="tea"/> Tea</label>

{% endverbatim %}
As we already said, the "name" attribute of a field is very important. Spincast uses it to create the element on the form model, when the form is submitted. This "name" will become the JsonPath of the element on the form model. In our example, the form model would contain a "favDrink" element.

Let's now have a look at the checked(...) filter :

{% verbatim %}

<label for="drinkTea">
    <input type="radio" 
           id="drinkTea" 
           name="userForm.favDrink"
           {{userForm.favDrink | checked("tea")}}
           value="tea"/> Tea</label>

{% endverbatim %}

We don't know in advance if a radio button should be checked or not, this depends on the current value of the "userForm.favDrink" element. That's why we use "checked(...)". This filter will compare the current value of the "userForm.favDrink" model element to the value of the radio button ("tea" in our example). If there is a match, a "checked" attribute is printed!

Note that the parameter of the "checked(...)" filter can be an array. In that case, the filter will output "checked" if the current value matches any of the elements. For example :

{% verbatim %}

<label for="drinkTea">
    <input type="radio" 
           id="drinkTea" 
           name="userForm.favDrink"
           {{userForm.favDrink | checked(["tea", "ice tea", chai"])}}
           value="tea"/> Tea</label>

{% endverbatim %}

This feature is mainly useful when the radio buttons are dynamically generated.

Speaking of dynamically generated radio buttons, let's see an example of those! The creation of the response model, in your Route Handler, may look like this :

public void myRouteHandler(AppRequestContext context) {

    //==========================================
    // Creates the available drink options and add them
    // to the reponse model directly. 
    // There is no need to add them to the form 
    // itself (but you can!).
    //==========================================
    JsonArray allDrinks = context.json().createArray();
    context.response().getModel().put("allDrinks", allDrinks);

    JsonObject drink = context.json().create();
    drink.put("id", 1);
    drink.put("name", "Tea");
    allDrinks.add(drink);

    drink = context.json().create();
    drink.put("id", 2);
    drink.put("name", "Coffee");
    allDrinks.add(drink);

    drink = context.json().create();
    drink.put("id", 3);
    drink.put("name", "Beer");
    allDrinks.add(drink);

    //==========================================
    // Creates the form, if it doesn't already exist.
    //==========================================
    JsonObject form = context.response().getModel().getJsonObject("userForm");
    if (userForm == null) {
        form = context.json().create();
        context.response().getModel().put("userForm", form);
        
        // Specifies the initial favorite drink of the user.
        User user = getUser(...);
        JsonObject user = context.json().create();
        form.put("favDrink", user.getFavDrink());
    }

    context.response().sendTemplateHtml("/templates/userTemplate.html");
}

With this response model in place, we can dynamically generate the radio buttons group and check the current favorite one of the user :

{% verbatim %}

<div class="form-group">
    {% for drink in allDrinks %}
        <label for="drink_{{drink.id}}">
            <input type="radio" 
                   id="drink_{{drink.id}}" 
                   name="userForm.favDrink"
                   {{userForm.favDrink | checked(drink.id)}}
                   value="{{drink.id}}"/> {{drink.name}}</label> 
    {% endfor %}
</div>

{% endverbatim %}

Checkboxes

Checkboxes are often used in one of those two situations :

First, let's look at a single checkbox field :

{% verbatim %}

<label for="tosAccepted">
    <input type="checkbox" 
           id="tosAccepted" 
           name="myForm.tosAccepted"
           {{myForm.tosAccepted | checked(true)}}
           value="true" /> I agree to the Terms of Service</label>

{% endverbatim %}

Note that, even if the value of the checkbox is "true" as a string, you can use true as a boolean as the filter parameter. This is possible because the checked(...) filter (and the selected(...) filter) compares elements using equivalence, not equality. So "true" would match true and "123.00" would match 123.

When this field is submitted, you would be able to access the boolean value associated with it using :

public void myRouteHandler(AppRequestContext context) {

    Form form = context.request().getForm("myForm");
    context.response().addForm(form);

    boolean tosAccepted = form.getBoolean("tosAccepted");
    
    //...
}

Now, let's see an example of a group of checkboxes :

{% verbatim %}

<div class="form-group">
    <label for="drinkTea">
        <input type="checkbox" 
               id="drinkTea" 
               name="userForm.favDrinks[0]"
               {{userForm.favDrinks[0] | checked("tea")}}
               value="tea"/> Tea</label>
    
    <label for="drinkCoffee">
        <input type="checkbox" 
               id="drinkCoffee" 
               name="userForm.favDrinks[1]"
               {{userForm.favDrinks[1] | checked("coffee")}}
               value="coffee"> Coffee</label>
    
    <label for="drinkBeer">
        <input type="checkbox" 
               id="drinkBeer" 
               name="userForm.favDrinks[2]"
               {{userForm.favDrinks[2] | checked("beer")}}
               value="beer"> Beer</label>
</div>

{% endverbatim %}
Here, the checkboxes are grouped together since they share the same "name" attribute, name that is suffixed with the position of the element in the group. Again, their "name" is the JsonPath of their associated element on the form model.

With this in place, we can access all the checked "favorite drinks" as a single array, in our handler.

In the following example, we will retrieve such array without using a proper Form object, but by using request.getFormData() directly, to show this is also an option! But note that if you do it that way, you won't have access to the built-in validation features a Form provide... You are manipulating the form data as a raw JsonObject! :

public void myRouteHandler(AppRequestContext context) {

    JsonObject model = context.request().getFormData();
    
    // The checked favorite drinks, as an array!
    JsonArray favDrinks = model.getJsonArray("userForm.favDrinks");
    
    //...
}

Finally, note that the positions used in the "name" HTML attributes are kept when we receive the array! This means that if the user only checked "beer" for example (the last option), the array received in our handler will be [null, null, "beer"], not ["beer"]! This is a good thing because the JsonPath we use for an element always stays valid ("userForm.favDrinks[2]" here).

File upload

Uploading a file is very easy using Spincast. The main difference between a "file" element and the other types of elements is that the uploaded file will not be available as a form data when submitted. You'll have to use a dedicated method to retrieve it.

The HTML part is very standard :

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" class="form-control" name="fileToUpload">
    <button type="submit">Submit</button>
</form>

To retrieve the uploaded file, you use one of the getUploadedFileXXX(...) methods on the request() add-on. For example :

public void myRouteHandler(AppRequestContext context) {

    File uploadedFile = context.request().getUploadedFileFirst("fileToUpload");
}

Note that even if the uploaded file is not part of the form data, you can still perform validation, as we'll see in the next section.

Form validation introduction

Validating a submitted form involves three main steps :

Retrieving the submitted form

When an HTML form is submitted, Spincast treats the "name" attributes of the fields as JsonPaths in order to create a Form (a plain JsonObject with extra validation features) representing the form model. In other words, Spincast converts the submitted data to a Form instance so you can easily validate and manipulate it.

You access that Form representing the submitted data by using the getForm(...) method of the request() add-on:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getForm("userForm");
    context.response().addForm(userForm);
    
    //...
}

If you have more than one form on the same HTML page, you simply give them different names, and check which one has been submitted, by looking for the presence of a field which should always be submitted:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getForm("userForm");
    if(userForm.getString("userFormBtn") != null) {
        context.response().addForm(userForm);
        processUserForm(context, userForm);
        return;
    }
    
    Form bookForm = context.request().getForm("bookForm");
    if(bookForm.getString("bookFormBtn") != null) {
        context.response().addForm(bookForm);
        processBookForm(context, bookForm);
        return;
    }
    
    //...
}

Performing validations

Once you have the Form representing the submitted data, you can start validating it. Forms implement the ValidationSet interface and allow you to store validation results directly in them.

Here's an example where we validate that a submitted "email" is valid, and add an error to the form if it's not:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form form = context.request().getForm("userForm");
    context.response().addForm(form);
    
    String email = form.getString("email");
    
    if (!form.validators().isEmailValid(email)) {
        form.addError("email",
                      "email_invalid",
                      "The email is invalid");
    }
    
    //... 
}

Explanation :

To validate an element of the form, you can use any method you need. Some validators, such as isEmailValid(...) are provided by the form.validators() method. But, most of the time, you're going to use custom code for your validations. For example:


Form form = context.request().getForm("userForm");

String name = form.getString("name");
if(StringUtils.isBlank(name)) {
    form.addError("name",
                  "name_empty",
                  "The name is required!");
}

Finally, note that there are "success" and "warning" validation messages too, in addition to the "error" ones.

Displaying Validation Messages

When you add the form to the response model, using context.response().addForm(form), you are in fact adding two elements :

By default, the Validation element containing the messages of a validated form is called "validation". You can choose a different name for this element when adding the form to the response model. For example:


// POST handler
public void myHandlerPost(AppRequestContext context) {

    Form userForm = context.request().getForm("userForm");
    
    // Uses "userFormValidation" as the name for the
    // validation element.
    context.response().addForm(userForm, "userFormValidation");
    
    // validation...
}

When it reaches the templating engine, the Validation element associated with a form will contain:

Here's a bigger chunk of the model the templating engine will have access to to redisplay an invalid form :

{
    // The form itself
    "userForm" : {
        "name" : ""
        "email" : "abc"
        "books": [
            {
                "title" : "Dune",
                "author": "Frank Herbert"
            },
            {
                "title" : "The Hitchhiker's Guide to the Galaxy",
                "author" : ""
            }
        ]
    },
    
    // The "validation" element
    "validation" : {
        "userForm._" : {
            "hasErrors" : true,
            "hasWarnings" : false,
            "isValid" : false,
            "hasSuccesses" : false
        },
        "userForm.name" : {
            "level" : "ERROR",
            "code" : "name_empty",
            "text" : "The name is required!" 
        },
        "userForm.email" : {
            "level" : "ERROR",
            "code" : "email_invalid",
            "text" : "The email is invalid" 
        },
        "userForm.books[1].author" : {
            "level" : "ERROR",
            "code" : "author_empty",
            "text" : "The author is required!" 
        }
    }
    
    // ...
}

The important things to notice are :

It is easy to find the validation messages associated with a specific element since the JsonPath of that element will be the key to use to retrieve them. For example: {% verbatim %}

<div class="form-group">
    <input type="text" 
           class="form-control" 
           name="userForm.email"
           value="{{userForm.email | default('')}}" />
    {{validation['userForm.email'] | validationMessages()}}
</div>

{% endverbatim %}

Validation Filters

Spincast provides utilities to display the validation messages with the default Templating Engine, Pebble. But, as we saw, the template model is a simple Map<String, Object> so no magic is involved and any other Templating Engine can be used.

Have a look at the Forms + Validation demos section to see the following validation filters in action!

Forms are generic

You may have noticed that we are not using a dedicated class to represent the form model (a "UserForm" class, for example) : we use plain JsonObject objects (which Form object are based on).

Here's why:

In case you still want to use a dedicated class to back your forms, you are free to do so, and here's a quick example.... First, you would create a dedicated class for the model :

public class UserCreationForm {
    
    private String username;
    private String email;
    private String captcha;
    
    //... Getters
    //... Setters
}

You would then create a model instance like so :

public void displayUserForm(AppRequestContext context) {

    // A typed form model
    UserCreationForm userForm = new UserCreationForm();
    
    // ... that is quickly converted to a 
    // JsonObject anyway when added to the response model!
    context.response().getModel().put("userForm", userForm);
    
    sendMyTemplate();
}

When the form is submitted, you would then convert the form, which is a JsonObject under the hood, to an instance of your UserCreationForm class :

public void manageUserForm(AppRequestContext context) {

    // Back to a typed version of the form model!
    UserCreationForm userForm = context.request()
                                       .getForm("userForm")
                                       .convert(UserCreationForm.class);
                                       
   // ...
}