Guide: Thymeleaf + Spring. Part 2

  • Tutorial
The first part
The third part

5 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:

<divclass="seedstarterlist"th:unless="${#lists.isEmpty(allSeedStarters)}"><h2th:text="#{title.list}">List of Seed Starters</h2><table><thead><tr><thth:text="#{seedstarter.datePlanted}">Date Planted</th><thth:text="#{seedstarter.covered}">Covered</th><thth:text="#{seedstarter.type}">Type</th><thth:text="#{seedstarter.features}">Features</th><thth:text="#{seedstarter.rows}">Rows</th></tr></thead><tbody><trth:each="sb : ${allSeedStarters}"><tdth:text="${{sb.datePlanted}}">13/01/2011</td><tdth:text="#{|bool.${sb.covered}|}">yes</td><tdth:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td><tdth:text="${#strings.arrayJoin(
                           ', ')}">Electric Heating, Turf</td><td><table><tbody><trth:each="row,rowStat : ${sb.rows}"><tdth:text="${rowStat.count}">1</td><tdth:text="${}">Thymus Thymi</td><tdth: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 .


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><thth:text="#{seedstarter.datePlanted}">Date Planted</th><thth:text="#{seedstarter.covered}">Covered</th><thth:text="#{seedstarter.type}">Type</th><thth:text="#{seedstarter.features}">Features</th><thth: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):

@Beanpublic ResourceBundleMessageSource messageSource(){
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    return messageSource;

... and this basename property indicates that we will have files in our classpath, such as or . Let's look at the spanish version:

title.list=Lista de semilleros
seedstarter.datePlanted=Fecha de plantación
seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato específico para semilleros
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.


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:


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:


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 an array of features .
  • 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:

                   ', ')}">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><trth:each="row,rowStat : ${sb.rows}"><tdth:text="${rowStat.count}">1</td><tdth:text="${}">Thymus Thymi</td><tdth: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 :


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 $ {} 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:

<inputtype="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:

<inputtype="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 of the path 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><labelth:for="${'covered')}"th:text="#{seedstarter.covered}">Covered</label><inputtype="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 # ('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 multivalued flag field:

<ul><lith:each="feat : ${allFeatures}"><inputtype="checkbox"th:field="*{features}"th:value="${feat}" /><labelth: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><inputid="features1"name="features"type="checkbox"value="SEEDSTARTER_SPECIFIC_SUBSTRATE" /><inputname="_features"type="hidden"value="on" /><labelfor="features1">Seed starter-specific substrate</label></li><li><inputid="features2"name="features"type="checkbox"value="FERTILIZER" /><inputname="_features"type="hidden"value="on" /><labelfor="features2">Fertilizer used</label></li><li><inputid="features3"name="features"type="checkbox"value="PH_CORRECTOR" /><inputname="_features"type="hidden"value="on" /><labelfor="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 = attribute to “checked” in 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><lith:each="ty : ${allTypes}"><inputtype="radio"th:field="*{type}"th:value="${ty}" /><labelth: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:

<selectth:field="*{type}"><optionth: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 extended binding capabilities of 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());
@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"));

And now we can add a dynamic table to our form:

<table><thead><tr><thth:text="#{seedstarter.rows.head.rownum}">Row</th><thth:text="#{seedstarter.rows.head.variety}">Variety</th><thth:text="#{seedstarter.rows.head.seedsPerCell}">Seeds per cell</th><th><buttontype="submit"name="addRow"th:text="#{seedstarter.row.add}">Add row</button></th></tr></thead><tbody><trth:each="row,rowStat : *{rows}"><tdth:text="${rowStat.count}">1</td><td><selectth:field="*{rows[__${rowStat.index}__].variety}"><optionth:each="var : ${allVarieties}"th:value="${}"th:text="${}">Thymus Thymi</option></select></td><td><inputtype="text"th:field="*{rows[__${rowStat.index}__].seedsPerCell}" /></td><td><buttontype="submit"name="removeRow"th:value="${rowStat.index}"th:text="#{seedstarter.row.remove}">Remove row</button></td></tr></tbody></table>

A lot of things are here, but not so much not to understand ... except for one strange thing:


If you remember from the “ Using Thymeleaftutorial , 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:


... 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><selectid="rows0.variety"name="rows[0].variety"><optionselected="selected"value="1">Thymus vulgaris</option><optionvalue="2">Thymus x citriodorus</option><optionvalue="3">Thymus herba-barona</option><optionvalue="4">Thymus pseudolaginosus</option><optionvalue="5">Thymus serpyllum</option></select></td><td><inputid="rows0.seedsPerCell"name="rows[0].seedsPerCell"type="text"value="" /></td><td><buttonname="removeRow"type="submit"value="0">Remove row</button></td></tr><tr><td>2</td><td><selectid="rows1.variety"name="rows[1].variety"><optionselected="selected"value="1">Thymus vulgaris</option><optionvalue="2">Thymus x citriodorus</option><optionvalue="3">Thymus herba-barona</option><optionvalue="4">Thymus pseudolaginosus</option><optionvalue="5">Thymus serpyllum</option></select></td><td><inputid="rows1.seedsPerCell"name="rows[1].seedsPerCell"type="text"value="" /></td><td><buttonname="removeRow"type="submit"value="1">Remove row</button></td></tr></tbody>

Also popular now: