Java 前端模板引擎学习:thymeleaf 模板引擎
模板引擎接口 ITemplateEngine
一、后台数据与外部数据
1.处理后台数据
$表达式是个变量表达式,用于处理在 request parameters and the request, session and application 中的变量
${x}
will return a variablex
stored into the Thymeleaf context or as a request attribute.${param.x}
will return a request parameter calledx
(which might be multivalued).${session.x}
will return a session attribute calledx
.${application.x}
will return a servlet context attribute calledx
.
<input type="text" name="userName" value="James Carrot" th:value="${user.name}" />
2.处理外部数据
外部化的片段通常叫作 messages,#表达式用于处理这类消息。外部消息可以从数据库中获取,或从 .properties
files 中获取,这取决于 StandardMessageResolver 的实现。Thymeleaf 的默认实现为 StandardMessageResolver。
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
为了实现属性名的 i18n,消息解析器 StandardMessageResolver 将/WEB-INF/templates/home.html
映射于同文件夹同名文 propreties 上,比如
/WEB-INF/templates/home_en.properties
for English texts./WEB-INF/templates/home_es.properties
for Spanish language texts./WEB-INF/templates/home_pt_BR.properties
for Portuguese (Brazil) language texts./WEB-INF/templates/home.properties
for default texts (if the locale is not matched).
注意在 spring boot 中,国际化消息直接映射到资源目录下,如 /i18n/ messages.properties 或 /i18n/ messages _en.properties
二、文本数据处理
1.不转义
如果 $-expression 获取的值包含 < >等,默认会对其转义为 < >
比如:
home.welcome=Welcome to our <b>fantastic</b> grocery store!
使用 th:text ,默认转义成返回
<p th:text="#{home.welcome}">Welcome to our grocery store!</p> <p>Welcome to our <b>fantastic</b> grocery store!</p>
使用 th:utext 返回
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p> <p>Welcome to our <b>fantastic</b> grocery store!</p>
三、# 表达式
#{...} expressions 外部消息常用于实现国际化
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p> i18n:home.welcome=¡Bienvenido a nuestra tienda de comestibles!
你可以指定 #-expression 的 key 从 $-表达式获取:
<p th:utext="#{${welcomeMsgKey}"> Welcome to our grocery store, Sebastian Pepper! </p>
#-expression 中夹带 变量进行结合输出,#{} 中使用 () 包裹传入的变量,多个变量使用 , 分隔
home.welcome=¡Bienvenido a nuestra tienda de comestibles, {0}! <p th:utext="#{home.welcome(${session.user.name})}"> Welcome to our grocery store, Sebastian Pepper! </p>
* 特殊情况
模板连接变量,形成消息
<td th:text="#{|seedstarter.type.${sb.type}|}">Wireframe</td>
消息变成变量,预处理
<p th:text="${__#{article.text('textVar')}__}">Some text here...</p>
四、$ 表达式
${...}
expressions 用于操作变量,基于 OGNL表达式 (在 springmvc 中使用 spel 代替 ognl),对其相应的 context中的 变量进行映射
为了更灵活地使用 ognl,也包含了 7 个预置对象,
#ctx
: the context object.#vars:
the context variables.#locale
: the context locale.#request
: (only in Web Contexts) theHttpServletRequest
object.#response
: (only in Web Contexts) theHttpServletResponse
object.#session
: (only in Web Contexts) theHttpSession
object.#servletContext
: (only in Web Contexts) theServletContext
object.
以 # 号开始以引用这些预置对象。示例:
Established locale country: <span th:text="${#locale.country}">US</span>.
* 内置对象的使用技巧:
访问 param, session, application 属性时,不需要加 #
// 直接访问对象域的属性 ${param.foo} // Retrieves a String[] with the values of request parameter 'foo' ${param.number[0]} // Retrieves a String with the values of request parameter 'number', ${param.size()} ${param.isEmpty()} ${param.containsKey('foo')} ${session.foo} // 获取 session 中的属性 ${session.size()} ${session.isEmpty()} ${session.containsKey('foo')} ${application.foo} // Retrieves the ServletContext atttribute 'foo' ${application.size()} ${application.isEmpty()} ${application.containsKey('foo')} # 直接访问 javax.servlet.http.HttpSession 对象 ${#session.getAttribute('foo')} ${#session.id} ${#session.lastAccessedTime}
v注意:返回的是一个字符串数组:
除了 7 个预置顶级对象,还有 16 个工具对象:
#execInfo
: information about the template being processed.#messages
: methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.#uris
: methods for escaping parts of URLs/URIs#conversions
: methods for executing the configured conversion service (if any).#dates
: methods forjava.util.Date
objects: formatting, component extraction, etc.#calendars
: analogous to#dates
, but forjava.util.Calendar
objects.#numbers
: methods for formatting numeric objects.#strings
: methods forString
objects: contains, startsWith, prepending/appending, etc.#objects
: methods for objects in general.#bools
: methods for boolean evaluation.#arrays
: methods for arrays.#lists
: methods for lists.#sets
: methods for sets.#maps
: methods for maps.#aggregates
: methods for creating aggregates on arrays or collections.#ids
: methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
工具对象的使用,请查看原文。 常用工具类对象的操作方法:
# 格式化数字 ${#numbers.formatDecimal(num,3,2)} // 整数位显示位数与小数精确位数 ${#numbers.formatCurrency(num)} // 货币 ${#numbers.formatPercent(num, 3, 2)} // 百分号 # 格式化日期 ${#dates.second(date)} ${#dates.createNow()} ${#dates.create(year,month,day)} ${#dates.format(date, 'dd/MMM/yyyy HH:mm')} # 操作字符串 ${#strings.toString(obj)} //打印对象 ${#strings.trim(str)} //去除首尾空白 ${#strings.containsIgnoreCase(name,'ez')} // 判断是否包含 ${#strings.startsWith(name,'Don')} // also array*, list* and set* ${#strings.endsWith(name,endingFragment)} // 判断前缀与后缀 ${#strings.abbreviate(str,10)} // 大于此位数内容,以省略号 ...代替 ${#strings.indexOf(name,frag)} ${#strings.substring(name,3,5)} ${#strings.substringAfter(name,prefix)} ${#strings.substringBefore(name,suffix)} // 截取字符串 ${#strings.replace(name,'las','ler')} // 替换字符串 ${#strings.arrayJoin(namesArray,',')} ${#strings.listJoin(namesList,',')} ${#strings.setJoin(namesSet,',')} ${#strings.toUpperCase(name)} // also array*, list* and set* ${#strings.toLowerCase(name)} ${#strings.capitalizeWords(str)} //首字母大写 ${#strings.arraySplit(namesStr,',')} // 返回 String[],用于遍历 # 聚集函数 ${#aggregates.sum(array)} // 求何 ${#aggregates.avg(array)} // 平均数 #随机数 ${#strings.randomAlphanumeric(count)} #遍历数字的工具 ${#numbers.sequence(from,to)} ${#numbers.sequence(from,to,step)} # 类型转换 ${#conversions.convert(object, 'java.util.TimeZone')} #转义字符串,防止JavaScript or JSON 注入 ${#strings.escapeXml(str)} ${#strings.escapeJava(str)} ${#strings.escapeJavaScript(str)} ${#strings.unescapeJava(str)} ${#strings.unescapeJavaScript(str)}
五、* 表达式
当对象被选择(使用 th:object )后,可以使用 *{} 获取对象属性
<div th:object="${session.user}"> <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p> <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p> <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p> </div>
下面几处用法效果等同
<div th:object="${session.user}"> <p>Name: <span th:text="${#object.firstName}">Sebastian</span>.</p> <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p> <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p> </div>
此外,如果没有对象被选择,则 *-expression 与 $-expression 完全相当
<div> <p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p> <p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p> <p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p> </div>
六、A 标签处理
1.标签 th:href 与 @{} expressions
<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) --> <a href="details.html" th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a> <!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) --> <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a> <!-- Will produce '/gtvg/order/3/details' (plus rewriting) --> <a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
* 为什么使用 th:href="@{}"
使用模板引擎管理 link 地址,方便对链接进行额外处理(如版本处理之类)
2.注意不可变部分,需要作预处理
<a href="#" th:href="@{__${#request.requestURI}__/learn}">Learn</a>
七、一些特殊的表达式语法
① 模板字符串代替 + 连接
<span th:text="'Welcome to our application, ' + ${user.name} + '!'"> <span th:text="|Welcome to our application, ${user.name}!|">
下文 https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#expressions-on-selections-asterisk-syntax
②预编译(预处理)
<tr th:each="book, itemStat : *{books}"> <td><input th:field="*{books[__${itemStat.index}__].title}" /></td> <td><input th:field="*{books[__${itemStat.index}__].author}" /></td> </tr>
也可以拆开写成 th:name 与 th:value (不推荐)
<tr th:each="book, itemStat : ${form.books}"> <td><input hidden th:name="|books[${itemStat.index}].id|" th:value="${book.getId()}"/></td> <td><input th:name="|books[${itemStat.index}].title|" th:value="${book.getTitle()}"/></td> <td><input th:name="|books[${itemStat.index}].author|" th:value="${book.getAuthor()}"/></td> </tr>
③ 代数运算,可以使用 div
(/
), mod
(%
). 等
<div th:with="isEven=(${prodStat.count} % 2 == 0)"> <div th:with="isEven=${prodStat.count % 2 == 0}">
④ 比较运算,可以使用 gt
(>
), lt
(<
), ge
(>=
), le
(<=
), not
(!
). Also eq
(==
), neq
/ne
(!=
)
<div th:if="${prodStat.count} > 1"> <span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">
⑤ 条件表达式,其中 else 部分也可以忽略不写,则当为否时返回 null
<tr th:class="${row.even}? 'even' : 'odd'"> ... </tr> <tr th:class="${row.even}? 'alt'"> ... </tr>
⑥ 默认值表达式,当变量没有赋任何值时,使用该默认值
<div th:object="${session.user}"> ... <p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p> </div>
也可以在子表达式中,效果类似于:
<p> Name: <span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span> </p>
⑦ 无操作符号 no-operation token ,即使用当前文字节点
<span th:text="${user.name} ?: _">no user authenticated</span>
相当于
<span th:text="${user.name} ?: 'no user authenticated'">...</span>
⑧ 自动应用转换服务
服务端配置文件 —— application.yml (这种配置只在页面格式化管用,在向 controller 传参时并不管用)
spring: mvc: date-format: yyyy-mm-dd
或 JavaCofig (页面和传参都管用)
@Override public void addFormatters(final FormatterRegistry registry) { super.addFormatters(registry); registry.addFormatter(varietyFormatter()); registry.addFormatter(dateFormatter()); } @Bean public VarietyFormatter varietyFormatter() { return new VarietyFormatter(); } @Bean public DateFormatter dateFormatter() { return new DateFormatter(); }
页面使用 双括号 {{ 与 }}
<td th:text="${{sb.datePlanted}}">13/01/2011</td>
⑨ 静态方法调用与 Bean 方法调用
将字符串转换为 Integer
T(Integer).parseInt(param.pagenum)
或者引用自定义工具类
th:text="${T(com.package.SUtils).reverseString('hello')}"
使用 @引用 Bean
<span th:text="${@sUtils.reverseString('hello')}"></span>
⑩ 对集合数组进行 map 与 filter 处理
- selection means filtering each element of the collection using its properties. The result of such an operation is a subset of the initial collection, only matching elements are retained
.
- projection means filtering the property of each element in the collection. The result of a projection operation is a a new collection, with the same number of elements than the original one, but containing only the filtered property of the elements, not the whole element object itself
示例:
map ( projection )
<tr th:each="artist,rowStat : ${listArtits.![firstname+' '+lastname]}"> <td class="center middle" th:text="${rowStat.count}">1</td> <td class="center middle" th:text="${artist}">Michael Jackson</td> </tr>
filter ( selection )
<tr th:each="artist,rowStat : ${listArtits.?[alive == true]}"> <td class="center middle" th:text="${rowStat.count}">1</td> <td class="center middle" th:text="${artist.name}">Michael Jackson</td> <td class="center middle" th:text="${artist.discography}">Got to Be There, Ben, Music & Me, Forever Michael...</td> <td class="center middle" th:text="${artist.bio}">Michael Joseph Jackson (August 29, 1958 - June 25, 2009) was an American recording artist, entertainer, and businessman...</td> </tr>
八、操作标签的属性值
th:attr
<form action="subscribe.html" th:attr="action=@{/subscribe}"> <fieldset> <input type="text" name="email" /> <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/> </fieldset> </form>
多个属性用逗号分开
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" /> <img src="/gtgv/images/gtvglogo.png" title="Logo de Good Thymes" alt="Logo de Good Thymes" />
或者属性联写
<img src="../../images/gtvglogo.png" th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" />
也可以直接使用 th:value 、 th:src、th:action 特殊属性(见 setting-value-to-specific-attributes )
<img src="../../images/gtvglogo.png" th:src="@{/images/gtvglogo.png}" th:title="#{logo}" th:alt="#{logo}" /> or <img src="../../images/gtvglogo.png" th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" />
增加属性值(Appending and prepending)
<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" />
如果 cssStyle 是 warning 的话,则 class 会增加一个值
<input type="button" value="Do it!" class="btn warning" />
也可是使用更简便的 th:classappend
<tr th:each="prod : ${prods}" class="row" th:classappend="${prodStat.odd}? 'odd'">
九、遍历迭代
普通的迭代
<tr th:each="prod : ${prods}"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> </tr> <tr th:each="prod : ${prods}"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td> </tr>
指定迭代状态量 th:each="prod,iterStat : ${prods}"
<table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</th> </tr> <tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.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> </tr> </table>
注意状态的几种属性
index: the current iteration index, starting with 0 (zero) count: the number of elements processed so far size: the total number of elements in the list even/odd: checks if the current iteration index is even or odd first: checks if the current iteration is the first one last: checks if the current iteration is the last one
* 如果不手动设定,可以使用默认实现的 状态变量后缀Stat,比如这里的 prodStat
<table> <tr> <th>NAME</th> <th>PRICE</th> <th>IN STOCK</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> </tr> </table>
* 如果遍历的内容需要更多 tr标签,可以使用 th:block,比如:
<table> <th:block th:each="user : ${users}"> <tr> <td th:text="${user.login}">...</td> <td th:text="${user.name}">...</td> </tr> <tr> <td colspan="2" th:text="${user.address}">...</td> </tr> </th:block> </table>
渲染结果
<table> <!--/*/ <th:block th:each="user : ${users}"> /*/--> <tr> <td th:text="${user.login}">...</td> <td th:text="${user.name}">...</td> </tr> <tr> <td colspan="2" th:text="${user.address}">...</td> </tr> <!--/*/ </th:block> /*/--> </table>
十、判断语句
th:if
- If value is a boolean and is
true
. - If value is a number and is non-zero
- If value is a character and is non-zero
- If value is a String and is not “false”, “off” or “no”
- If value is not a boolean, a number, a character or a String.
<a href="comments.html" th:href="@{/product/comments(prodId=${prod.id})}" th:if="${not #lists.isEmpty(prod.comments)}">view</a>
相反 th:unless
<a href="comments.html" th:href="@{/comments(prodId=${prod.id})}" th:unless="${#lists.isEmpty(prod.comments)}">view</a>
或者,使用 switch-case 语句
<div th:switch="${user.role}"> <p th:case="'admin'">User is an administrator</p> <p th:case="#{roles.manager}">User is a manager</p> <p th:case="*">User is some other thing</p> </div>
十一、模板布局
1.基本用法
使用 th:fragment 定义 partial 模板
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <body> <div th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </div> </body> </html>
使用 th:insert 或 th:replace 插入模板(th:include 也可以,但在 3.0 后并不建议使用)
<body> ... <div th:insert="footer :: copy"></div> </body>
或者也可以给引号部分加一个 ~{} ,将器封装起来
<body> ... <div th:insert="~{footer :: copy}"></div> </body>
2. th:insert 、th:replace 与 th:include 区别
定义模板
<footer th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery </footer>
使用 div 标签引用模板
<body> ... <div th:insert="footer :: copy"></div> <div th:replace="footer :: copy"></div> <div th:include="footer :: copy"></div> </body>
分别渲染出的结果:
<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>
得出结论:
- th:insert 包括自身标签和子标签,
- th:replace 包括子标签但不包括自身标签,
- th:include 则是包括自身标签不包括子标签。
3.没有 th:fragment
定义模板
... <div id="copy-section"> © 2011 The Good Thymes Virtual Grocery </div> ...
使用 id 属性引用该模板
<body> ... <div th:insert="~{footer :: #copy-section}"></div> </body>
4.模板中传递参数
① 普通变量
定义模板
<div th:fragment="frag (onevar,twovar)"> <p th:text="${onevar} + ' - ' + ${twovar}">...</p> </div>
调用模板,并传递相应的变量(注意这里调用的是当前模板文件中的 fragment,因此可以省略写成 th:replace=":: 函数名称 "
<div th:replace="::frag (${value1},${value2})">...</div>
② 局部变量
定义模板
<div th:fragment="frag"> ... </div>
传递局部变量
<div th:replace="::frag (onevar=${value1},twovar=${value2})">
上面的表达式,相当于
<div th:insert="::frag" th:with="onevar=${value1},twovar=${value2}">
5.常见的技巧
① head中引用 title 与 link 块
定义模板
<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>
调用模板(注意这里调用的模板文件名称是 base
... <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
渲染结果
... <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> ...
* 传递到模板的参数,除了使用 ${} 符号引入变量,或者使用 ~{:: 嵌入当前的标签,还可以使用 no-operation token 或者 参数留空。比如:
无操作符号 no-operation token
... <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> ...
参数留空
<head th:replace="base :: common_header(~{::title},~{})"> <title>Awesome - Main</title> </head> ...
② 条件调用模板
根据条件决定是否调用模板
... <div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div> ...
或者条件不满足,调用默认模板
... <div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _"> Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support. </div> ...
③ 隐藏或移除 mock 数据(感觉跟 fragment 没多大关系)
通常在我们会定义一些 mock 数据,用来做静态网页测试,查看渲染效果。像下面这样后两段 <tr> 作为 mock 数据。
<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>
由于使用 th:remove 标记,该内容将在服务端渲染时被移除。
移除的形式有很多种:
all
: Remove both the containing tag and all its children.body
: Do not remove the containing tag, but remove all its children.tag
: Remove the containing tag, but do not remove its children.all-but-first
: Remove all children of the containing tag except the first one.none
: Do nothing. This value is useful for dynamic evaluation.
鉴于此,在 tbody 标签上使用 all-but-first
<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>
* 根据条件移除
<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>
④ 单独文件作为 partial 模板
定义模板
<!DOCTYPE html> <html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org"> <head> <title th:replace="${title}">Layout Title</title> </head> <body> <h1>Layout H1</h1> <div th:replace="${content}"> <p>Layout content</p> </div> <footer> Layout footer </footer> </body> </html>
调用模板,并传入相应的参数
<!DOCTYPE html> <html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}"> <head> <title>Page Title</title> </head> <body> <section> <p>Page content</p> <div>Included on page</div> </section> </body> </html>
十二、局部变量
定义一个或多个局部变量,并使用它们
<div th:with="firstPer=${persons[0]},secondPer=${persons[1]}"> <p> The name of the first person is <span th:text="${firstPer.name}">Julius Caesar</span>. </p> <p> But the name of the second person is <span th:text="${secondPer.name}">Marcus Antonius</span>. </p> </div>
一个实用的例子,比如根据 i18n 文件读取格式化方式,来格式化日期,比如:
home_en.properties
date.format=MMMM dd'','' yyyy
home_es.properties
date.format=dd ''de'' MMMM'','' yyyy
定义局部变量,并引用它
<p th:with="df=#{date.format}"> Today is: <span th:text="${#calendars.format(today,df)}">13 February 2011</span> </p>
或者
<p> Today is: <span th:with="df=#{date.format}" th:text="${#calendars.format(today,df)}">13 February 2011</span> </p>
十三、特殊页面处理
1. 分页页面
2. 表单页面
① 基本用法
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post"> <input type="text" th:field="*{datePlanted}" /> ... </form>
checkbox 处理(需要传两个对象,一个是全部属性 allFeatures,一个是当前选择的 features)
<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>
radio 处理
<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>
dropdown 处理
<select th:field="*{type}"> <option th:each="type : ${allTypes}" th:value="${type}" th:text="#{${'seedstarter.type.' + type}}">Wireframe</option> </select>
② 动态 field:
需求: 当 command 对象有一个 collection property,对这个属性进行遍历,需要生成不同的 field
示例:
创建书籍(显示页面)
@GetMapping("/create") public String showCreateForm(Model model) { BooksCreationDto booksForm = new BooksCreationDto(); for (int i = 1; i <= 3; i++) { booksForm.addBook(new Book()); } model.addAttribute("form", booksForm); return "books/createBooksForm"; }
表单页面
<form action="#" th:action="@{/books/save}" th:object="${form}" method="post"> <fieldset> <input type="submit" id="submitButton" th:value="Save"> <input type="reset" id="resetButton" name="reset" th:value="Reset"/> <table> <thead> <tr> <th> Title</th> <th> Author</th> </tr> </thead> <tbody> <tr th:each="book, itemStat : *{books}"> <td><input th:field="*{books[__${itemStat.index}__].title}" /></td> <td><input th:field="*{books[__${itemStat.index}__].author}" /></td> </tr> </tbody> </table> </fieldset> </form>
* 说明:
上面的模板通过 * 表达式选择该 集合属性,然后使用__ 预处理__表达式生成不同的 field(即带有数组样式的 id 与 name)渲染结果类似
<input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value="" />
添加书籍(提交页面)
@PostMapping("/save") public String saveBooks(@ModelAttribute BooksCreationDto form, Model model) { bookService.saveAll(form.getBooks()); model.addAttribute("books", bookService.findAll()); return "redirect:/books/all"; }
③ 表单验证(* 关于 controller 部分请参考文末参考链接)
判断错误是否存在
<input type="text" th:field="*{datePlanted}" th:class="${#fields.hasErrors('datePlanted')}? fieldError" />
或者直接使用 th:errorclass
<input type="text" th:field="*{datePlanted}" class="small" th:errorclass="fieldError" />
遍历该 field 的错误
<li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" />
直接输出该 field 的错误
<p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>
获取全部 field 错误
<div th:if="${#fields.hasAnyErrors()}"> <p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</p> </div>
注意:
#fields.hasErrors('*') <=> #fields.hasAnyErrors() #fields.errors('*') <=> #fields.allErrors()
或通过 detailedErrors 详细遍历
<ul> <li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr"> <span th:text="${e.global}? '*' : ${e.fieldName}">The field name</span> | <span th:text="${e.message}">The error message</span> </li> </ul>
233
参考链接
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#integrating-thymeleaf-with-spring