First partThe third part5 Seed Starter Data Display
The first thing that our
/WEB-INF/templates/seedstartermng.html page
shows is the list with the initial starting data that is currently stored. To do this, we need some external messages, as well as some expression work for model attributes. Like this:
')
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}"> <h2 th:text="#{title.list}">List of Seed Starters</h2> <table> <thead> <tr> <th th:text="#{seedstarter.datePlanted}">Date Planted</th> <th th:text="#{seedstarter.covered}">Covered</th> <th th:text="#{seedstarter.type}">Type</th> <th th:text="#{seedstarter.features}">Features</th> <th th:text="#{seedstarter.rows}">Rows</th> </tr> </thead> <tbody> <tr th:each="sb : ${allSeedStarters}"> <td th:text="${{sb.datePlanted}}">13/01/2011</td> <td th:text="#{|bool.${sb.covered}|}">yes</td> <td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td> <td th:text="${#strings.arrayJoin( #messages.arrayMsg( #strings.arrayPrepend(sb.features,'seedstarter.feature.')), ', ')}">Electric Heating, Turf</td> <td> <table> <tbody> <tr th:each="row,rowStat : ${sb.rows}"> <td th:text="${rowStat.count}">1</td> <td th:text="${row.variety.name}">Thymus Thymi</td> <td th:text="${row.seedsPerCell}">12</td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div>
There is a lot to see. Let's look at each piece separately.
First of all, this section will be displayed only if there is a seed starter. We achieve this with the
th: never attribute and the
# lists.isEmpty (...) function.
<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">
Note that all utility objects, such as
#lists , are available in Spring EL expressions as well as in standard OGNL expressions.
The next thing to see is a lot of internationalized (externalized) texts, such as:
<h2 th: text = "# {title.list}"> List of Seed Starters
<table> <thead> <tr> <th th:text="#{seedstarter.datePlanted}">Date Planted</th> <th th:text="#{seedstarter.covered}">Covered</th> <th th:text="#{seedstarter.type}">Type</th> <th th:text="#{seedstarter.features}">Features</th> <th th:text="#{seedstarter.rows}">Rows</th> ...
This is the Spring MVC application, we have already defined the
MessageSource beans in our Spring configuration (
MessageSource objects are the standard way to manage external texts in Spring MVC):
@Bean public ResourceBundleMessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("Messages"); return messageSource; }
... and this
basename property indicates that we will have files in our classpath, such as
Messages_es.properties or
Messages_en.properties . Let's look at the spanish version:
title.list=Lista de semilleros date.format=dd/MM/yyyy bool.true=sí bool.false=no seedstarter.datePlanted=Fecha de plantación seedstarter.covered=Cubierto seedstarter.type=Tipo seedstarter.features=Características seedstarter.rows=Filas seedstarter.type.WOOD=Madera seedstarter.type.PLASTIC=Plástico seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato específico para semilleros seedstarter.feature.FERTILIZER=Fertilizante seedstarter.feature.PH_CORRECTOR=Corrector de PH
In the first column of the table we show the date when the starter was prepared. But
we will show that it is formatted the way we defined in our
DateFormatter . To do this, we will use the double-bracket syntax (
$ {{...}} ), which will automatically use the Spring conversion service, including the DateFormatter, which we registered during setup.
<td th:text="${{sb.datePlanted}}">13/01/2011</td>
The following shows whether the initial seed starter container is covered or not by converting the property value of a boolean covered bin to an internationalized yes or no with a literal substitution expression:
<td th:text="#{|bool.${sb.covered}|}">yes</td>
Now we have to show the type of initial seed starter of the container. The type is a java-enumeration with two values (
WOOD and
PLASTIC ), and therefore we defined two properties in our
Messages file with the names
seedstarter.type.WOO D and
seedstarter.type.PLASTIC .
But to get internationalized type names, we need to add a
seedstarter.type. prefix to the enum value using an expression, the result of which we will then use as the message key:
<td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td>
The most difficult part of this list is the
feature column. In it, we want to display all the functions of our container, which are represented as an array of
Feature enums, separated by commas. Like "
Electric heating, lawn ."
Note that this is especially difficult, because these enumeration values must also be inferred, as we did with Types. The output stream is as follows:
- Substitute the appropriate prefix to all elements of the feature array.
- Receive external messages corresponding to all keys from step 1.
- Attach all messages received in step 2, using a comma as a delimiter.
To do this, we create the following code:
<td th:text="${#strings.arrayJoin( #messages.arrayMsg( #strings.arrayPrepend(sb.features,'seedstarter.feature.')), ', ')}">Electric Heating, Turf</td>
The last column of our list will actually be quite simple. Even if it has a nested table to display the contents of each row in the container:
<td> <table> <tbody> <tr th:each="row,rowStat : ${sb.rows}"> <td th:text="${rowStat.count}">1</td> <td th:text="${row.variety.name}">Thymus Thymi</td> <td th:text="${row.seedsPerCell}">12</td> </tr> </tbody> </table> </td>
6 Creating Forms
6.1 Processing Command Object
The command object is the name that Spring MVC gives forms support bins, that is, objects that model form fields and provide getter and install methods that the platform will use to establish and retrieve values entered by the user in the browser.
Thymeleaf requires that you specify a command object using the
th: object attribute in your
<form> tag :
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post"> ... </form>
This is consistent with another use of
th: object , but in fact this particular script adds some limitations for proper integration with the Spring MVC infrastructure:
- The th: object attribute values in form tags must be variable expressions ( $ {...} ), indicating only the name of the model attribute, without navigating through the properties. This means that an expression like $ {seedStarter} is valid, but $ {seedStarter.data} will not.
- Inside the <form> tag another attribute th: object cannot be specified. This is consistent with the fact that HTML forms cannot be nested.
6.2 Inputs
Let's now see how to add input to our form:
<input type="text" th:field="*{datePlanted}" />
As you can see, we are introducing a new attribute:
th: field . This is a very important feature for integrating Spring MVC because it does all the hard work of linking your
input to a property in the form support component. You can see it as the equivalent of the path attribute in a tag from the Spring MVC JSP tag library.
The th: field attribute behaves differently depending on whether it is attached to the <input> tag, <select> or <textarea> tag (and also depending on the specific type of <input> tag). In this case (input [type = text]), the above line of code looks like:
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />
... But actually, this is a little more, because
th: field will also use the registered Spring transformation service, including the
DateFormatter , which we saw earlier (even if the field expression is not enclosed in square brackets). Due to this, the date will be displayed correctly formatted.
The values for the
th: field attributes must be selectable expressions (
* {...} ), which makes sense given the fact that they will be evaluated on the component that supports the form, and not on the context variables (or model attributes in the Spring MVC jargon). ).
Unlike expressions in
th: object , these expressions can include navigation through properties (in fact, any expression allowed for the <form: input> JSP tag attribute) is allowed here.
Notice that th: field also understands the new types of <input> element introduced in HTML5, such as <input type = "datetime" ... />, <input type = "color" ... />, etc., effectively adding full HTML5 support for Spring MVC.
6.3 Checkbox fields
th: field also allows you to define input checkbox checkboxes. Let's see an example from our HTML page:
<div> <label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">Covered</label> <input type="checkbox" th:field="*{covered}" /> </div>
Notice that there is something else besides the checkbox itself, for example, an external label, as well as using the function
# ids.next ('closed') to get the value that will be applied to the
id attribute of the checkbox data.
Why do we need the dynamic creation of the
id attribute for this field? Since the flags are potentially multivalued, and therefore the sequence number suffix will always be added to their identifier values (internally using the
# ids.seq (...) function) to ensure that each of the input flags of the same property has a different identifier value .
It will be easier for us to see this if we look at such a multi-valued checkbox:
<ul> <li th:each="feat : ${allFeatures}"> <input type="checkbox" th:field="*{features}" th:value="${feat}" /> <label th:for="${#ids.prev('features')}" th:text="#{${'seedstarter.feature.' + feat}}">Heating</label> </li> </ul>
Notice that this time we added the
th: value attribute, because the function field is not logical, as described above, but is an array of values.
Let's see the HTML output generated by this code:
<ul> <li> <input id="features1" name="features" type="checkbox" value="SEEDSTARTER_SPECIFIC_SUBSTRATE" /> <input name="_features" type="hidden" value="on" /> <label for="features1">Seed starter-specific substrate</label> </li> <li> <input id="features2" name="features" type="checkbox" value="FERTILIZER" /> <input name="_features" type="hidden" value="on" /> <label for="features2">Fertilizer used</label> </li> <li> <input id="features3" name="features" type="checkbox" value="PH_CORRECTOR" /> <input name="_features" type="hidden" value="on" /> <label for="features3">PH Corrector used</label> </li> </ul>
Here we see how the sequence suffix is added to each
id input attribute and how the
# ids.prev (...) function allows us to extract the last value of the sequence generated for a specific input identifier.
Do not worry about these hidden entries with
name = "_ features" : they are automatically added to avoid problems with browsers that do not send unselected flag values to the server when submitting a form.
Also note that if our
features property contained some selected values in our form-backing bean, then the
th: field would take care of this and add the
checked = "checked" attribute to the appropriate input tags.
6.4 Radio Button fields
Switch fields are set similarly to non-boolean (multi-valued) flags, except that, of course, they are not multi-valued:
<ul> <li th:each="ty : ${allTypes}"> <input type="radio" th:field="*{type}" th:value="${ty}" /> <label th:for="${#ids.prev('type')}" th:text="#{${'seedstarter.type.' + ty}}">Wireframe</label> </li> </ul>
6.5 Dropdown / List selectors
The selection fields consist of two parts: the <select> tag and its nested <option> tags. When creating a field of this type, only the <select> tag must include the
th: field attribute, but the
th: value attributes in the nested <option> tags will be very important, as they will provide an opportunity to know what the currently selected option is (similar to non-boolean flags and switches) ).
Let's rebuild the type field of the drop-down list:
<select th:field="*{type}"> <option th:each="type : ${allTypes}" th:value="${type}" th:text="#{${'seedstarter.type.' + type}}">Wireframe</option> </select>
At the moment, understanding this piece of code is pretty easy. Just notice how attribute priority allows us to set the
th: each attribute in the <option> tag itself.
6.6 Dynamic fields
Thanks to the advanced capabilities of binding form fields in Spring MVC, we can use the complex
Spring EL expressions to bind dynamic form fields to our form-backing bean. This will allow us to create new
Row objects in our
SeedStarter component and add fields of these lines to our form upon user request.
To do this, we need a couple of new mapped methods in our controller, which will add or remove a line from our
SeedStarter depending on the presence of certain query parameters:
@RequestMapping(value="/seedstartermng", params={"addRow"}) public String addRow(final SeedStarter seedStarter, final BindingResult bindingResult) { seedStarter.getRows().add(new Row()); return "seedstartermng"; } @RequestMapping(value="/seedstartermng", params={"removeRow"}) public String removeRow( final SeedStarter seedStarter, final BindingResult bindingResult, final HttpServletRequest req) { final Integer rowId = Integer.valueOf(req.getParameter("removeRow")); seedStarter.getRows().remove(rowId.intValue()); return "seedstartermng"; }
And now we can add a dynamic table to our form:
<table> <thead> <tr> <th th:text="#{seedstarter.rows.head.rownum}">Row</th> <th th:text="#{seedstarter.rows.head.variety}">Variety</th> <th th:text="#{seedstarter.rows.head.seedsPerCell}">Seeds per cell</th> <th> <button type="submit" name="addRow" th:text="#{seedstarter.row.add}">Add row</button> </th> </tr> </thead> <tbody> <tr th:each="row,rowStat : *{rows}"> <td th:text="${rowStat.count}">1</td> <td> <select th:field="*{rows[__${rowStat.index}__].variety}"> <option th:each="var : ${allVarieties}" th:value="${var.id}" th:text="${var.name}">Thymus Thymi</option> </select> </td> <td> <input type="text" th:field="*{rows[__${rowStat.index}__].seedsPerCell}" /> </td> <td> <button type="submit" name="removeRow" th:value="${rowStat.index}" th:text="#{seedstarter.row.remove}">Remove row</button> </td> </tr> </tbody> </table>
Quite a lot of things are here, but not so much not to understand ... except for one
strange thing:
<select th:field="*{rows[__${rowStat.index}__].variety}"> ... </select>
If you remember from the “
Using Thymeleaf ”
tutorial , the syntax
__ $ {...} __ is a preprocessing expression, which is an internal expression that is evaluated before the actual evaluation of the entire expression. But why such a way to specify the index of the string? Wouldn't that be enough with:
<select th:field="*{rows[rowStat.index].variety}"> ... </select>
... actually not. The problem is that Spring EL does not evaluate the variables in the brackets of the array index, so when you run the above expression, we get an error telling us that
rows [rowStat.index] (instead of
rows [[0] ,
rows [1] , etc.) ) Invalid position in the row collection. That is why preliminary processing is necessary here.
Let's look at a fragment of the resulting HTML-code after several times clicked "Add Row":
<tbody> <tr> <td>1</td> <td> <select id="rows0.variety" name="rows[0].variety"> <option selected="selected" value="1">Thymus vulgaris</option> <option value="2">Thymus x citriodorus</option> <option value="3">Thymus herba-barona</option> <option value="4">Thymus pseudolaginosus</option> <option value="5">Thymus serpyllum</option> </select> </td> <td> <input id="rows0.seedsPerCell" name="rows[0].seedsPerCell" type="text" value="" /> </td> <td> <button name="removeRow" type="submit" value="0">Remove row</button> </td> </tr> <tr> <td>2</td> <td> <select id="rows1.variety" name="rows[1].variety"> <option selected="selected" value="1">Thymus vulgaris</option> <option value="2">Thymus x citriodorus</option> <option value="3">Thymus herba-barona</option> <option value="4">Thymus pseudolaginosus</option> <option value="5">Thymus serpyllum</option> </select> </td> <td> <input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value="" /> </td> <td> <button name="removeRow" type="submit" value="1">Remove row</button> </td> </tr> </tbody>