[MAJOR FEAT] Fragment Expressions
See original GitHub issueBuilding on the idea explained at #180, Thymeleaf 3.0 will introduce a new type of simple expressions for the Thymeleaf Standard Expression engine. These expressions are called Fragment Expressions and their main idea is that they generalize the syntax that was already being used in th:insert
/th:replace
/th:include
in order to be able to use resolved fragments as any other kind of objects in the template execution context.
Their syntax is similar to that of other simple expressions, using the tilde (~
) as a selector char. So expressions such as these:
~{commons::main}
…will now not only be usable in th:insert
/th:replace
/th:include
like:
<div th:insert="~{commons :: main}">...</div>
…but also as a part of other more complex expressions like:
<div th:insert="${user.admin}? ~{adm :: admintools} : ~{common :: basictools}">...</div>
…or even:
<div th:with="frag=~{footer :: #main/text()}">
<p th:insert="${frag}">
</div>
All features available before Thymeleaf 3.0 in fragment insertion syntax are also available in fragment expressions, including:
- Same-template fragments:
~{::frag}
,~{this::frag}
, … - Complete-template fragments:
~{main}
,~{usermain}
, … - Variable-based resolution,
~{${templateName} :: ${fragmentName}}
- Fragment parameters, both named and unnamed, like
~{common::details(${user.name})
,~{::main(title=#{corp.title})}
Besides, Thymeleaf 3.0 fully embraces the new Markup Selector syntax provided by AttoParser 2.0, which builds on the previously existing syntax and largely extends it, providing new selection capabilities (see below).
Fragments as parameters (layout)
Now that fragments (objects of class org.thymeleaf.standard.expression.Fragment
) can be put into context just as any other variables, we can use them as parameters to another fragments, effectively turning fragment expressions into a complete layout mechanism.
For example, we might develop a common template called base.html
, containing the <head>
tag we want to use for most of our application’s pages, containing all the style and script imports we will need but allowing the specification of a page title and some additional <link>
s:
<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>
Then we could have a main.html
template making use of the fragment above:
...
<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>
...
Note how those ~{::title}
and ~{::links}
parameters we are specifying in our th:replace
will effectively pass parts of the original template markup (main.html
) on to the inserted fragment (base.html
), where these parts can be themselves inserted into specific placeholders. So the end result will look like:
...
<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>
...
Compatibility with fragment insertion in older versions
Even if the new fragment expressions bring a lot of flexibility to fragment insertion operations in Thymeleaf, this does not mean that old th:include
or th:replace
attributes need to be changed. So even if Thymeleaf 3.0 recommends using specifying fragment insertion like this:
<div th:replace="~{commons :: main}">...</div>
…this does not mean that the previous, unwrapped way of expressing fragments doesn’t work anymore. So this will still work perfectly:
<div th:replace="commons :: main">...</div>
Writing fragments as text, not as… fragments
A new possibility that fragment expressions bring is using fragment expressions outside fragment insertion attributes (th:insert
/th:replace
/th:include
). Specifically, they can be useful in th:text
and th:utext
attributes:
<span th:utext="~{::title/text()}">...</span>
Note there are important differences between th:insert
, th:text
and th:utext
when used with a fragment expression:
th:insert
will process the inserted fragment (i.e. anyth:*
attributes in the fragment will get processed) while the others won’t.th:text
will escape the inserted fragment (after converting it to aString
if needed) while the others won’t.th:utext
will therefore be almost equivalent toth:insert
, but nothing inside the fragment will ever get processed.
So we can revisit the example above and rewrite the title of our base.html
template, using a more elegant th:utext
instead of the previous th:replace
:
<head th:fragment="common_header(title,links)">
<title th:utext="${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>
But note this will input the <title>
tag coming from main.html
inside the <title>
tag in base.html
, so we need to modify the expression by which we call the fragment from main.html
in order to include only the title’s text body (/text()
):
...
<head th:replace="base :: common_header(~{::title/text()},~{::link})">
...
</head>
...
Fragment existence
Fragment expressions do in general check the existence of the fragment they represent. But they do this in two different ways depending on when they are used:
- For fragment expressions used directly (i.e. not as a part of a complex expression) in attributes such as
th:insert
/th:replace
/th:include
/th:text
/th:utext
/… these expressions will raise an exception if the resolved fragment does not exist. This includes both the fragment’s template not existing and also the specified fragment not existing in an existing template. - For fragment expressions used as a part of complex expressions (conditionals, defaults, etc.) or also as parameters of other fragment expressions, these expressions will simply evaluate to
null
whenever the fragment or its container template do no exist. Note that this nullity means that fragment expressions used as the condition of a conditional expression will evaluate tofalse
.
Let’s see some examples. These will raise an exception if template or fragment do not exist:
<div th:insert="~{commons :: main}">...</div>
<div th:insert="commons :: main">...</div>
<div th:text="~{commons :: main}">...</div>
However, when used as a fragment expression parameter, it will simply evaluate to null
:
<!-- Call the fragment with a "title" parameter that does not exist: title=null -->
<div th:insert="~{commons :: header(title=~{::#maintitle})}">...</div>
...
<!-- Once "title" is null th:utext's behaviour will be to write nothing -->
<span th:utext="${title}">...</span>
...
<!-- But note that, with the same null, th:insert's behaviour will be to fail -->
<span th:insert="${title}">...</span>
Also, when used in a conditional or default expression the fragment will evaluate to false
(because null
is false
in conditionals). Let’s see a quite elaborate expression inserting a base_{TYPE}
fragment depending on the type of user, but knowing that not all types actually exist in our template:
<div th:insert="~{commons :: |#base_${user.type}|} ?: ~{commons :: #default}">...</div>
The EMPTY fragment
If a nonexisting fragment will either fail or just return null, how can we make a fragment insertion expression insert nothing? In such case we can use the empty fragment expression: ~{}
.
So we can modify the expression above to specify that, if no base for the user type exists, we should simply insert nothing:
<div th:insert="~{commons :: |#base_${user.type}|} ?: ~{}">...</div>
Note that inserting nothing is not the same as doing nothing. In this case, the ...
in the body of the <div>
tag will actually get removed and the result will be:
<div></div>
With this, we can revisit our header template and take into account the possibility that the calling template might not have any <link>
s to add:
<head th:fragment="common_header(title,links)">
<title th:utext="${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>
The new fragment syntax
Fragment expressions in Thymeleaf 3.0 allow the use of the powerful markup selector syntax provided by AttoParser 2.0, Thymeleaf’s new parsing library. This syntax builds on (and importantly extends) the fragment specification syntax available in Thymeleaf version 2.1, so that backwards compatibility is ensured.
For more detail on this syntax, read AttoParser’s documentation for this syntax at http://www.attoparser.org/apidocs/attoparser/2.0.0.BETA3/org/attoparser/select/package-summary.html
As a Thymeleaf-specific adaptation, note that what AttoParser generically calls reference-based matching is applied by Thymeleaf by considering the th:fragment
and th:ref
attributes as references. So the %whatever
and whatever
expressions will match any tags with th:fragment="whatever"
or th:ref="whatever"
(see #196 on th:ref
).
Some very quick examples:
whatever
or//whatever
: match any<whatever>
tags, or anywhatever
references (th:fragment="whatever"
,th:ref="whatever"
) at any level in the markup hierarchy./whatever
: same as above, but only at the root level of the markup hierarchy.foo/whatever
: same as above, but only as a direct child of an element/reference calledfoo
.foo//whatever
: same as above, but at any hierarchy level inside an element/reference calledfoo
..whatever
: any tags withclass="whatever"
.div.whatever
: any<div>
tags withclass="whatever"
.#whatever
: any tags withid="whatever"
.div#whatever
: any<div>
tags withid="whatever"
.div#whatever/text()
: any text nodes contained as direct children of a<div class="whatever">
.input[type='text']: any
tags with
type=“text”`.- …
Issue Analytics
- State:
- Created 8 years ago
- Reactions:10
- Comments:17 (4 by maintainers)
Top GitHub Comments
Will preprocessing still be possible, e.g.
I am asking because right now I get the deprecation warning (thymeleaf:3.0.12-SNAPSHOT)
hi, I’m from china,i’m learning thymeleaf ,but when i use thyemleaf3.0 layout examples,it always throws exception,hope 4 solution. In my project(spring boot1.4,spring-boot-starter-thymeleaf) template files like this:
all content in header.html (copy from http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-layout)
and content in create.html
when i visit http://localhost/strategy/create,then it throws exception: