📜 ⬆️ ⬇️

Thymeleaf Tutorial: Chapter 8. Template Layout

Table of contents

8 Template Layout


8.1 Including Template Fragments


Fragment definition and reference

In the templates, we often include fragments from other templates, such as the footer, title, menu ...

To do this simply, Thymeleaf will require us to define these fragments for later inclusion using the th: fragment attribute.
')
Suppose we want to add a standard footer for all our product pages, so create the file /WEB-INF/templates/footer.html containing this code:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <div th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </div> </body> </html> 

The above code defines a fragment called “copy” that we can easily include on our home page using one of the attributes th: insert or th: replace (and also th: include , although its use is no longer recommended with Thymeleaf 3.0):

 <body> ... <div th:insert="~{footer :: copy}"></div> </body> 

Notice that th: insert expects a fragment expression (~ {...}) to the fragment. However, in the example, which is a non-complex fragment expression, the element (~ {,}) is completely optional, so the above code will be equivalent:

 <body> ... <div th:insert="footer :: copy"></div> </body> 

Fragment syntax


The syntax of fragment expressions is quite simple. There are three different formats:


Remember again, the ~ {...} block is optional in th: insert / th: replace .

Snippets can include any th: * attributes. These attributes will be executed after the fragment is included in the target template (the one that contains the attribute th: insert / th: replace and they can refer to any context variables defined in this target template.

The big advantage of this approach to fragments is that you can write your fragments on pages that are perfectly displayed by the browser, with full and even the correct layout of the markup, while retaining the ability to turn Thymeleaf into other templates.

Link to fragments without th: fragment

Due to the strength of the Markup Selectors, we can include fragments that do not use the th: fragment attributes. It may even be a markup code coming from another application without knowledge of Thymeleaf:

 <div id="copy-section"> © 2011 The Good Thymes Virtual Grocery </div> 

We can use the snippet above by simply referring to it by its id attribute, similar to the CSS selector:

 <body> ... <div th:insert="~{footer :: #copy-section}"></div> </body> 

The difference between th: insert and th: replace (and th: include)

And what is the difference between th: insert and th: replace (and th: include, not recommended from 3.0)?

th: insert is the simplest: it simply inserts a specific fragment as the body of its parent (host) tag.

th: replace overwrites its parent (host) tag with a specific fragment.

th: include is similar to th: insert , but instead of inserting a fragment, it simply inserts the contents of the fragment.

That is the following HTML fragment:

 <footer th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </footer> 

included three times in the parent (host) tag:

 <body> ... <div th:insert="footer :: copy"></div> <div th:replace="footer :: copy"></div> <div th:include="footer :: copy"></div> </body> 

will give the result:

 <body> ... <div> <footer> © 2011 The Good Thymes Virtual Grocery </footer> </div> <footer> © 2011 The Good Thymes Virtual Grocery </footer> <div> © 2011 The Good Thymes Virtual Grocery </div> </body> 

8.2 Parameterizable Fragments


To create a more functional mechanism for template fragments, fragments defined with th: fragment can take a set of parameters:

 <div th:fragment="frag (onevar,twovar)"> <p th:text="${onevar} + ' - ' + ${twovar}">...</p> </div> 

This requires using one of these two syntaxes to call a fragment from th: insert or th: replace :

 <div th:replace="::frag (${value1},${value2})">...</div> <div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div> 

Please note that the order is not important in the latter version:

 <div th:replace="::frag (twovar=${value2},onevar=${value1})">...</div> 

Local fragment variables without fragment arguments

Even if the fragments are defined without arguments:

 <div th:fragment="frag"> ... </div> 

we could use the second syntax specified above to call them (and only the second one):

 <div th:replace="::frag (onevar=${value1},twovar=${value2})"> 

This will be equivalent to the combination of th: replace and th: with :

 <div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}"> 

Please note that this specification of local variables for a fragment — whether it has arguments or not — does not empty the context before it is executed. The snippets will still have access to each context variable used in the calling template.

th: assert for statements in a pattern

The th: assert attribute can specify a list of expressions, separated by commas, which should be evaluated and displayed as true for each evaluation, and throw exceptions otherwise.

 <div th:assert="${onevar},(${twovar} != 43)">...</div> 

This is useful for checking whether the parameters match the signature of the fragment:

 <header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header> 

8.3 Flexible layouts: in addition to simply inserting fragments


Thanks to fragments, we can specify parameters that are not text, numbers, beans ... but instead are markup fragments.

This allows us to create fragments in such a way that they can be enriched with markup coming from the calling templates, which leads to a very flexible template layout mechanism.

Note the use of header and link variables in the following snippet:

 <head th:fragment="common_header(title,links)"> <title th:replace="${title}">The awesome application</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}"> <link rel="shortcut icon" th:href="@{/images/favicon.ico}"> <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script> <!--/* Per-page placeholder for additional links */--> <th:block th:replace="${links}" /> </head> 

You can call this snippet like this:

 <head th:replace="base :: common_header(~{::title},~{::link})"> <title>Awesome - Main</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}"> </head> 

... and the result will be the use of the actual <title> and <link & gt tags from our invocation template as the value of the header variables and links, with the result that our fragment is configured during insertion:

 <head> <title>Awesome - Main</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css"> <link rel="shortcut icon" href="/awe/images/favicon.ico"> <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script> <link rel="stylesheet" href="/awe/css/bootstrap.min.css"> <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css"> </head> 

Use of an empty fragment

The special fragment expression " empty fragment " (~ {}) can be used to indicate markup. Using the previous example:

 <head th:replace="base :: common_header(~{::title},~{})"> <title>Awesome - Main</title> </head> 

Notice how the second parameter of the fragment (link) is set to an empty fragment, and therefore nothing is written to the block <th: block th: replace = "$ {links}" />:

 <head> <title>Awesome - Main</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css"> <link rel="shortcut icon" href="/awe/images/favicon.ico"> <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script> </head> 

Using "non-surgical" token

A non-operational token can also be used as a parameter for a fragment, if we just want our fragment to use its current markup as the default value. Again, using the common_header example:

 <head th:replace="base :: common_header(_,~{::link})"> <title>Awesome - Main</title> <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"> <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}"> </head> 

See how the title argument (the first argument of the common_header fragment) is set to no-op (_) , which causes this part of the fragment to not execute at all (title = no-operation):

 <title th:replace="${title}">The awesome application</title> 

The result is as follows:

 <head> <title>The awesome application</title> <!-- Common styles and scripts --> <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css"> <link rel="shortcut icon" href="/awe/images/favicon.ico"> <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script> <link rel="stylesheet" href="/awe/css/bootstrap.min.css"> <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css"> </head> 

Advanced fragment insertion conditions

The availability of both the emtpy fragment and the no-operation fragment allows us to conditionally insert fragments in a very simple and elegant way.

For example, we could do this in order to insert our “common :: adminhead” fragment only if the user is an administrator, and not insert anything (emtpy fragment) if not:

 <div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div> 

In addition, we can use the " no-operation " token to insert a fragment only if the specified condition is met, but leave the markup unchanged if the condition is not met:

 <div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _"> Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support. </div> 

In addition, if we set up our template resolvers to check for the availability of template resources — using the checkExistence flag — we can use the existence of the fragment itself as a condition in the default operation:

 <!-- The body of the <div> will be used if the "common :: salutation" fragment --> <!-- does not exist (or is empty) --> <div th:insert="~{common :: salutation} ?: _"> Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support. </div> 

8.4 Deleting template fragments


Returning to the sample application, let's revise the latest version of the product list template:

 <table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> </table> 

This code is simply good as a template, but as a static page (when directly opened by the browser without processing Thymeleaf), it will not become a good prototype.

Why? Since, although it is perfectly displayed in browsers, this table has only a row, and in this row is the layout of the data. As a prototype, it simply would not look realistic ... we should have more than one product, we need more lines.

So, add some:

 <table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> <tr class="odd"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </table> 

Ok, now we have three, definitely better for the prototype. But ... what happens when processing this template with Thymeleaf ?:

 <table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr> <td>Fresh Sweet Basil</td> <td>4.99</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Italian Tomato</td> <td>1.25</td> <td>no</td> <td> <span>2</span> comment/s <a href="/gtvg/product/comments?prodId=2">view</a> </td> </tr> <tr> <td>Yellow Bell Pepper</td> <td>2.50</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Old Cheddar</td> <td>18.75</td> <td>yes</td> <td> <span>1</span> comment/s <a href="/gtvg/product/comments?prodId=4">view</a> </td> </tr> <tr class="odd"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </table> 

The last two lines are frozen lines! Well, of course, those: iteration applies only to the first row, so there is no reason why Thymeleaf should have removed the other two.

We need a way to remove these two lines during template processing. Let's use the th: remove attribute in the second and third <tr> tags:

 <table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> <tr class="odd" th:remove="all"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr th:remove="all"> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </table> 

After processing, everything will look as it should:

 <table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> <tr> <td>Fresh Sweet Basil</td> <td>4.99</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Italian Tomato</td> <td>1.25</td> <td>no</td> <td> <span>2</span> comment/s <a href="/gtvg/product/comments?prodId=2">view</a> </td> </tr> <tr> <td>Yellow Bell Pepper</td> <td>2.50</td> <td>yes</td> <td> <span>0</span> comment/s </td> </tr> <tr class="odd"> <td>Old Cheddar</td> <td>18.75</td> <td>yes</td> <td> <span>1</span> comment/s <a href="/gtvg/product/comments?prodId=4">view</a> </td> </tr> </table> 

And what do all these attributes mean? th: remove may behave differently, depending on its meaning:

all : remove both the tag containing the tag and all its children.
body : Do not remove the containing tag, but remove all its child elements.
tag : remove the containing tag, but do not delete its children.
all-but-first : remove all child elements of the containing tag except the first.
none : Do nothing. This value is useful for dynamic evaluation.

When is it useful to use all-but-first ? This will allow us to save some th: remove = "all" when prototyping:

 <table> <thead> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> <th>COMMENTS</th> </tr> </thead> <tbody th:remove="all-but-first"> <tr th:each="prod : ${prods}" th:class="${prodStat.odd}? 'odd'"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> <td> <span th:text="${#lists.size(prod.comments)}">2</span> comment/s <a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a> </td> </tr> <tr class="odd"> <td>Blue Lettuce</td> <td>9.55</td> <td>no</td> <td> <span>0</span> comment/s </td> </tr> <tr> <td>Mild Cinnamon</td> <td>1.99</td> <td>yes</td> <td> <span>3</span> comment/s <a href="comments.html">view</a> </td> </tr> </tbody> </table> 

The th: remove attribute can take any standard Thymeleaf expression if it returns one of the valid String values ​​(all, tag, body, all-but-first, or none).

This means that the deletion may be conditional, for example:

 <a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a> 

Also note that th: remove considers null a synonym for none , so the following works just like the example above:

 <a href="/something" th:remove="${condition}? tag">Link text not to be removed</a> 

In this case, if $ {condition} is false, null will be returned, and therefore the removal will not be performed.

Source: https://habr.com/ru/post/351844/


All Articles