spring+thymeleaf实现表单验证数据双向绑定
前⾔
这个教程介绍了Thymeleaf与Spring框架的集成,特别是SpringMvc框架。
注意Thymeleaf⽀持同Spring框架的3.和4.版本的集成,但是这两个版本的⽀持是封装在thymeleaf-spring3和thymeleaf-spring4这两个独⽴的库中,项⽬中需要根据实际情况分别引⽤。
样例代码针对的是spring4.,但⼀般情况下,spring3.也可以⽆缝使⽤,所需要的仅仅是改变⼀下引⽤库。
1 Thymeleaf同Spring的整合
Thymeleaf与Spring进⾏整合后,可以在SpringMVC应⽤中完全替代JSP⽂件。
集成后你将:
*就像控制JSP⼀样,使⽤SpringMvc的@Controller注解来映射Thymeleaf的模板⽂件。
*在模板中使⽤SpringEL表达式来替换OGNL
*在模板中创建的表单,完全⽀持Beans和结果的绑定,包括使⽤PropertyEditor,转换,和验证等。
*可以通过Spring来管理国际化⽂件显⽰国际化信息。
*注意,在使⽤本教程之前,您应该充分了解Thymeleaf的标准⽅⾔。
2 Spring标准⽅⾔
为了更加⽅便,更快捷的集成,Thymeleaf提供了⼀套能够与Spring正确⼯作的特有⽅⾔。
这套⽅⾔基于Thymeleaf标准⽅⾔实现,它在类 org.thymeleaf.spring.dialect.SpringStandardDialect 中,事实上,他继承
于 org.thymeleaf.standard.StandardDialect 中。
除了已经出现在标准⽅⾔中的所有功能,Spring中还有以下特点:
*不适⽤OGNL,⽽是SpringEL做完变量表达式,因此,所有的${...}和*{...}表达式将⽤Spring的表达式引擎进⾏处理。
*访问应⽤context中的beans可以使⽤SpringEL语法:${@myBean.doSomething()}
*基于表格处理的新属性:th:field,th:errors和th:errorclass,除此还有⼀个th:object的新实现,允许它使⽤表单命令选择器(??)。
*⼀个新的表达式:#de(...),相当于jsp⾃定义标签中的spring:theme。
*在spring4.0集成中的⼀个新的表达式:#mvc.uri(...),相当于jsp⾃定义标签中的spring:mvcUrl(...)
注意,上述这些⽅⾔特性是不能再普通的TemplateEngine对象中使⽤的,应该配置⼀个org.thymeleaf.spring4.SpringTemplateEngine来执⾏。
其他thymeleaf基础使⽤⽅法部分不在叙述。
创建表单
处理命令对象
SpringMVC的表单⽀持bean就是命令对象,这个对象通过对象领域模型的⽅式提供get和set⽅法,在浏览器建⽴获取⽤户输⼊值的输⼊框架。
Thymeleaf需要你显⽰的在form标签内通过th:object属性指定命令对象:
1<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
2 ...
3</form>
这个th:object与其他的的地⽅⽤途是⼀直的,但是事实上在这种特定情况下,为了与SpringMVC框架的正确整合增加了⼀些特定的限制:
1.在form标签中的th:object的值必须是变量表达式(...),只能指定属性模型属性的名字,⽽不能使⽤属性导航,这意味着,表达式...),只能指定属性模型属性的名字,⽽不能使⽤属性导航,
这意味着,表达式 {seedStarter}是正确的,⽽${seedStarter.data}则不是。
2.⼀个form标签内只能指定⼀个th:object属性,这与html中form标签不能嵌套的特性相⼀致。
input
下⾯是如何将⼀个input插⼊到表单中
<input type="text" th:field="*{datePlanted}"/>
正象上边的代码所⽰,新增了⼀个 th:field 的属性,这是SpringMVC集成的⼀个重要特征,它帮你完成了表单bean和输⼊框之间的繁重的绑定⼯作。可以看出他在from中的路径属性和SpringMVC的jsp标签库⼀样。
th:field 属性的不同⾏为取决于它所附加的不同标签,包括<input> , <select>或<textarea> (还包括标签的不同type属性类型),在这种情况下,时间上上⾯哪⾏代码会是这样的:
<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}"/>
事实上,可能⽐上边的代码还要多⼀些东西,因为 th:field 还可能会注册⼀个Spring的转换服务,包括之前我们看到
的 DateFormatter (甚⾄这个表达式中没使⽤双⼤括号),因此,这个⽇期也将被正确的格式化。
th:field 的值必须使⽤选择表达式,这样将在这个环境中使⽤表单bean,⽽不是上下⽂变量或SpringMVC的模型属性。
相反对于 th:object 这类,它的表达式可以使⽤属性导航(事实上在JSP的<form:input标签中,可以使⽤任何的路径属性表达式)
注意th:field属性也可以在HTML5的的新增类型中使⽤,如<input type="datetime">,<input type="color">等,有效的增加了对SpringMVC对HTML5⽀持的完整性。
复选框
th:field 也可以⽤在 checkbox 中,⽐如如下代码:
1<div>
2<label th:for="${#('covered')}" th:text="#{vered}">已种植</label>
3<input type="checkbox" th:field="*{covered}"/>
4</div>
注意这⾥有⼀些除了复选框之外的好东西,⽐如外部label和它使⽤的 #("covered") ⽅法,⽤于当该id的复选框执⾏的时候获取它的id值。
那么为什么我们需要这个字段的id属性动态⽣成呢?因为复选框可能是多值的,因此它会给id值添加⼀个序列号后缀(内部使
⽤ #ids.seq(...) 函数)来保证同⼀属性的复选框有不同的id值。
我们可以看看多值的复选框:
1<ul>
2<li th:each="feat : ${allFeatures}">
3<input type="checkbox" th:field="*{features}" th:value="${feat}"/>
4<label th:for="${#ids.prev('features')}"
5 th:text="#{${'seedstarter.feature.' + feat}}">Heating</label>
6</li>
7</ul>
注意这次我们增加了⼀个 th:value 属性,因为这次的特征属性不是⼀个布尔值,⽽是⼀个数组。
⼀般情况下,它的输出为:
1<ul>
2<li>
3<input id="features1" name="features" type="checkbox"
4 value="SEEDSTARTER_SPECIFIC_SUBSTRATE"/>
5<input name="_features" type="hidden" value="on"/>
6<label for="features1">Seed starter-specific substrate</label>
7</li>
8<li>
9<input id="features2" name="features" type="checkbox"
10 value="FERTILIZER"/>
11<input name="_features" type="hidden" value="on"/>
12<label for="features2">Fertilizer used</label>
13</li>
14<li>
15<input id="features3" name="features" type="checkbox"
16 value="PH_CORRECTOR"/>
17<input name="_features" type="hidden" value="on"/>
18<label for="features3">PH Corrector used</label>
19</li>
20</ul>
我们可以看到⼀个序列后缀增加在每⼀个id的属性中, #ids.prev(....) 函数允许我们把检索最后⼀个序列值,⽣成的⼀个特定的id。
⽤不着担⼼那些隐藏域的名称为"_features":这是为了避免浏览器将未选中的复选框的值在表单提交是没有⾃动发送⽽故意添加的。
还应注意到,如果我们的表单bean中的feature属性已经包含了⼀些特定的值,那么th:field还将会⾃动在相应的标签中增加
checked="checked"属性。
单选框
单选框的⽤法和⼀个⾮布尔值的多选框使⽤⽅式类似,只是他不是多选:
1<ul>
2<li th:each="ty : ${allTypes}">
3<input type="radio" th:field="*{type}" th:value="${ty}"/>
4<label th:for="${#ids.prev('type')}" th:text="#{${'pe.' + ty}}">Wireframe</label>
5</li>
6</ul>
下拉列表
下拉列表包含两个部分:<select>标签和它包含的<option>标签。在创建这种表单域的时候,只有<select>标签需要导⼊th:field属性,
但 th:value 属性却在<option>标签中⾮常重要,因为他们提供了⽬前选选择框的选项(使⽤和⾮布尔复选框和单选框类似的⼿段)
使⽤类型作为下拉列表:
1<select th:field="*{type}">
2<option th:each="type : ${allTypes}"
3 th:value="${type}"
4 th:text="#{${'pe.' + type}}">Wireframe</option>
5</select>
这段代码理解起来很容易,只是注意属性优先级让我们可以在option标签内使⽤th:each属性。
动态域
由于SpringMVC的⾼级表单绑定功能,使得我们可以使⽤复杂的SpringEL表达式来绑定动态表单域到表单bean中。这将允许我们
在 SeedStarter bean 中创建⼀个新的Row对象,并将这个row的域添加到⽤户请求的form中。
为了做到这⼀点,我们需要在控制器中提供⼀些新的映射⽅法,它将根据我们的特定请求的参数来决定添加或删除⼀⾏我们定义的.
1 @RequestMapping(value="/seedstartermng", params={"addRow"})
2public String addRow(final SeedStarter seedStarter, final BindingResult bindingResult) {
3 Rows().add(new Row());
4return "seedstartermng";
5 }
6
7 @RequestMapping(value="/seedstartermng", params={"removeRow"})
8public String removeRow(
9final SeedStarter seedStarter, final BindingResult bindingResult,
10final HttpServletRequest req) {
11final Integer rowId = Integer.Parameter("removeRow"));
12 Rows().remove(rowId.intValue());
13return "seedstartermng";
14 }
现在给form添加⼀个动态table
1<table>
2<thead>
3<tr>
4<th th:text="#{wnum}">Row</th>
5<th th:text="#{ws.head.variety}">Variety</th>
6<th th:text="#{ws.head.seedsPerCell}">Seeds per cell</th>
7<th>
8<button type="submit" name="addRow" th:text="#{w.add}">Add row</button>
9</th>
10</tr>
11</thead>
12<tbody>
13<tr th:each="row,rowStat : *{rows}">
14<td th:text="${unt}">1</td>
15<td>
16<select th:field="*{rows[__${rowStat.index}__].variety}">
17<option th:each="var : ${allVarieties}"
18 th:value="${var.id}"
19 th:text="${var.name}">Thymus Thymi</option>
20</select>
21</td>
22<td>
23<input type="text" th:field="*{rows[__${rowStat.index}__].seedsPerCell}"/>
24</td>
25<td>
26<button type="submit" name="removeRow"
27 th:value="${rowStat.index}" th:text="#{ve}">Remove row</button>
28</td>
29</tr>
30</tbody>
31</table>
这⾥出现了很多东西,但都不难理解,除了这⼀句:
1<select th:field="*{rows[__${rowStat.index}__].variety}">
2 ...
3</select>
如果你记得Thymeleaf教程,那么应该明⽩ __${...}__ 是⼀种预处理表达式的语法。这是⼀个在处理整个表达式之前的内部表达式,但为什么⽤这种⽅式指定⾏的索引呢,下⾯这种⽅式不⾏么:
1<select th:field="*{rows[rowStat.index].variety}">
2 ...
3</select>
嗯事实上,是不⾏的,他的问题是SpringEL表达式不执⾏数值中括号⾥边的表达式变量,索引执⾏上边的语句时,会得到⼀个错误的结果,就是字⾯形式的 row[rowStat.index] (⽽不是 row[0],row[1] )⽽不是⾏集合中的正确位置,这就是为什么在这⾥需要预处理。
让我们看看产⽣的html后按"添加⾏"按钮⼏次:
1<tbody>
2<tr>
3<td>1</td>
4<td>
5<select id="rows0.variety" name="rows[0].variety">
6<option selected="selected" value="1">Thymus vulgaris</option>
7<option value="2">Thymus x citriodorus</option>
8<option value="3">Thymus herba-barona</option>
9<option value="4">Thymus pseudolaginosus</option>
10<option value="5">Thymus serpyllum</option>
11</select>
12</td>
13<td>
14<input id="rows0.seedsPerCell" name="rows[0].seedsPerCell" type="text" value=""/>
15</td>
16<td>
17<button name="removeRow" type="submit" value="0">Remove row</button>
18</td>
19</tr>
20<tr>
21<td>2</td>
22<td>
23<select id="rows1.variety" name="rows[1].variety">
24<option selected="selected" value="1">Thymus vulgaris</option>
25<option value="2">Thymus x citriodorus</option>
26<option value="3">Thymus herba-barona</option>
27<option value="4">Thymus pseudolaginosus</option>
28<option value="5">Thymus serpyllum</option>
29</select>
30</td>
31<td>
32<input id="rows1.seedsPerCell" name="rows[1].seedsPerCell" type="text" value=""/>
33</td>
34<td>
35<button name="removeRow" type="submit" value="1">Remove row</button>
36</td>
37</tr>
38</tbody>
验证和错误信息
让我们看看当有错误的时候如何给⼀个表单域⼀个CSS类:
1<input type="text" th:field="*{datePlanted}"
2 th:class="${#fields.hasErrors('datePlanted')}? fieldError"/>
可以看到, #fields.hasErrors(...) 函数接受⼀个表达式参数(datePlanted),返回⼀个布尔值告诉field该字段是否有验证错误。
我们可以根据他们各⾃的field获取所有的错误:
1<ul>
2<li th:each="err : ${#s('datePlanted')}" th:text="${err}"/>
3</ul>
通过迭代,我们可以使⽤ th:errors ,⼀个专门⽤于创建⼀个通过制定选择器筛选的错误列表的属性,通过分隔。
1<input type="text" th:field="*{datePlanted}"/>
2<p th:if="${#fields.hasErrors('datePlanted')}" th:errors="*{datePlanted}">Incorrect date</p>
简单错误基础css样式,th:errorclass
在上边的例⼦中,如果字段有错误,将为表单的input域设置⼀个css类,因为这种⽅式很常见,Thymeleaf提供了⼀个特定的属性
为 th:errorclass
应⽤于form域的标签(input,select,textarea等),它将从现有的name属性或th:field属性字段的名词相同的属性,如果发⽣错误,则将制定的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"/>
全部错误
如果我们想要在form中显⽰所有的错误呢?我们只需要通过'*'或'all'(等价)来查询 #field.hasErrors(...) ⽅法和 #s(...) ⽅法:
1<ul th:if="${#fields.hasErrors('*')}">
2<li th:each="err : ${#s('*')}" th:text="${err}">Input is incorrect</li>
3</ul>
在上边的例⼦中,我们得到所有的错误并迭代他们:
1<ul>
2<li th:each="err : ${#s('*')}" th:text="${err}"/>
3</ul>
建⽴⼀个以<Enter>分隔的列表:
1<p th:if="${#fields.hasErrors('all')}" th:errors="*{all}">Incorrect date</p>
最后,注意 #field.hasErrors("") 等效的属性 #fields.hasAnyErrors() 和 #s() 的等效的 #fields.allErrors() ,可以使⽤喜欢的任何语法。
1<div th:if="${#fields.hasAnyErrors()}">
2<p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</p>
3</div>
全局错误
Spring表单还有⼀种错误,全局错误,都是些不与窗体的任何特定字段关联的错误。
Thymeleaf提供了⼀个global的常量来访问这些错误。
1<ul th:if="${#fields.hasErrors('global')}">
2<li th:each="err : ${#s('global')}" th:text="${err}">Input is incorrect</li>
3</ul>
Incorrect date以及等效的 #field.hasGlobalErrors() 和 #field.globalErrors() ⽅法。
1<div th:if="${#fields.hasGlobalErrors()}">
2<p th:each="err : ${#fields.globalErrors()}" th:text="${err}">...</p>
3</div>
在表单外部显⽰错误
表单验证错误也可以在表单外部显⽰,⽅法是通过变量(即${...})的内部选择变量(*{...})增加表单bean的名字作为前缀的⽅式。
1 <form>
2 <div th:errors="${myForm}">...</div>
3 <div th:errors="${myForm.date}">...</div>
4 <div th:errors="${myForm.*}">...</div>
5 <div th:if="${#fields.hasErrors('${myForm}')}">...</div>
6 <div th:if="${#fields.hasErrors('${myForm.date}')}">...</div>
7 <div th:if="${#fields.hasErrors('${myForm.*}')}">...</div>
8 <form th:object="${myForm}">
9 ...
10 </form>
富错误对象
Thymeleaf提供了以bean的形式(代替单纯的String)提供错误信息的能⼒,包括fieldName(String),message(String),和global(String)属性的错误。这些错误可以通过⼯具⽅法#fields.datailedErrors()来实现:
1<ul>
2<li th:each="e : ${#fields.detailedErrors()}" th:class="${e.global}? globalerr : fielderr">
3<span th:text="${e.global}? '*' : ${e.fieldName}">The field name</span> |
4<span th:text="${e.message}">The error message</span>
5</li>
6</ul>
转换服务
配置
就像前⽂所说,Thymeleaf可以在上下⽂中注册⼀个转换服务,再次看⼀下他的配置信息
1<?xml version="1.0" encoding="UTF-8"?>
2<beans ...>
3 ...
thyme4<mvc:annotation-driven conversion-service="conversionService"/>
5 ...
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论