{#==========================================
Docs : "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
We're going to learn :
Forms
Json objects.
Both approach are supported out of the box by Spincast but this specific section is about
traditional HTML forms and their validation!
form model.
You need to prepare a model to back the form you are going to display. This
model is sometimes called "form backing object", "form backing bean"
or "command object". It's the object used to transfert the values
of a form from the server to the client (to populate the form's fields) and vice versa.
You create that form model as a JsonObject and you add it to the response model, the root object the Templating Engine has access to. For example :
public void displayUserForm(AppRequestContext context) {
// Creates a JsonObject as the the model for the form
JsonObject userForm = context.json().create();
// ... populates it with inital values,
// if required
userForm.put("email", "test@example.com");
// Adds the form model to the response model
context.response().getModel().put("userForm", userForm);
// Displays the form using a HTML template
context.response().sendTemplateHtml("/templates/userCreationTemplate.html");
}
You can then use this form model to populate fields, in the template. For example :
<form method="post">
<input type="text"
name="userForm.email"
value="{{userForm.email}}" />
//...
</form>
{% endverbatim %}
When the form is submitted, you get back its model
using the context.request().getFormData()
method, in your Route Handler. We will see this in details in the
Getting the submitted form data section,
but here's a quick example :
public void manageUserForm(AppRequestContext context) {
// Gets the submitted form model
JsonObject userForm = context.request()
.getFormData()
.getJsonObject("userForm");
// ...
}
You may have noticed that we are not using a dedicated class to represent the form
model (a "UserForm" class, for example) : we use a dynamic JsonObject.
Spincast supports both approaches, but we think a dynamic JsonObject
is better for the model of a form. 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 model to a dedicated Entity class and pass it to
services, repositories, etc.
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 form, 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
}
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 context.request().getFormData(),
which is a JsonObject, 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()
.getFormData()
.getJsonObject("userForm")
.convert(UserCreationForm.class);
// ...
}
By using a dynamic JsonObject as the form model, a benefit is
that we don't have to create in advance all the elements required
to match the fields of our 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 create a user. This form will display two fields : one for a username and one for an email. Our inital form model doesn't have to specify those two elements when it is first created :
public void myRouteHandler(AppRequestContext context) {
// Empty model! No username and no email
// element specified...
JsonObject userForm = context.json().create();
// Adds the form model to the response model
context.response().getModel().put("userForm", userForm);
// Renders a template containing the user creation
// form
context.response().sendTemplateHtml("/templates/userCreationTemplate.html");
}
Here's what that HTML form may looks like (we are using the syntax for
the default Templating Engine, Pebble) :
<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 fields 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 yet (this is required if
strictVariables is on).
The "name" attributes of the fields are very important : they represent
the JsonPaths that Spincast is going to use to
dynamically create the required elements on the model, when the form is submitted.
Let's say this form is submitted. You would then access the values of the fields like so,
in your Route Handler :
public void myRouteHandler(AppRequestContext context) {
// Gets the form model
JsonObject userForm = context.request()
.getFormData()
.getJsonObject("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");
}
JsonPaths :
public void myRouteHandler(AppRequestContext context) {
String username = context.request().getFormData().getString("userForm.username");
String email = context.request().getFormData().getString("userForm.email");
}
As you can see, Spincast uses the "name" attribute
of a field as a JsonPath to dynamically create an element
for the field. This gives you a lot of flexibility client-side
since you can dynamically generate new fields or even entire new forms,
using javascript.
Text based fields, such as text, password,
email and textarea are
very easy to manipulate :
JsonPath you want for their associated model element as their
"name" attribute.
JsonPath to target
the current value of the element on the model,
and you output it in the "value" attribute.
default("") filter to make sure not exception
is thrown if the model element doesn't exist yet.
<input type="text"
name="user.email"
value="{{user.email | default('')}}" />
{% endverbatim %}
Sometimes we want multiple text fields to be grouped together. For example, let's say we
want various "tags" to be associated with an "article" 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 :
"name" attribute for every field, but we suffix this name with the
position of the tag inside the final array.
For example : "article.tags[0]" or "article.tags[1]"
"[X]" suffixed name to get and display the "value"
attributes.
JsonPath to target each element!
For example :
<form method="post">
<input type="text" class="form-control" name="article.tags[0]"
value="{{article.tags[0] | default('')}}" />
<input type="text" class="form-control" name="article.tags[1]"
value="{{article.tags[1] | default('')}}">
<input type="text" class="form-control" name="article.tags[2]"
value="{{article.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) {
JsonObject model = context.request().getFormData();
// Get all the tags of the article, as an array
JsonArray tags = model.getJsonArray("article.tags");
// You could also access one of the tag directly, using
// its full JsonPath
String thirdTag = model.getString("article.tags[2]");
//...
}
The select fields come in two flavors : single value or multiple values. To use them :
JsonPath of the associated element in the
"name" attribute of the select field.
option elements of the field you
use the selected(...) filter to check if the option
should be selected or not.
select field :
<select name="user.favDrink" class="form-control">
<option value="tea" {{user.favDrink | selected("tea")}}>Tea</option>
<option value="coffee" {{user.favDrink | selected("coffee")}}>Coffee</option>
<option value="beer" {{user.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 :
<select name="user.favDrink" class="form-control">
{% for drink in allDrinks %}
<option value="{{drink.id}}" {{user.favDrink | selected(drink.id)}}>{{drink.name}}</option>
{% endfor %}
</select>
{% endverbatim %}
In this example, the selected(...) filter
compares the current favorite drink
of the user ("user.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 :
"[]" after the "name" attribute of the select
field. This tells Spincast that an array of values is expected when the form
is submitted.
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.
<select multiple name="user.favDrinks[]" class="form-control">
<option value="tea" {{user.favDrinks | selected("tea")}}>Tea</option>
<option value="coffee" {{user.favDrinks | selected("coffee")}}>Coffee</option>
<option value="beer" {{user.favDrinks | selected("beer")}}>WBeer</option>
</select>
{% endverbatim %}
To display a radio buttons group :
JsonPath of the associated model element as the
"name" attributes.
"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).
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 :
<div class="form-group">
<label for="drinkTea">
<input type="radio"
id="drinkTea"
name="user.favDrink"
{{user.favDrink | checked("tea")}}
value="tea"/> Tea</label>
<label for="drinkCoffee">
<input type="radio"
id="drinkCoffee"
name="user.favDrink"
{{user.favDrink | checked("coffee")}}
value="coffee"> Coffee</label>
<label for="drinkBeer">
<input type="radio"
id="drinkBeer"
name="user.favDrink"
{{user.favDrink | checked("beer")}}
value="beer"> Beer</label>
</div>
{% endverbatim %}
Let's focus on the first radio button of that group. First,
its "name" attribute :
<label for="drinkTea">
<input type="radio"
id="drinkTea"
name="user.favDrink"
{{user.favDrink | checked("tea")}}
value="tea"/> Tea</label>
{% endverbatim %}
"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 model would contain a "user" root element with a "favDrink" element
under it.
Let's now have a look at the checked(...) filter :
<label for="drinkTea">
<input type="radio"
id="drinkTea"
name="user.favDrink"
{{user.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 "user.favDrink" element. That's why we use
"checked(...)". This filter will compare the current
value of the "user.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 :
<label for="drinkTea">
<input type="radio"
id="drinkTea"
name="user.favDrink"
{{user.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) {
// Gets the response model
JsonObject model = context.response().getModel();
// Creates the available drink options
JsonArray allDrinks = context.json().createArray();
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 a "user" object and specifies
// his favorite drink
JsonObject user = context.json().create();
user.put("favDrink", 2);
// Adds the drinks options and the user object
// to the response model
model.put("allDrinks", allDrinks);
model.put("user", user);
// Renders an HTML template containing the form
// to display
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 :
<div class="form-group">
{% for drink in allDrinks %}
<label for="drink_{{drink.id}}">
<input type="radio"
id="drink_{{drink.id}}"
name="user.favDrink"
{{user.favDrink | checked(drink.id)}}
value="{{drink.id}}"/> {{drink.name}}</label>
{% endfor %}
</div>
{% endverbatim %}
You may notice that, in this example, we haven't scoped the elements of the form
under a dedicated "userForm" parent element as we did in other examples. We added the
"allDrinks" and "user" objects as root elements
(instead of naming them "userForm.allDrinks" and "userForm.user").
You don't have to scope the elements of a form inside a parent element
when you are working with a simple template... But as soon as your template is somewhat complex,
for example if it contains more than one form, then scoping elements is a good idea.
Checkboxes are often used in one of those two situations :
[ ] Do you want to subscribe to our newsletter?{% endverbatim %}
Which drinks do you like? [ ] Tea [ ] Coffee [ ] Beer{% endverbatim %}
First, let's look at a single checkbox field :
<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) {
JsonObject model = context.request().getFormData();
boolean tosAccepted = model.getBoolean("myForm.tosAccepted");
//...
}
Now, let's see an example of a group of checkboxes :
<div class="form-group">
<label for="drinkTea">
<input type="checkbox"
id="drinkTea"
name="user.favDrinks[0]"
{{user.favDrinks[0] | checked("tea")}}
value="tea"/> Tea</label>
<label for="drinkCoffee">
<input type="checkbox"
id="drinkCoffee"
name="user.favDrinks[1]"
{{user.favDrinks[1] | checked("coffee")}}
value="coffee"> Coffee</label>
<label for="drinkBeer">
<input type="checkbox"
id="drinkBeer"
name="user.favDrinks[2]"
{{user.favDrinks[2] | checked("beer")}}
value="beer"> Beer</label>
</div>
{% endverbatim %}
"name"
attribute, name that is suffixed with the position of the element in the group.
In fact, 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 Route Handler :
public void myRouteHandler(AppRequestContext context) {
JsonObject model = context.request().getFormData();
// The checked favorite drinks, as an array!
JsonArray favDrinks = model.getJsonArray("user.favDrinks");
//...
}
Note that the positions used in the "name" 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 Route handler would be [null, null, "beer"], not ["beer"]!
This is a good thing because the
JsonPath we use for an element always stays valid ("user.favDrinks[2]"
here).
Uploading a file is very easy using Spincast. The main difference between a "file" field
and the other types of fields is that the uploaded file
will not be available on the model when the form is submitted. You'll have to use a dedicated method to
retrieve it in your Route handler.
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 add
Validation Messages for it, if the file is not valid.
Validating a submitted form involves three main steps :
Make sure you read the dedicated Validation section! Here,
we are going to take for granted that you already know how to create a Validation Set from
a JsonObject and how to validate that JsonObject.
When a HTML form is submitted, Spincast threats the
"name" attributes of the fields as JsonPaths in order to create
a JsonObject representing the form model. In other words,
Spincast converts the submitted form to a JsonObject so you can easily validate and manipulate it.
You access that JsonObject representing the submitted form using the
getFormData() method of the request() add-on. For example :
public void myRouteHandler(AppRequestContext context) {
JsonObject myForm = context.request().getFormData();
//... validates the form
}
If you have more than one form on the same HTML page, you will probably want to
scope the names of every field. For example, a text
field of a form named "myFirstForm" could be :
<input type="text"
class="form-control"
name="myFirstForm.email"
value="{{myFirstForm.email | default('')}}" />
{% endverbatim %}
JsonObject representing this particular
"myFirstForm" form model using :
public void myRouteHandler(AppRequestContext context) {
// Gets the "myFirstForm" form model
JsonObject myFirstForm = context.request()
.getFormData()
.getJsonObject("myFirstForm");
// Gets the value of the "email" field from
// the "myFirstForm" form model
String email = myFirstForm.getString("email");
}
public void myRouteHandler(AppRequestContext context) {
String email = context.request().getFormData().getString("myFirstForm.email");
}
Spincast supports arrays to group a bunch of fields together.
When multiple fields have the same "name" attribute, they are
automatically grouped together when a form is submitted. For example :
<div class="form-group">
<label for="drinkTea">
<input type="checkbox"
id="drinkTea"
name="user.favDrinks"
{{user.favDrinks[0] | checked("tea")}}
value="tea"/> Tea</label>
<label for="drinkCoffee">
<input type="checkbox"
id="drinkCoffee"
name="user.favDrinks"
{{user.favDrinks[1] | checked("tea")}}
value="coffee"> Coffee</label>
</div>
{% endverbatim %}
"user.favDrinks" fields to be grouped
in a single array. We could access this array with :
public void myRouteHandler(AppRequestContext context) {
JsonArray tags = context.request().getFormData().getJsonArray("user.favDrinks");
//...
}
There are two problems with this approach though :
"user.favDrinks" field is submitted,
Spincast has no way of knowing it should be part of an array, so
it won't create one. In other words, your code must be prepare to
receive those "favDrinks" in an array or as a single element.
Not cool!
"favDrinks" elements in
a different order, from requests to requests. Not cool either!
The first improvement we can make, is to make sure
we always receive an array, never a single element alone! To achieve that, we can use
a "name" attribute ending with "[]". For example :
<div class="form-group">
<label for="drinkTea">
<input type="checkbox"
id="drinkTea"
name="user.favDrinks[]"
{{user.favDrinks[0] | checked("tea")}}
value="tea"/> Tea</label>
<label for="drinkCoffee">
<input type="checkbox"
id="drinkCoffee"
name="user.favDrinks[]"
{{user.favDrinks[1] | checked("tea")}}
value="coffee"> Coffee</label>
</div>
{% endverbatim %}
"user.favDrinks[]"
fields, even if only one of those fields is actually submitted.
But this approach still doesn't guarantee the order of the elements in the
resulting array, when multiple fields are submitted!
The third and best method, is to suffix the "names" attribute not only with
"[]", but with "[X]",
where "X" is the position of the element in the group!
For example :
<div class="form-group">
<label for="drinkTea">
<input type="checkbox"
id="drinkTea"
name="user.favDrinks[0]"
{{user.favDrinks[0] | checked("tea")}}
value="tea"/> Tea</label>
<label for="drinkCoffee">
<input type="checkbox"
id="drinkCoffee"
name="user.favDrinks[1]"
{{user.favDrinks[1] | checked("tea")}}
value="coffee"> Coffee</label>
</div>
{% endverbatim %}
"user.favDrinks[0]" field will always be at
the first position of the resulting array and "user.favDrinks[1]" always be at the second
position. Note that since the positions are now guaranteed, Spincast will automatically adds null elements
if some indexes are not associated with a submitted field.
In this third solution, notice that
the "name" attribute is the actual JsonPath
of the associated element. We like that!
Once you we the JsonObject representing the model of the submitted form, we can start validating it.
To do so, we create a Validation Set from this object.
For example :
public void myRouteHandler(AppRequestContext context) {
// Gets the form model
JsonObject myForm = context.request().getFormData().getJsonObject("myForm");
// Gets a Validation Set for the form model
JsonObjectValidationSet formValidationSet = myForm.validationSet();
// ...
}
Validation Set object to store the
Validation Messages resulting from the validation we're about
to perform.
Here's a quick example where we make sure that an "email"
field is valid and, if it is, that the email is not already
used by another user in our system :
public void myRouteHandler(AppRequestContext context) {
JsonObject myForm = context.request().getFormData().getJsonObject("myForm");
JsonObjectValidationSet formValidationSet = myForm.validationSet();
ValidationSet lastResult = formValidationSet.validationEmail().jsonPath("email").validate();
if(lastResult.isValid()) {
boolean emailIsAvailable = isEmailAvailable(myForm.getString("email"));
if(!emailIsAvailable) {
formValidationSet.addError("email",
"EMAIL_ALREADY_EXISTS",
"This email is already used.");
}
}
// ...
}
Explanation :
validationEmail() predefined validation, we validate the
submitted email.
Error Validation Message
to our Validation Set, using the addError(...) method.
When we are done validating a form, we usually have two choices :
If the form is valid, we can process it. This often involves
creating Entities objects from the form data,
calling business logic in services and
making modifications to data sources.
When the submitted (and valid) form has been processed, it's a good practice to redirect the user to a confirmation page, with a Flash message to be displayed. Doing do, you prevent the form to be submitted again, in case the user tries to refresh the resulting page.
Error Validation Messages
have been generated by the validation), then we need to redisplay the form to the user with
the Validation Messages. We're going to see how to display those messages
in the following Displaying validation messages section
but, for now, let's see what first needs to be done in the Route Handler...
In order to prepare a form to be redisplayed with the Validation Messages resulting from
its validation, we need to add the form model and the resulting Validation Set back to
the response model! This is required so the
Templating Engine can redisplay the fields with their submitted (but potentially invalid values)
and the various Validation Messages. For example :
public void myRouteHandler(AppRequestContext context) {
JsonObject myForm = context.request().getFormData().getJsonObject("myForm");
JsonObjectValidationSet formValidationSet = myForm.validationSet();
// ... Perfoms validation and saves Validation Messages
// on the Validation Set
if(formValidationSet.isValid()) {
// ... Calls services / business logic / data sources
// to process the submitted data
// Redirects to a confirmation page
context.response().redirect(FlashMessageLevel.SUCCESS,
"The form has been processed successfully.");
} else {
// Adds the form model back to the response model
context.response().getModel().put("myForm", myForm);
// Adds the Validation Set to the response model
// using a key of our choice
context.response().getModel().put("validation", formValidationSet);
// Redisplays the form using the Templating Engine
context.response().sendTemplateHtml("/templates/myTemplate.html");
}
}
Explanation :
Validation Set to the
response model too.
HTML
template.
In this example, we added the Validation Set to the response model using a
"validation" key. This would result in the Validation Messages
to be accessible in the template using validation['some.validation.key']. For example :
{% verbatim %}
<div>{{validation['myForm.email']}}</div>
Now, everything required by our Templating Engine to be able to redisplay the form
with the Validation Messages has been made available.
At the end of the previous section, we showed that two main elements have to be added to
the response model in order to redisplay an invalid form :
Validation Set resulting from the validation
"Use the JsonPath of a validated element as its validation key" convention as suggested in
the Validation Keys section, the final model your templates will
have access to will look like this :
{
"myForm" : {
"name" : "Stromgol"
"email" : "abc"
"books": [
{
"title" : "Dune",
"author": "Frank Herbert"
},
{
"title" : "The Hitchhiker's Guide to the Galaxy",
"author" : ""
}
]
},
"validation" : {
"_" : {
"hasErrors" : true,
"hasWarnings" : false,
"isValid" : false,
"hasSuccesses" : false
},
"myForm.email" : {
"level" : "ERROR",
"code" : "VALIDATION_TYPE_EMAIL",
"text" : "Invalid email address"
},
"myForm.books[1].author" : {
"level" : "ERROR",
"code" : "VALIDATION_TYPE_NOT_BLANK",
"text" : "The author can't be empty"
}
}
// ...
}
JsonPath
of every element is respected.
Below is the serialized Validation Set
resulting from the validation. Here, the validation keys have been scoped
in a "validation" object so they don't clash with any other element on the model.
Notice that each validation key is the string representation of the JsonPath of the element
it is associated with!
You can also see the special "_" element that is automatically added when
a Validation Set is converted to a JsonObject.
This element summarizes the validation.
This structure of the templating model makes the Validation Messages very easy to be retrieved and displayed.
For example :
<div class="form-group">
<input type="text"
class="form-control"
name="myForm.email"
value="{{myForm.email | default('')}}" />
{{validation['myForm.email'] | validationMessages()}}
</div>
{% endverbatim %}
Here, validationMessages() is a
Pebble filter
provided by Spincast that can be used to display the Validation Messages associated with a field.
Since our validation keys are in fact the string representation of the JsonPaths of the
validated elements, it's very clear how to retrieve the Validation Messages for a particular
field : it is, in most cases, the same as the "name" attribute of that field [4].
Spincast provides utilities to display the
Validation Messages when the default Templating Engine is used,
Pebble. But, as we saw, the template model is pretty much a
simple Map<String, Object> so no magic is involved and any other Templating Engine
could 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 a field.
Here's an example :
<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 fields.
Here's an example :
<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. In the Validation section,
you can learn that Spincast can automatically add such Validation Messages for an array itself,
when the validation of its elements is done.
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 :
<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 %}
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
(more info), as the element
passed to those filters.
For example :
{% if validation['myForm._'] | validationFresh() %}
<div>This form is displayed for the first time!</div>
{% endif %}
{% endverbatim %}
{% 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.
{% 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 :
{% 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 :
{% if validation['myForm._'] | validationSubmitted() and validation['myForm.email'] | validationIsValid() %}
<div>The email has been validated and is ok!</div>
{% endif %}
{% endverbatim %}