
templates.docs.forms.html Maven / Gradle / Ivy
Show all versions of spincast-website Show documentation
{#==========================================
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);
// ...
}