All Downloads are FREE. Search and download functionalities are using the official Maven repository.

templates.docs.forms.html Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
{#==========================================
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 :

  • How to populate a form and bind its fields to an underlying Form object.
  • How to validate a form that has been submitted.
  • How to redisplay a validated form with resulting validation messages.

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) {

    Form form = context.request().getForm("userForm");
    if (form == null) {
        form = context.request().getFormOrCreate("userForm");
        context.response().addForm(form);
        
        User user = getUser(...);
        form.set("name", user.getName());
    }
    
    context.response().sendTemplateHtml("/templates/userEdit.html");
}

Explanation :

  • 5 : We check if the form already exist in the response model. This may be the case if this GET handler is called from an associated POST handler, because some validation failed.
  • 7 : If the form doesn't exist yet, we create an new one.
  • 8 : We add the form to the response model, so it is available to the templating engine.
  • 10-11 : We populate the form with the initial values, if required.
  • 14 : We send the response by evaluating a template which will display the form.

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


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

    Form form = context.request().getFormOrCreate("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 :

  • 5 : We retrieve the posted form from the request.
  • 6 : We immediately add the form to the response model. This will make the form available to the templating engine but will also provide a "validation" element containing any validation messages to display.
  • 8 : We validate the form and add error, warning or success validation messages to it.
  • 10 : Once the validation is done, we check if the form is valid.
  • 11-12 : if the form contains errors, we simply call the GET handler so the form is displayed again, with the validation messages we added to it.
  • 15 : if the form is valid, we process it. This may involve calling services, editing entities, etc.
  • 17-19 : we redirect the page with a Flash message to indicate that the form was processed successfully!

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) {

    Form userForm = context.request().getForm("userForm");
    if (userForm == null) {
    
        // Empty form! 
        // No username and no email elements are specified.
        userForm = context.request().getFormOrCreate("userForm");
        
        context.response().getModel().set("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().getFormOrCreate("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 :

  • You use the JsonPath you want for their associated model element as their "name" attribute.
  • You use that same JsonPath to target the current value of the element on the model, and you output it in the "value" attribute.
  • You use the default('') filter to make sure not exception is thrown if the model element doesn't exist yet.
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 :

  • We use the same "name" attribute for every field, but we suffix this name with the position of the tag inside the final array. For example : "articleForm.tags[0]" or "articleForm.tags[1]"
  • We also use that same "[X]" suffixed name to get and display the "value" attributes.
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().getFormOrCreate("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 :

  • You specify the JsonPath of the associated element in the "name" attribute of the select HTML element.
  • For every option elements of the field you use the selected(...) filter to check if the option should be selected or not.
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.

To select a default option, you can specify null as one of its accepted values:

{% verbatim %}


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

{% endverbatim %}

Displaying a multiple values select field is similar, but :

  • You use "[]" after the "name" attribute of the select field. This tells Spincast that an array of values is expected when the form is submitted.
  • The left side of a selected(...) filter will be a list of values (since more than one option may have been selected). The filter will output the "seleted" attribute as long as the value of an option matches any of the values from the list.
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 :

  • You use the JsonPath of the associated model element as the "name" attributes.
  • You output the "value" of each radio button. Those values can be hardcoded, or they can be dynamically generated inside a loop (we'll see an example of both).
  • You use the checked(...) filter provided by Spincast determine if a radio button should be checked or not.

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.

If you need a default radio button to be checked, without providing this information in the initial form model, you simply have to add "null" as an accepted element for the checkbox:

{% verbatim %}

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

{% endverbatim %}

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().set("allDrinks", allDrinks);

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

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

    drink = context.json().create();
    drink.set("id", 3);
    drink.set("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().set("userForm", form);
        
        // Specifies the initial favorite drink of the user.
        User user = getUser(...);
        JsonObject user = context.json().create();
        form.set("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 :

  • To allow the user to select a single boolean value. For example :
    {% verbatim %}

    [ ] Do you want to subscribe to our newsletter?

    {% endverbatim %}
  • To allow the user to select multiple values for a single preference. For example :
    {% verbatim %}

    Which drinks do you like?
    [ ] Tea
    [ ] Coffee
    [ ] Beer

    {% endverbatim %}

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().getFormOrCreate("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 data.
  • Validating the form, and adding resulting validation messages to it.
  • Redisplaying the form with the validation messages resulting from the validation. If the form is valid, you may instead want to redirect the user to a confirmation page where a success Flash Message will be displayed.

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 getFormOrCreate(...) method of the request() add-on:


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

    Form userForm = context.request().getFormOrCreate("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().getFormOrCreate("userForm");
    if(userForm.getString("userFormBtn") != null) {
        context.response().addForm(userForm);
        processUserForm(context, userForm);
        return;
    }
    
    Form bookForm = context.request().getFormOrCreate("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().getFormOrCreate("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 :

  • 5 : We retrieve the submitted form from the request.
  • 6 : We immediately add the form back to the response model.
  • 8 : We get the "email" from the form.
  • 10 : We validate the email using a validator provided on the Form object itself!
  • 11-13 : If the email is invalid, we add an error validation message to the form.
  • 11 : The first parameter, "email" is the JsonPath of the validated element.
  • 12 : The second parameter, "email_invalid" is a code representing the error. This can be used client-side to know what exact error occured.
  • 13 : The third parameter is the message to display to the user.

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().getFormOrCreate("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 :

  • The form itself, using its name as the key in the response model.
  • A Validation element, containing the validation messages added on the form.

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().getFormOrCreate("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:

  • An object for every validation message added to the form, with the JsonPath of the validated element as the key and three fields, "level", "code" and "text":

    "userForm.name" : {
        "level" : "ERROR",
        "code" : "name_empty",
        "text" : "The name is required!" 
    }

  • A special "_" element that summarizes all the validations performed on the form:

    "userForm._" : {
        "hasErrors" : true,
        "hasWarnings" : false,
        "isValid" : false,
        "hasSuccesses" : false
    }

    This "_" element can be used in a template to display something if the form contains errors, for example.

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 :

  • In the form object, each element is positioned at its JsonPath. For example, the author of the second book is located at userForm.books[1].author.
  • In the "validation" element, each keys is the string representation of the JsonPath of the validated element! For example : validation['userForm.books[1].author'].
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 %}

Note that when you add a validation message, you can specify some options on how to render the message. You do this by passing a ValidationHtmlEscapeType parameter:


form.addError("email",
              "email_invalid",
              "Invalid email: <em>" + email + "</em>",
              ValidationHtmlEscapeType.NO_ESCAPE);

The possible values of ValidationHtmlEscapeType are:

  • ESCAPE: escapes the message to display. This is the default value.
  • NO_ESCAPE: does not escape the message to display. Any HTML will be rendered.
  • PRE: displays the message inside "<pre></pre>" tags.

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!

  • ValidationMessages | validationMessages()

    This filter uses a HTML template fragment to output the Validation Messages associated with an element.

    Here's an example :

    {% verbatim %}

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

    {% endverbatim %}

    The path to the template fragment is configurable using the SpincastPebbleTemplatingEngineConfig#getValidationMessagesTemplatePath() method. The default path is "/spincast/spincast-plugins-pebble/spincastPebbleExtension/validationMessagesTemplate.html" which points to a template fragment provided by Spincast.

  • ValidationMessages | validationGroupMessages()

    This filter is similar to validationMessages() but uses a different template fragment. Its purpose is to output the Validation Messages of a group of elements.

    Here's an example :

    {% verbatim %}

    <div id="tagsGroup" class="form-group {{validation['demoForm.tags'] | validationClass()}}">
    
        <div class="col-sm-4">
            <label class="control-label">Tags *</label>
            {{validation['demoForm.tags'] | validationGroupMessages()}}
        </div>
        
        <div class="col-sm-8">
            <input type="text" name="demoForm.tags[0]" 
                   class="form-control {{validation['demoForm.tags[0]'] | validationClass()}}"
                   value="{{demoForm.tags[0] | default('')}}" />
            {{validation['demoForm.tags[0]'] | validationMessages()}}
            
            <input type="text" name="demoForm.tags[1]" 
                   class="form-control {{validation['demoForm.tags[1]'] | validationClass()}}"
                   value="{{demoForm.tags[1] | default('')}}">
            {{validation['demoForm.tags[1]'] | validationMessages()}}
        </div>
    </div>

    {% endverbatim %}

    In this example, we ask the user to enter two tags. If one is invalid, we may want to display a "This tag is invalid" message below the invalid field, but we may also want to display a global "At least one tag is invalid" below the group title, "Tags *".

    This is exactly what the validationGroupMessages() filter is for. As you may notice, "demoForm.tags" is, in fact, the JsonPath to the tags array itself.

    The path to the template fragment used by this filter is configurable using the SpincastPebbleTemplatingEngineConfig#getValidationGroupMessagesTemplatePath() method. The default path is "/spincast/spincast-plugins-pebble/spincastPebbleExtension/validationGroupMessagesTemplate.html" which is a template fragment provided by Spincast.

  • ValidationMessages | validationClass()

    The validationClass(...) filter checks if there are Validation Messages and, if so, it outputs a class name.

    The default class names are :

    • "has-error" : when there is at least one Error Validation Message.
    • "has-warning" : when there is at least one Warning Validation Message.
    • "has-success" : when there is at least one Success Validation Message.
    • "has-no-message" : when there are no Validation Messages at all.

    For example :

    {% verbatim %}

    
    <div id="tagsGroup" class="form-group {{validation['demoForm.tags'] | validationClass()}}">
    
        <div class="col-sm-4">
            <label class="control-label">Tags *</label>
            {{validation['demoForm.tags'] | validationGroupMessages()}}
        </div>
        
        <div class="col-sm-8">
            <input type="text" name="demoForm.tags[0]" 
                   class="form-control {{validation['demoForm.tags[0]'] | validationClass()}}"
                   value="{{demoForm.tags[0] | default('')}}" />
            {{validation['demoForm.tags[0]'] | validationMessages()}}
            
            <input type="text" name="demoForm.tags[1]" 
                   class="form-control {{validation['demoForm.tags[1]'] | validationClass()}}"
                   value="{{demoForm.tags[1] | default('')}}">
            {{validation['demoForm.tags[1]'] | validationMessages()}}
        </div>
    </div>

    {% endverbatim %}

    The validationClass() filter can be used both on single fields and on a group of fields. It is up to you to tweak the CSS of your application so the generated class are used properly.
  • ValidationMessages | validationFresh()
    ValidationMessages | validationSubmitted()

    Those two filters are used to determine if a form is displayed for the first time, or if it has been submitted and is currently redisplayed with potential Validation Messages. When one of those filters returns true, the other necessarily returns false.

    Most of the time, you are going to use the special "_" element, representing the validation as a whole, as the element passed to those filters. For example :

    {% verbatim %}

    
     {% if validation['myForm._'] | validationFresh() %}
         <div>This form is displayed for the first time!</div> 
     {% endif %}
    

    {% endverbatim %}
    and :
    {% verbatim %}

    
     {% if validation['myForm._'] | validationSubmitted() %}
         <div>This form has been validated!</div> 
     {% endif %}
    

    {% endverbatim %}

  • ValidationMessages | validationHasErrors()
    ValidationMessages | validationHasWarnings()
    ValidationMessages | validationHasSuccesses()
    ValidationMessages | validationIsValid()

    Those four filters check if there are Validation Messages of a particular level and return true or false.

    For example, you could use those filters to determine if you have to display an element or not, depending of the result of a validation.

    • validationHasErrors() : returns true if there is at least one Error Validation Message.
    • validationHasWarnings() : returns true if there is at least one Warning Validation Message.
    • validationHasSuccesses() :returns true if there is at least one Success Validation Message.
    • validationIsValid() : returnstrue if there is no Validation Message at all.
    For example :
    {% verbatim %}

    
     {% if validation['myForm.email'] | validationHasErrors() %}
         <div>There are errors associated with the email field.</div> 
     {% endif %}
    

    {% endverbatim %}


    An important thing to know is that you can also use those filters to see if the form itself, as a whole, contains Validation Messages at a specific level. To do that, you use the special "_" element representing the form itself. For example :

    {% verbatim %}

    
     {% if validation['myForm._'] | validationHasErrors() %}
         <div>The form contains errors!</div> 
     {% endif %}
    

    {% endverbatim %}


    It is also important to know that those filters will often be used in association with the validationSubmitted(...) filter. The reason is that when a form is displayed for the first time, it doesn't contain any Validation Messages, so the validationIsValid(...) filter will return true.

    But if you want to know if the form is valid after having been validated, then you need to use the validationSubmitted(...) filter too :

    {% verbatim %}

    
     {% if validation['myForm._'] | validationSubmitted() and validation['myForm.email'] | validationIsValid() %}
         <div>The email has been validated and is ok!</div> 
     {% endif %}
    

    {% endverbatim %}

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:

  • You may be thinking about reusing an existing Entity class for the model of your form. For example, you may want to use an existing "User" Entity class for the model of a form dedicated to the creation of a new user. This seems logical at first since a lot of fields on the form would have a matching field on that User Entity class... But, in practice, it's very rare that an existing Entity class contains all the fields required to model the form.

    Let's say our form has a "name" field and a "email" field and uses those to create a new user : those fields would probably indeed have matching fields on a "User" Entity. But what about a captcha? Or an option to "subscribe to our newsletter"? Those two fields on the form have nothing to do with a "user" and there won't be matching fields for them on a "User" Entity class... So, what you do then? You have to create a new class that contains all the required fields. For that, you may be tempted to extend the "User" Entity and simply add the missing fields, but our opinion is that this is hackish at best and clearly not a good practice.

  • You may also feel that using a dedicated class for such form model is more robust, since that model is then typed. We understand this feeling since we're huge fans of statically typed code! But, for this particular component, for the model of a form, our opinion is that a dedicated class is not very beneficial...

    As soon as your form model leaves your controller, it is pretty much converted to a simple and dumb Map<String, Object>, so the Templating Engine can use it easily. At that moment, your typed form model is no more! And, at the end of the day, the model becomes plain HTML fields : nothing is typed there either.

    In other words, if you use a dedicated class for your form model, this model is going to be typed for a very short period, and we feel this doesn't worth the effort. That said, when your form has been validated and everything is fine, then you may want to convert the JsonObject/Form object to a dedicated Entity class and pass it to services, repositories, etc.

  • Last but not least : using an existing Entity class as a form model can lead to security vulnerabilities (PDF) if you are not careful.

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().set("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()
                                       .getFormOrCreate("userForm")
                                       .convert(UserCreationForm.class);
                                       
   // ...
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy