Thymeleaf+Spring整合

前言

這個教程介紹了Thymeleaf與Spring框架的集成,特別是SpringMvc框架。javascript

注意Thymeleaf支持同Spring框架的3.和4.版本的集成,可是這兩個版本的支持是封裝在thymeleaf-spring3和thymeleaf-spring4這兩個獨立的庫中,項目中須要根據實際狀況分別引用。css

樣例代碼針對的是spring4.,但通常狀況下,spring3.也能夠無縫使用,所須要的僅僅是改變一下引用庫。html

1 Thymeleaf同Spring的整合

Thymeleaf與Spring進行整合後,能夠在SpringMVC應用中徹底替代JSP文件。java

集成後你將:git

  • 就像控制JSP同樣,使用SpringMvc的@Controller註解來映射Thymeleaf的模板文件。
  • 在模板中使用SpringEL表達式來替換OGNL
  • 在模板中建立的表單,徹底支持Beans和結果的綁定,包括使用PropertyEditor,轉換,和驗證等。
  • 能夠經過Spring來管理國際化文件顯示國際化信息。

注意,在使用本教程以前,您應該充分了解Thymeleaf的標準方言。github

2 Spring標準方言

爲了更加方便,更快捷的集成,Thymeleaf提供了一套可以與Spring正確工做的特有方言。web

這套方言基於Thymeleaf標準方言實現,它在類org.thymeleaf.spring.dialect.SpringStandardDialect中,事實上,他繼承於org.thymeleaf.standard.StandardDialect中。ajax

除了已經出如今標準方言中的全部功能,Spring中還有如下特色:spring

  • 不適用OGNL,而是SpringEL作完變量表達式,所以,全部的${...}和*{...}表達式將用Spring的表達式引擎進行處理。
  • 訪問應用context中的beans可使用SpringEL語法:${@myBean.doSomething()}
  • 基於表格處理的新屬性:th:field,th:errors和th:errorclass,除此還有一個th:object的新實現,容許它使用表單命令選擇器(??)。
  • 一個新的表達式:#themes.code(...),至關於jsp自定義標籤中的spring:theme。
  • 在spring4.0集成中的一個新的表達式:#mvc.uri(...),至關於jsp自定義標籤中的spring:mvcUrl(...)

注意,上述這些方言特性是不能再普通的TemplateEngine對象中使用的,應該配置一個org.thymeleaf.spring4.SpringTemplateEngine來執行。數組

一個配置的簡單例子:

<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/" />
    <property name="suffix" value=".html" />
</bean>

<bean id="templateEngine"   class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver" />
</bean>

視圖和視圖解釋器

SpringMvc中的視圖和視圖解釋器

Spring有兩個符合其模板系統核心的接口:

  • org.springframework.web.servlet.View
  • org.springframework.web.servlet.ViewResolver

視圖模型頁面在應用中,讓我修改和預約義他的行爲的頁面,可將其做爲Bean來定義,視圖是負責渲染實際的HTML,一般由一些模板引擎來負責,如JSP和Thymeleaf。

ViewResolvers是一個獲取特定操做和語言的的視圖對象的對象。一般,controller會向ViewResolvers要求轉發到一個特定的視圖(視圖名爲控制器返回的字符串)。而後在順序執行應用中全部的視圖解析器,直到有一個可以解析這個視圖。在這種狀況下,視圖對象返回並控制傳遞給他的一個html渲染相。

注意,在一個應用中,並非全部的頁面都被定義爲視圖,可是隻有那些行爲咱們但願以特定的方式進行非標準方式操做或者進行特定配置,例如,一些特殊的bean。若是一個ViewResolver請求一個view但沒有響應的bean(這是一個常見的狀況),一個新的視圖對象將被臨時建立並返回。

一個SpringMVC中Jsp+JSTL視圖解釋器的典型配置以下:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/jsps/" />
  <property name="suffix" value=".jsp" />
  <property name="order" value="2" />
  <property name="viewNames" value="*jsp" />
</bean>

根據他的屬性就足夠知道他是怎麼配置的了:

  • viewClass:創建視圖實例的類,在JSP解析的時候所必須的,可是如今咱們使用Thymeleaf,因此它是不須要的。
  • prefix和suffix,和Thymeleaf的TemplateResolver對象的方式一直,設置前綴和後綴屬性。
  • order:設置在視圖解析器查詢鏈中的順序
  • viewNames:容許定義視圖名稱(可經過通配符),定義內的視圖由視圖解析器解析。

Thymeleaf中的視圖和視圖解析器

Thymeleaf和Spring相似,一樣是對應兩個接口:

  • org.thymeleaf.spring4.view.ThymeleafView
  • org.thymeleaf.spring4.view.ThymeleafViewResolver

這兩個類將用於處理控制器返回Thymeleaf執行的結果。

Thymeleaf視圖解析器的配置一樣和JSP是很是類似的:

<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
  <property name="templateEngine" ref="templateEngine" />
  <property name="order" value="1" />
  <property name="viewNames" value="*.html,*.xhtml" />
</bean>

它的templateEngin的值固然是前一章定義的SpringTemplateEngin對象,另外兩個參數都是可選的,而且也以前的JSP 視圖解析器配置的時候參數含義相同

須要注意一點,咱們並不須要配置前綴和後綴,由於這些已經在模板解析器中指定,並會依次傳遞到模板引擎中。

若是咱們想定義一個View的bean並設置一些靜態變量該如何作呢?很簡單:

<bean name="main" class="org.thymeleaf.spring4.view.ThymeleafView">
<property name="staticVariables">
    <map>
    <entry key="footer" value="foot信息" />
    </map>
 </property>
</bean>

模板配置

Spring基礎配置

在與Spring配合使用的時候,Thymeleaf提供了ITemplateResolver和與之相關聯的IResourceResolver的與Spring資源處理器相結合的實現,這些是:

  • org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver用於解析模板.
  • org.thymeleaf.spring4.resourceresolver.SpringResourceResourceResolver主要供內部使用.

這個模板解析器容許應用使用標準Spring資源解析語法來解析模板程序,它能夠這樣配置:

<bean id="templateResolver"
  class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML5" />
</bean>

而後就能夠像這樣使用視圖:

@RequestMapping("/doit")
public String doIt() {
    ...
    return "classpath:resources/templates/doit";
}

注意Spring基礎的資源解析器不會被默認使用,它只是一個除了Thymeleaf核心所提供的模板資源解析器以外的模板資源解析器。

麝香生長管理系統

示例代碼能夠今後處下載下載

簡介

有不少人都喜歡麝香,每一年春天咱們都會在小花盆裏放上優良的土壤,還有麝香的種子,將它們放在陽光下,耐心的等待它們的生長。

可是今年咱們受夠了靠貼標籤來知道每一個花盆裏種的是什麼,因此咱們決定使用Spring+Thymeleaf來製做一個應用,用於管理咱們的一個培育目錄,這個應用叫:春葉培育管理員系統。

同Thymeleaf教程中的古泰虛擬商店同樣,這個春葉培育管理系統將會設計到Spring+Thymeleaf的最重要的部分。

業務層

咱們將爲咱們的應用配置一個簡單的業務層,首先看看數據模型:

用幾個簡單的服務類提供所需的業務方法:

@Service
public class SeedStarterService {

    @Autowired
    private SeedStarterRepository seedstarterRepository; 

    public List<SeedStarter> findAll() {
        return this.seedstarterRepository.findAll();
    }

    public void add(final SeedStarter seedStarter) {
        this.seedstarterRepository.add(seedStarter);
    }

}

和:

@Service
public class VarietyService {

    @Autowired
    private VarietyRepository varietyRepository; 

    public List<Variety> findAll() {
        return this.varietyRepository.findAll();
    }

    public Variety findById(final Integer id) {
        return this.varietyRepository.findById(id);
    }

}

Spring MVC配置

接下來咱們須要在應用中創建MVC配置文件,它將不只包括SpringMvc的資源處理和註解掃描,還建立了模板引擎和視圖解釋器的實例。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
                           http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
    
  <!-- **************************************************************** -->
  <!--  RESOURCE FOLDERS CONFIGURATION                                  -->
  <!--  Dispatcher configuration for serving static resources           -->
  <!-- **************************************************************** -->
  <mvc:resources location="/images/" mapping="/images/**" />
  <mvc:resources location="/css/" mapping="/css/**" />
    

  <!-- **************************************************************** -->
  <!--  SPRING ANNOTATION PROCESSING                                    -->
  <!-- **************************************************************** -->
  <mvc:annotation-driven conversion-service="conversionService" />
  <context:component-scan base-package="thymeleafexamples.stsm" />


  <!-- **************************************************************** -->
  <!--  MESSAGE EXTERNALIZATION/INTERNATIONALIZATION                    -->
  <!--  Standard Spring MessageSource implementation                    -->
  <!-- **************************************************************** -->
  <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="Messages" />
  </bean>


  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService" 
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>


  <!-- **************************************************************** -->
  <!--  THYMELEAF-SPECIFIC ARTIFACTS                                    -->
  <!--  TemplateResolver <- TemplateEngine <- ViewResolver              -->
  <!-- **************************************************************** -->

  <bean id="templateResolver"
        class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/" />
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML5" />
  </bean>
    
  <bean id="templateEngine"
        class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver" />
  </bean>
   
  <bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
    <property name="templateEngine" ref="templateEngine" />
  </bean>    

    
</beans>

注意:這裏選擇了HTML5做爲模板模式。

控制器

固然,這個應用程序中還須要一個控制器,因爲這個應用只有一個頁面,用戶種子的生長的查看和添加,因此只須要一個控制器就能夠了:

@Controller
public class SeedStarterMngController {

    @Autowired
    private VarietyService varietyService;
    
    @Autowired
    private SeedStarterService seedStarterService;

    ...

}

如今看看在這個控制器中能夠添加什麼?

模型屬性(ModelAttribute註解)

@ModelAttribute("allTypes")
public List<Type> populateTypes() {
    return Arrays.asList(Type.ALL);
}
    
@ModelAttribute("allFeatures")
public List<Feature> populateFeatures() {
    return Arrays.asList(Feature.ALL);
}
    
@ModelAttribute("allVarieties")
public List<Variety> populateVarieties() {
    return this.varietyService.findAll();
}
    
@ModelAttribute("allSeedStarters")
public List<SeedStarter> populateSeedStarters() {
    return this.seedStarterService.findAll();
}

方法映射

接下來是控制器最重要的一部分了,那就是方法映射(RequestMapping),一個表單頁和一個新的種子對象添加頁。

@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter) {
    seedStarter.setDatePlanted(Calendar.getInstance().getTime());
    return "seedstartermng";
}

@RequestMapping(value="/seedstartermng", params={"save"})
public String saveSeedstarter(
        final SeedStarter seedStarter, final BindingResult bindingResult, final ModelMap model) {
    if (bindingResult.hasErrors()) {
        return "seedstartermng";
    }
    this.seedStarterService.add(seedStarter);
    model.clear();
    return "redirect:/seedstartermng";
}

配置轉換服務

爲了在模板視圖中更加方便的使用日期和咱們本身定義的各類對象,咱們註冊的了一個轉換服務在上下文中:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
  ...    
  <mvc:annotation-driven conversion-service="conversionService" />
  ...
  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService"
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>
  ...
</beans>

轉換服務容許咱們註冊兩個org.springframework.format.Formatter接口的實現,關於Spring轉換的更多信息,請查驗文檔

首先看一下DateFormatter,它的日期格式定義的字符串定義在Message.properties文件中,而且以date.format做爲key.

public class DateFormatter implements Formatter<Date> {

    @Autowired
    private MessageSource messageSource;


    public DateFormatter() {
        super();
    }

    public Date parse(final String text, final Locale locale) throws ParseException {
        final SimpleDateFormat dateFormat = createDateFormat(locale);
        return dateFormat.parse(text);
    }

    public String print(final Date object, final Locale locale) {
        final SimpleDateFormat dateFormat = createDateFormat(locale);
        return dateFormat.format(object);
    }

    private SimpleDateFormat createDateFormat(final Locale locale) {
        final String format = this.messageSource.getMessage("date.format", null, locale);
        final SimpleDateFormat dateFormat = new SimpleDateFormat(format);
        dateFormat.setLenient(false);
        return dateFormat;
    }

}

VarietyFormatter能夠自動轉換咱們的各類實體,將他們用在表單上(基本經過id)

public class VarietyFormatter implements Formatter<Variety> {

    @Autowired
    private VarietyService varietyService;


    public VarietyFormatter() {
        super();
    }

    public Variety parse(final String text, final Locale locale) throws ParseException {
        final Integer varietyId = Integer.valueOf(text);
        return this.varietyService.findById(varietyId);
    }


    public String print(final Variety object, final Locale locale) {
        return (object != null ? object.getId().toString() : "");
    }
}

在以後的內容,咱們會學習更多的關於formatter的內容。

種子生長列表

首先,在/WEB-INF/templatesseedstartermng.html頁將顯示一個當前的已培育種子的列表,爲此咱們須要一些額外的信息,和經過表達式執行一些模型屬性:

<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">

  <h2 th:text="#{title.list}">List of Seed Starters</h2>
  
  <table>
    <thead>
      <tr>
        <th th:text="#{seedstarter.datePlanted}">Date Planted</th>
        <th th:text="#{seedstarter.covered}">Covered</th>
        <th th:text="#{seedstarter.type}">Type</th>
        <th th:text="#{seedstarter.features}">Features</th>
        <th th:text="#{seedstarter.rows}">Rows</th>
      </tr>
    </thead>
    <tbody>
      <tr th:each="sb : ${allSeedStarters}">
        <td th:text="${{sb.datePlanted}}">13/01/2011</td>
        <td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>
        <td th:text="#{${'seedstarter.type.' + sb.type}}">Wireframe</td>
        <td th:text="${#strings.arrayJoin(
                           #messages.arrayMsg(
                               #strings.arrayPrepend(sb.features,'seedstarter.feature.')),
                           ', ')}">Electric Heating, Turf</td>
        <td>
          <table>
            <tbody>
              <tr th:each="row,rowStat : ${sb.rows}">
                <td th:text="${rowStat.count}">1</td>
                <td th:text="${row.variety.name}">Thymus Thymi</td>
                <td th:text="${row.seedsPerCell}">12</td>
              </tr>
            </tbody>
          </table>
        </td>
      </tr>
    </tbody>
  </table>
</div>

這裏幾乎是所有代碼,如今分別查看每個片斷。

首先,這一部分將只在有種子在培育的時候顯示,咱們將使用th:unless屬性來經過#lists.iEmpty(...)方法來實現這個目標。

<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">

objects的工具類,好比#lists是SpringEL表達式,他就像在OGNL表達式中一樣的方式使用。

接下來是一些國際化的文本:

<h2 th:text="#{title.list}">List of Seed Starters</h2>

<table>
  <thead>
    <tr>
      <th th:text="#{seedstarter.datePlanted}">Date Planted</th>
      <th th:text="#{seedstarter.covered}">Covered</th>
      <th th:text="#{seedstarter.type}">Type</th>
      <th th:text="#{seedstarter.features}">Features</th>
      <th th:text="#{seedstarter.rows}">Rows</th>
      ...

在這個SpringMVC應用程序中,咱們經過一個bean定義了一個MessageSource在咱們的spring的XML配置文件中:

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basename" value="Messages" />
</bean>

basename表示咱們將使用message打頭的資源文件,如Message_en.properties或者Message_ZH_cn.properties,好比英文版以下:

title.list=Lista de semilleros

date.format=dd/MM/yyyy
bool.true=sí
bool.false=no

seedstarter.datePlanted=Fecha de plantación
seedstarter.covered=Cubierto
seedstarter.type=Tipo
seedstarter.features=Características
seedstarter.rows=Filas

seedstarter.type.WOOD=Madera
seedstarter.type.PLASTIC=Plástico

seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato específico para semilleros
seedstarter.feature.FERTILIZER=Fertilizante
seedstarter.feature.PH_CORRECTOR=Corrector de PH

在表格的第一列,將顯示種子的培育開始時間,咱們將經過定義的DateFormatter將它自動格式化顯示,爲了作到這一點,將使用${{}}語法,這個語法將自動應用Spring的轉換服務。

<td th:text="${{sb.datePlanted}}">13/01/2011</td>

下面將顯示花盆中是否有種子,經過改變bean的布爾值屬性將布爾值轉換爲國際化的是和否。:

<td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>

下一步將展現花盆的類型,它的類型是有兩個值的枚舉型(值分別爲木製和塑料),這也是我爲何在配置文件中定義了seedstarter.type.WOOD和seedstarter.type.PLAStIC兩個屬性的緣由。

但爲了獲取國際化以後的值,咱們須要給實際值增長seedstarter.type的前綴,來生成Message 屬性的key返回所需的值:

<td th:text="#{${'seedstarter.type.' + sb.type}}">Wireframe</td>

列表中最困難的部分就是功能列,由於在這裏須要顯示左右的功能,如"電加熱,草皮",這裏講採用逗號分隔原有枚舉數組的方式。

注意這樣也是有些困難的,由於這些枚舉須要根據他的類型進行具體化,須要:

  • 給特徵數組的全部元素規劃響應的前綴,
  • 得到從步驟1相對應的外部信息
  • 把全部從步驟2獲取的信息,用逗號分隔

爲了實現這一點,咱們建立了以下的代碼:

<td th:text="${#strings.arrayJoin(
               #messages.arrayMsg(
                   #strings.arrayPrepend(sb.features,'seedstarter.feature.')),
               ', ')}">Electric Heating, Turf</td>

列表的最有一列很簡單,事實上,它有一個嵌套表,用於顯示每一行的內容。

<td>
  <table>
    <tbody>
      <tr th:each="row,rowStat : ${sb.rows}">
        <td th:text="${rowStat.count}">1</td>
        <td th:text="${row.variety.name}">Thymus Thymi</td>
        <td th:text="${row.seedsPerCell}">12</td>
      </tr>
    </tbody>
  </table>
</td>

建立表單

處理命令對象

SpringMVC的表單支持bean就是命令對象,這個對象經過對象領域模型的方式提供get和set方法,在瀏覽器創建獲取用戶輸入值的輸入框架。

Thymeleaf須要你顯示的在form標籤內經過th:object屬性指定命令對象:

<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
    ...
</form>

這個th:object與其餘的的地方用途是一直的,可是事實上在這種特定狀況下,爲了與SpringMVC框架的正確整合增長了一些特定的限制:

  • 在form標籤中的th:object的值必須是變量表達式(${...}),只能指定屬性模型屬性的名字,而不能使用屬性導航,這意味着,表達式${seedStarter}是正確的,而${seedStarter.data}則不是。
  • 一個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:fild還可能會註冊一個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中,好比以下代碼:

<div>
  <label th:for="${#ids.next('covered')}" th:text="#{seedstarter.covered}">已種植</label>
  <input type="checkbox" th:field="*{covered}" />
</div>

注意這裏有一些除了複選框以外的好東西,好比外部label和它使用的#ids.next("covered")方法,用於當改id的複選框執行的時候獲取它的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屬性,由於此次的特徵屬性不是一個布爾值,而是一個數組。

通常狀況下,它的輸出爲:

<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":這是爲了不瀏覽器將未選中的複選框的值在表單提交是沒有自動發送而故意添加的。

還應注意到,若是咱們的表單bean中的feature屬性已經包含了一些特定的值,那麼th:field還將會自動在相應的標籤中增長checked="checked"屬性。

單選框

單選框的用法和一個非布爾值的多選框使用方式相似,只是他不是多選:

<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>

下拉列表

下拉列表包含兩個部分:<select>標籤和它包含的<option>標籤。在建立這種表單域的時候,只有<select>標籤須要導入th:field屬性,但th:value屬性卻在<option>標籤中很是重要,由於他們提供了目前選選擇框的選項(使用和非布爾複選框和單選框相似的手段)

使用類型做爲下拉列表:

<select th:field="*{type}">
  <option th:each="type : ${allTypes}" 
          th:value="${type}" 
          th:text="#{${'seedstarter.type.' + type}}">Wireframe</option>
</select>

這段代碼理解起來很容易,只是注意屬性優先級讓咱們能夠在option標籤內使用th:each屬性。

動態域

因爲SpringMVC的高級表單綁定功能,使得咱們可使用複雜的SpringEL表達式來綁定動態表單域到表單bean中。這將容許咱們在SeedStarter bean中建立一個新的Row對象,並將這個row的域添加到用戶請求的form中。

爲了作到這一點,咱們須要在控制器中提供一些新的映射方法,它將根據咱們的特定請求的參數來決定添加或刪除一行咱們定義的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";
}

如今給form添加一個動態table

<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>

嗯事實上,是不行的,他的問題是SpringEL表達式不執行數值中括號裏邊的表達式變量,索引執行上邊的語句時,會獲得一個錯誤的結果,就是字面形式的row[rowStat.index] (而不是row[0],row[1])而不是行集合中的正確位置,這就是爲何在這裏須要預處理。

讓咱們看看產生的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>

驗證和錯誤信息

讓咱們看看當有錯誤的時候如何給一個表單域一個CSS類:

<input type="text" th:field="*{datePlanted}" 
               th:class="${#fields.hasErrors('datePlanted')}? fieldError" />

能夠看到,#fields.hasErrors(...)函數接受一個表達式參數(datePlanted),返回一個布爾值告訴field該字段是否有驗證錯誤。

咱們能夠根據他們各自的field獲取全部的錯誤:

<ul>
  <li th:each="err : ${#fields.errors('datePlanted')}" th:text="${err}" />
</ul>

經過迭代,咱們可使用th:errors,一個專門用於建立一個經過制定選擇器篩選的錯誤列表的屬性,經過
分隔。

<input type="text" th:field="*{datePlanted}" />
<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(...)方法和#field.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>

創建一個以
分隔的列表:

<p th:if="${#fields.hasErrors('all')}" th:errors="*{all}">Incorrect date</p>

最後,注意#field.hasErrors("")等效的屬性#fields.hasAnyErrors()和#fields.errors()的等效的#fields.allErrors(),可使用喜歡的任何語法。

<div th:if="${#fields.hasAnyErrors()}">
  <p th:each="err : ${#fields.allErrors()}" th:text="${err}">...</p>
</div>

全局錯誤

Spring表單還有一種錯誤,全局錯誤,都是些不與窗體的任何特定字段關聯的錯誤。

Thymeleaf提供了一個global的常量來訪問這些錯誤。

<ul th:if="${#fields.hasErrors('global')}">
  <li th:each="err : ${#fields.errors('global')}" th:text="${err}">Input is incorrect</li>
</ul>
  • Incorrect date

以及等效的#field.hasGlobalErrors()和#field.globalErrors()方法。

<div th:if="${#fields.hasGlobalErrors()}">
  <p th:each="err : ${#fields.globalErrors()}" th:text="${err}">...</p>
</div>

在表單外部顯示錯誤

表單驗證錯誤也能夠在表單外部顯示,方法是經過變量(即${...})的內部選擇變量(*{...})增長表單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>

富錯誤對象

Thymeleaf提供了以bean的形式(代替單純的String)提供錯誤信息的能力,包括fieldName(String),message(String),和global(String)屬性的錯誤。

這些錯誤能夠經過工具方法#fields.datailedErrors()來實現:

<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>

它仍然是一個原型

如今程序已經好了,如今看一下建立的html模板頁面。

使用Thymeleaf框架的一大好處就是,全部這些功能加入到網頁後,網頁仍然可做爲原型使用(因此咱們說他是自然模板),打開瀏覽器,不執行程序直接運行seedstartermng.html:

能夠看到,雖然他沒有運行起來,不是一個有效的數據,但它是一個徹底有效的,能夠直接顯示的原型,試想一下,若是是jsp的話,那會怎樣呢?

轉換服務

配置

就像前文所說,Thymeleaf能夠在上下文中註冊一個轉換服務,再次看一下他的配置信息

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
  ...    
  <mvc:annotation-driven conversion-service="conversionService" />
  ...
  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService"
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>
  ...
</beans>

${{...}}語法

轉換服務能夠經過${{...}}語法很輕鬆的實現對象到字符串的轉換或格式化:

  • 變量語法${{...}}
  • 選擇變量語法*{{...}}

例如,將一個Integer型轉換爲字符串類型,並經過逗號來分隔:

<p th:text="${val}">...</p>
<p th:text="${{val}}">...</p>

返回結果爲:

<p>1234567890</p>
<p>1,234,567,890</p>

表單中使用

咱們以前看到的每個th:field屬性都將始終使用轉換服務:

<input type="text" th:field="*{datePlanted}" />

等效於:

<input type="text" th:field="*{{datePlanted}}" />

注意這是惟一一種在表達式中使用單大括號的轉換服務。

#conversions工具對象

conversions工具對象表達式容許手動執行轉換服務:

<p th:text="${'Val: ' + #conversions.convert(val,'String')}">...</p>

工具對象表達式的語法爲:

  • conversions.convert(Object,Class):將對象轉換爲指定的類

  • conversions.convert(Object,String):和上邊相同,可是指定的目標爲String類(java.lang包名能夠省略)

渲染片斷模板

Thymeleaf提供了將一個模板只渲染一部分,並做爲一個片斷返回的能力。

這是一個很是有用的組件化工具,好比,它能夠用於執行AJAX的Controller的調用,用於在已經加載的瀏覽器中返回一個片斷標籤(如用於更新選擇,啓用禁用按鈕等)。

片斷渲染可使用Thymeleaf的片斷規範:一個實現了org.thymeleaf.fragment.IFragmentSpec接口的對象。

最經常使用的一個實現是org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec類,它容許一個片斷規範包括以前說過的th:insert,th:replace使用DOM選擇器。

在視圖bean中指定片斷

視圖bean是在應用程序上下文中聲明的org.thymeleaf.spring4.view.ThymeleafView的bean,它容許這樣定義一個片斷:

<bean name="content-part" class="org.thymeleaf.spring4.view.ThymeleafView">
  <property name="templateName" value="index" />
  <property name="fragmentSpec">
    <bean class="org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec"
          c:selectorExpression="content" />
  </property>
</bean>

經過上邊的bean的定義,若是controller返回一個content-part(bean的名字),

@RequestMapping("/showContentPart")
public String showContentPart() {
    ...
    return "content-part";
}

Thymeleaf將只返回index模板的content片斷。一旦前綴後綴都設置並匹配,那麼它可能爲/WEB-INF/templates/index.html,

<!DOCTYPE html>
<html>
  ...
  <body>
    ...
    <div th:fragment="content">
      只有這裏渲染!!
    </div>
    ...
  </body>
</html>

另外應該注意到,由於Thymeleaf可使用DOM選擇器,全部咱們能夠不用任何th:fragment屬性,而只用id屬性來選擇一個片斷,如:

<bean name="content-part" class="org.thymeleaf.spring4.view.ThymeleafView">
  <property name="fragmentSpec">
    <bean class="org.thymeleaf.standard.fragment.StandardDOMSelectorFragmentSpec"
          c:selectorExpression="#content" />
  </property>
  <property name="templateName" value="index" />
</bean>

一樣完美的適用:

<!DOCTYPE html>
<html>
  ...
  <body>
    ...
    <div id="content">
       只有這裏渲染!!
    </div>
    ...
  </body>
</html>

經過控制權的返回值指定片斷

不聲明一個視圖bean,能夠從控制器本身就可使用與片斷相同的語法,相似於th:insert,th:rplace屬性等,如:

@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')";
}

先進的集成功能

與RequestDataValueProcessor集成

如今Thymeleaf無縫的與Spring的RequestDataValueProcessor接口集成,這個接口容許攔截連接URLS,表達URLS和表達域的值,以及爲了啓用安全,如抵禦CSRF而自動透明的添加一些隱藏域。

在應用的上下文中能夠簡單的配置RequestDataValueProcessor:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                     http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
 
    ...
 
    <bean name="requestDataValueProcessor"
          class="net.example.requestdata.processor.MyRequestDataValueProcessor" />
 
</beans>

Thymeleaf將經過這種方式使用它:

  • 在渲染URL以前,th:href和th:src將會調用RequestDataValueProcessor.processUrl(...)
  • 在渲染表單的action屬性以前,th:action會調用RequestDataValueProcessor.processAction(...),另外他會檢查
    標籤,由於通常來講這是使用action的惟一一個地方,而且在的關閉標籤
    以前執行RequstDataValueProcessor.getExtraHiddenFields(...)用來新增返回的hidden域。
  • 在渲染value屬性以前,th:value會調用RequestDataProcessor.processFormFieldValue(...),除非在這個標籤中存在了th:field(這時候th:field屬性起做用)
  • 當存在th:field的時候,在渲染value屬性以前會調用RequestDataValueProcessor.processFormFieldValue(...)處理這個屬性值(<textarea>處理內容值)

此功能只有Spring3.x之後使用

綁定地址到Controller

在Spring4.1以後的版本中,Spring容許經過註解直接從從視圖連接到控制器,而不須要知道這些控制器映射的URI.

在Thymeleaf中能夠經過#mvc.url(...)表達式方法調用Controller中符合駝峯命名規則的方法(get,set),調用的方式爲方法的名字,即至關於jsp的spring:mvcUrl(...)自定義方法。

好比

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()}">獲取Data參數</a>
<a th:href="${(#mvc.url('EC#getDataParam').arg(0,'internal')).build()}">獲取Data參數</a>

查閱更多這種機制能夠查看這裏

Spring WebFlow的集成

基礎配置

Thymeleaf-spring4集成包包括與Spring WebFlow 2.3.x的集成

WebFlow包括當特定的事件(過渡)被觸發時渲染頁面片斷的一些Ajax的功能,將來讓Thymeleaf參加這些Ajax請求,咱們將使用一個不經過的視圖解析器的實現,它這樣配置:

<bean id="thymeleafViewResolver" class="org.thymeleaf.spring4.view.AjaxThymeleafViewResolver">
    <property name="viewClass" value="org.thymeleaf.spring4.view.FlowAjaxThymeleafView" />
    <property name="templateEngine" ref="templateEngine" />
</bean>

而後在ViewResolver中配置WebFlow的ViewFactoryCreator.

<bean id="mvcViewFactoryCreator" 
      class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
    <property name="viewResolvers" ref="thymeleafViewResolver"/>
</bean>

在這裏能夠指定模板的視圖狀態

<view-state id="detail" view="bookingDetail">
     ...
</view-state>

在上邊的實例中,bookingDetail是Thymeleaf模板一般使用的一個方式,是模板引擎內任何模板解析器均可以懂的

Ajax片斷

WebFlow的片斷規範容許片斷經過 標籤呈現,就像這樣:

<view-state id="detail" view="bookingDetail">
    <transition on="updateData">
        <render fragments="hoteldata"/>
    </transition>
</view-state>

這些片斷(即hoteldata)能夠是逗號分隔的列表標記在th:fragment標籤中。

<div id="data" th:fragment="hoteldata">
    這裏內容替換
</div>

永遠記住,指定的片斷必須有一個id屬性,這樣瀏覽器運行的Spring
JavaScript庫才能對標籤進行替換。

標籤,也能夠經過DOM選擇器設定:

<view-state id="detail" view="bookingDetail">
    <transition on="updateData">
        <render fragments="[//div[@id='data']]"/>
    </transition>
</view-state>

這將意味着th:fragment不在須要:

<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>
相關文章
相關標籤/搜索