一、创建表单
1、Thymeleaf要求您使用标记中的th:object属性指定命令对象
<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
...
</form>
这与th:object的其他用法一致,但实际上这个特定的场景增加了一些限制,以便正确地与Spring MVC的基础架构集成:
- 表单标记中的th:object属性的值必须是变量表达式($ {...}),仅指定模型属性的名称,不带属性导航。这意味着像$ {seedStarter}这样的表达式是有效的,但$ {seedStarter.data}不会。
- 进入标记后,不能指定其他th:object属性。这与HTML表单无法嵌套这一事实是一致的。
2、Inputs现在让我们看看如何在表单中添加输入
<input type="text" th:field="*{datePlanted}" />
如您所见,我们在这里引入了一个新属性:th:field。这是Spring MVC集成的一个非常重要的特性,因为它完成了将输入与表单支持bean中的属性绑定的所有繁重工作。您可以将其视为Spring MVC的JSP标记库中的标记中的path属性的等价物。
th:field属性的行为有所不同,具体取决于它是否附加到<input><select>or <textarea>标签并且还取决于具体的<input>类型,在这种情况下(input[type=text]),上面的代码类似于
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />
...但事实上它比这更多一点,因为th:field也将应用已注册的Spring转换服务,包括我们之前看到的DateFormatter(即使字段表达式不是双括号)。多亏了这一点,日期将正确显示格式。
th:field属性的值必须是选择表达式(* {...}),这是有意义的,因为它们将在表单支持bean上进行评估,而不是在上下文变量(或Spring MVC术语中的模型属性)上进行评估)。
与th:object中的对象相反,这些表达式可以包含属性导航(实际上,此处允许使用 JSP标记的path属性允许的任何表达式)。
th:field也支持html5的新input类型,像<input type="datetime".../>,<input type="color".../>等,有效地为Spring MVC添加完整的HTML5支持。
3、复选框字段(Checkbox fields)
th:field还允许我们定义复选框输入。我们从HTML页面看一个例子:
<div>
<label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">Covered</label>
<input type="checkbox" th:field="*{covered}" />
</div>
注意除了复选框本身之外还有一些很好的东西,比如外化标签,还使用#ids.next('covered')函数来获取将应用于复选框输入的id属性的值。
为什么我们需要为此字段动态生成id属性?因为复选框可能是多值的,因此它们的id值将始终以序列号为后缀(通过内部使用#ids.seq(...)函数),以确保相同属性的每个复选框输入具有不同的id值。
如果我们查看这样一个多值复选框字段,我们可以更容易地看到这一点:
<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>
请注意,这次我们添加了一个th:value属性,因为features字段不是像覆盖的那样的布尔值,而是一个值数组。让我们看看这段代码生成的HTML输出:
<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>
我们在这里可以看到如何将序列后缀添加到每个输入的id属性,以及#ids.prev(...)函数如何允许我们检索为特定输入id生成的最后一个序列值。
不要担心名称=“_ features”的隐藏输入:它们会自动添加,以避免浏览器在提交表单时不向服务器发送未经检查的复选框值的问题。
另请注意,如果我们的features属性在我们的表单支持bean中包含一些选定的值,那么:field将会处理这个并且会在相应的输入标记中添加checked =“checked”属性。
4、单选按钮字段(Radio Button fields)
单选按钮字段的指定方式与非布尔(多值)复选框类似 - 当然它们不是多值的:
<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>
5、下拉列表/列表选择器(Dropdown/List selectors)
下拉列表包含两部分,<select>和<option>标签,在创建这种下拉列表时,只有<select>标签包含 th:field属性,但<option>中的th:value属性将非常重要,因为它们将提供了解当前所选选项的方法(与非布尔复选框和单选按钮类似)。
让我们重新构建类型字段作为下拉选择:
<select th:field="*{type}">
<option th:each="type : ${allTypes}"
th:value="${type}"
th:text="#{${'seedstarter.type.' + type}}">Wireframe</option>
</select>
在这一点上,理解这段代码非常容易,请注意属性优先级如何允许我们设置th;each属性在<option>标签本身。
6、动态字段(Dynamic fields)
由于Spring MVC中的高级表单字段绑定功能,我们可以使用复杂的Spring EL表达式将动态表单字段绑定到表单支持bean。这将允许我们在SeedStarter bean中创建新的Row对象,并根据用户请求将这些行的字段添加到表单中。
为了做到这一点,我们需要在控制器中使用几个新的映射方法,这将根据特定请求参数的存在在SeedStarter中添加或删除一行:
@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";
}
现在我们可以在表单中添加动态表:
<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>
在这里可以看到很多东西,但现在我们不应该理解太多......除了一件奇怪的事情:
<select th:field="*{rows[__${rowStat.index}__].variety}">
...
</select>
如果您回忆一下“使用Thymeleaf”教程,那么__ $ {...} __语法是一个预处理表达式,它是一个在实际计算整个表达式之前计算的内部表达式。但为什么这种方式指定行索引?这不足以:
<select th:field="*{rows[rowStat.index].variety}">
...
</select>
......好吧,实际上,没有。问题是Spring EL没有评估数组索引括号内的变量,所以在执行上面的表达式时,我们会得到一个错误告诉我们行[rowStat.index](而不是rows [0],rows [1]等)不是rows集合中的有效位置。这就是为什么需要预处理的原因。
在按“添加行”几次后,让我们看看生成的HTML片段:
<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>
二、验证和错误消息(Validation and Error Message)
我们的大多数表单都需要显示验证消息,以便告知用户他/她所犯的错误。
Thymeleaf为此提供了一些工具:#fields对象中的几个函数,th:errors和th:errorclass属性。
- 领域错误(Filed Errors)
让我们看看如果它有一个错误我们如何设置一个特定的CSS类到字段:
<input type="text" th:field="*{datePlanted}"
th:class="${#fields.hasErrors('datePlanted')}? fieldError" />
如您所见,#fields.hasErrors(...)函数接收字段表达式作为参数(datePlanted),并返回一个布尔值,告知该字段是否存 在任何验证错误。
我们还可以获取该字段的所有错误并迭代它们:
<ul>
<li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" />
</ul>
而不是迭代,我们也可以使用th:errors,一个专门的属性,它构建一个包含指定选择器的所有错误的列表,由<br/>分隔:
<input type="text" th:field="*{datePlanted}" />
<p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>
简化基于错误的CSS样式:th:errorclass
我们在上面看到的示例,如果该字段有错误,将CSS类设置为表单输入,这是很常见的,以至于Thymeleaf提供了一个特定的属性来执行:th:errorclass。
应用于表单字段标记(input,select,textarea ...),它将从同一标记中的任何现有名称或th:字段属性读取要检查的字段的名称,然后将指定的CSS类附加到标记如果此类字段有任何相关错误:
<input type="text" th:field="*{datePlanted}" class="small" th:errorclass="fieldError" />
如果datePlanted有错误,则会显示为:
<input type="text" id="datePlanted" name="datePlanted" value="2013-01-01" class="small fieldError" />
- 所有错误(All Errors)
如果我们想要显示表单中的所有错误怎么办?我们只需要使用'*'或'all'常量(等效)查询#fields.hasErrors(...)和#fields.errors(...)方法:
<ul th:if="${#fields.hasErrors('*')}">
<li th:each="err : ${#fields.errors('*')}" th:text="${err}">Input is incorrect</li>
</ul>
如上例所示,我们可以获取所有错误并迭代它们......
<ul>
<li th:each="err : ${#fields.errors('*')}" th:text="${err}" />
</ul>
以及建立一个由<br/>分开的列表:
<p th:if="${#fields.hasErrors('all')}" th:errors="*{all}">Incorrect date</p>
最后请注意,#fields.hasErrors('*')等同于#fields.hasAnyErrors()和#fields.errors('*')等同于#fields.allErrors()。使用您喜欢的任何语法:
<div th:if="${#fields.hasAnyErrors()}">
<p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</p>
</div>
- 全局错误(Global errors)
Spring表单中存在第三种类型的错误:全局错误。这些错误与表单中的任何特定字段无关,但仍然存在。
Thymeleaf为访问这些错误提供了全局常量:
<ul th:if="${#fields.hasErrors('global')}">
<li th:each="err : ${#fields.errors('global')}" th:text="${err}">Input is incorrect</li>
</ul>
<p th:if="${#fields.hasErrors('global')}" th:errors="*{global}">Incorrect date</p>
...以及等效的#fields.hasGlobalErrors()和#fields.globalErrors()方便的方法:
<div th:if="${#fields.hasGlobalErrors()}">
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}">...</p>
</div>
- 在表单外显示错误(Displaying errors outside forms)
表单验证错误也可以通过使用变量($ {...})而不是选择(* {...})表达式并在表单支持bean的名称前面显示在表单外部:
<div th:errors="${myForm}">...</div>
<div th:errors="${myForm.date}">...</div>
<div th:errors="${myForm.*}">...</div>
<div th:if="${#fields.hasErrors('${myForm}')}">...</div>
<div th:if="${#fields.hasErrors('${myForm.date}')}">...</div>
<div th:if="${#fields.hasErrors('${myForm.*}')}">...</div>
<form th:object="${myForm}">
...
</form>
- 丰富的错误对象(Rich error objects)
Thymeleaf提供了使用fieldName(String),message(String)和global(boolean)属性以bean(而不仅仅是字符串)的形式获取表单错误信息的可能性。
这些错误可以通过#fields.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>
三、转换服务(The Conversion Service)
配置(Configuration)
如前所述,Thymeleaf可以使用在应用程序上下文中注册的转换服务。我们的应用程序配置类通过扩展Spring自己的WebMvcConfigurerAdapter帮助程序,将自动注册这样的转换服务,我们可以通过添加我们需要的格式化程序来配置它。让我们再看一下它的样子:
@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();
}
2、双括号语法(Double-brace syntax)
可以轻松应用转换服务,以便将任何对象转换/格式化为String。这是通过双括号表达式语法完成的:
- 对于变量表达式:$ {{...}}
- 对于选择表达式:* {{...}}
因此,例如,给定一个Integer-to-String转换器,将逗号添加为千位分隔符,这样:
<p th:text="${val}">...</p>
<p th:text="${{val}}">...</p>
.....结果是:
<p>1234567890</p>
<p>1,234,567,890</p>
3、在表单中使用(Use in forms)
我们之前看到,每个th:field属性将始终应用转换服务,因此:
<input type="text" th:field="*{datePlanted}" />
......实际上相当于:
<input type="text" th:field="*{{datePlanted}}" />
请注意,根据Spring的要求,这是使用单括号语法在表达式中应用转换服务的唯一方案。
4、#conversions实用对象
#conversions表达式实用程序对象允许在需要时手动执行转换服务:
<p th:text="${'Val: ' + #conversions.convert(val,'String')}">...</p>
此实用程序对象的语法:
- #conversions.convert(Object,Class):将对象转换为指定的类。
- #conversions.convert(Object,String):与上面相同,但是将目标类指定为String(注意可以省略java.lang包)。
四、渲染模板碎片(Rendering Template Fragments)
Thymeleaf提供了仅执行模板的一部分作为执行结果的可能性:片段。
这可以是一个有用的组件化工具。例如,它可以在AJAX调用上执行的控制器上使用,这可能会返回已经在浏览器中加载的页面的标记片段(用于更新选择,启用/禁用按钮......)。
通过使用Thymeleaf的片段规范:实现org.thymeleaf.fragment.IFragmentSpec接口的对象,可以实现片段渲染。
这些实现中最常见的是org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec,它允许使用DOM选择器指定一个完全类似于th:include或th:replace的片段。
1、在视图bean中指定片段
视图bean是在应用程序上下文中声明的org.thymeleaf.spring4.view.ThymeleafView类的bean(如果使用Java配置,则为@Bean声明)。它们允许像这样的片段的规范:
@Bean(name="content-part")
@Scope("prototype")
public ThymeleafView someViewBean() {
ThymeleafView view = new ThymeleafView("index"); // templateName = 'index'
view.setMarkupSelector("content");
return view;
}
鉴于上面的bean定义,如果我们的控制器返回content-part(上面bean的名称)......
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "content-part";
}
... thymeleaf将仅返回索引模板的内容片段 - 一旦应用前缀和后缀,该位置可能类似于/WEB-INF/templates/index.html。所以结果将完全等同于指定index :: content:
<!DOCTYPE html>
<html>
...
<body>
...
<div th:fragment="content">
Only this div will be rendered!
</div>
...
</body>
</html>
还要注意,由于Thymeleaf标记选择器的强大功能,我们可以在模板中选择一个片段,而根本不需要任何th:fragment属性。我们使用id属性,例如:
@Bean(name="content-part")
@Scope("prototype")
public ThymeleafView someViewBean() {
ThymeleafView view = new ThymeleafView("index"); // templateName = 'index'
view.setMarkupSelector("#content");
return view;
}
...将完美选择:
<!DOCTYPE html>
<html>
...
<body>
...
<div id="content">
Only this div will be rendered!
</div>
...
</body>
</html>
2、在控制器返回值中指定片段
可以使用片段表达式的语法从控制器本身指定片段,而不是声明视图bean。就像在:插入或th:替换属性:
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "index :: content";
}
当然,DOM选择器的全部功能也是可用的,因此我们可以根据标准HTML属性选择我们的片段,例如id =“content”:
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "index :: #content";
}
我们还可以使用参数,例如:
@RequestMapping("/showContentPart")
public String showContentPart() {
...
return "index :: #content ('myvalue')";
}
五、高级集成功能(Advanced Integration Features)
1、与RequestDataValueProcessor集成
Thymeleaf与Spring的RequestDataValueProcessor接口无缝集成。此接口允许在将链接URL,表单URL和表单字段值写入标记结果之前截取它们,以及透明地添加隐藏表单字段以启用安全功能,例如:保护再次CSRF(跨站请求伪造)。
可以在应用程序上下文中轻松配置RequestDataValueProcessor的实现。它需要实现org.springframework.web.servlet.support.RequestDataValueProcessor接口并将requestDataValueProcessor作为bean名称:
@Bean
public RequestDataValueProcessor requestDataValueProcessor() {
return new MyRequestDataValueProcessor();
}
......而Thymeleaf将以这种方式使用它:
- th:href和th:src在呈现URL之前调用RequestDataValueProcessor.processUrl(...)。
- th:action在呈现表单的action属性之前调用RequestDataValueProcessor.processAction(...),另外它检测何时在标签上应用此属性 - 无论如何应该是唯一的地方 - 在这种情况下调用RequestDataValueProcessor.getExtraHiddenFields(...)并在结束标记之前添加返回的隐藏字段。
- th:value调用RequestDataValueProcessor.processFormFieldValue(...)来呈现它引用的值,除非在同一个标记中存在th:字段(在这种情况下th:field将注意)。
- th:field调用RequestDataValueProcessor.processFormFieldValue(...)来呈现它应用的字段的值
请注意,您需要在应用程序中显式实现RequestDataValueProcessor的情况非常少。在大多数情况下,这将由您透明使用的安全库自动使用,例如, Spring Security的CSRF支持。
2、构建控制器的URI(Building URIs to controllers)
从版本4.1开始,Spring允许直接从视图构建到带注释的控制器的链接,而无需知道这些控制器映射到的URI。
在Thymeleaf中,这可以通过#mvc.url(...)表达式对象方法来实现,该方法允许通过它们所在的控制器类的大写字母来指定控制器方法,然后是方法的名称。本身。这相当于JSP的spring:mvcUrl(...)自定义函数。
For example, for:
public class ExampleController {
@RequestMapping("/data")
public String getData(Model model) { ... return "template" }
@RequestMapping("/data")
public String getDataParam(@RequestParam String type) { ... return "template" }
}
以下代码将创建一个指向它的链接:
<a th:href="${(#mvc.url('EC#getData')).build()}">Get Data Param</a>
<a th:href="${(#mvc.url('EC#getDataParam').arg(0,'internal')).build()}">Get Data Param</a>
六、Spring WebFlow集成
1、基本配置
Thymeleaf + Spring集成包包括与Spring WebFlow(2.3+)的集成。
WebFlow包含一些AJAX功能,用于在触发特定事件(转换)时呈现显示页面的片段,并且为了使Thymeleaf能够参与这些AJAX请求,我们将不得不使用不同的ViewResolver实现,配置如下:
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring4.view.AjaxThymeleafViewResolver">
<property name="viewClass" value="org.thymeleaf.spring4.view.FlowAjaxThymeleafView" />
<property name="templateEngine" ref="templateEngine" />
</bean>
...然后可以在WebFlow ViewFactoryCreator上配置此ViewResolver,如:
<bean id="mvcViewFactoryCreator"
class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
<property name="viewResolvers" ref="thymeleafViewResolver"/>
</bean>
从这里开始,您可以在视图状态中指定Thymeleaf模板:
<view-state id="detail" view="bookingDetail">
...
</view-state>
在上面的示例中,bookingDetail是以常规方式指定的Thymeleaf模板,可由TemplateEngine中配置的任何模板解析器理解。
2、Spring WebFlow中的AJAX片段
请注意,这里解释的只是创建与Spring WebFlow一起使用的AJAX片段的方法。如果您没有使用WebFlow,那么创建一个响应AJAX请求并返回一大块HTML的Spring MVC控制器就像创建任何其他模板返回控制器一样简单,唯一的例外是您可能会返回一个像“ main :: admin“来自你的控制器方法。
WebFlow允许通过带有标记的AJAX呈现片段的规范,如下所示:
<view-state id="detail" view="bookingDetail">
<transition on="updateData">
<render fragments="hoteldata"/>
</transition>
</view-state>
这些片段(在本例中为hoteldata)可以是逗号分隔的片段列表,在标记处使用th:fragment指定:
<div id="data" th:fragment="hoteldata">
This is a content to be changed
</div>
始终记住指定的片段必须具有id属性,以便在浏览器上运行的Spring JavaScript库能够替换标记。
标签也可以使用DOM选择器指定:
<view-state id="detail" view="bookingDetail">
<transition on="updateData">
<render fragments="[//div[@id='data']]"/>
</transition>
</view-state>
......这意味着不需要片段:
<div id="data">
This is a content to be changed
</div>
至于触发updateData转换的代码,它看起来像:
<script type="text/javascript" th:src="@{/resources/dojo/dojo.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring.js}"></script>
<script type="text/javascript" th:src="@{/resources/spring/Spring-Dojo.js}"></script>
...
<form id="triggerform" method="post" action="">
<input type="submit" id="doUpdate" name="_eventId_updateData" value="Update now!" />
</form>
<script type="text/javascript">
Spring.addDecoration(
new Spring.AjaxEventDecoration({formId:'triggerform',elementId:'doUpdate',event:'onclick'}));
</script>