asis.garcia@trabesoluciones.com marcos.abel@trabesoluciones.com
NOTA: parte de los materiales de esta charla están tomados y/o adaptados (con permiso del autor) de la documentación oficial del proyecto disponible en http://www.thymeleaf.org/
Maneras de generar HTML
Las plantillas de la aplicación deberían ser prototipos válidos para trabajar sobre ellos e idealmente visualizables en un navegador sin necesidad de ser procesadas.
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Spring MVC view layer: Thymeleaf vs. JSP</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all"
href="<s:url value='/css/thvsjsp.css' />"/>
</head>
<body>
<h2>This is a JSP</h2>
<s:url var="formUrl" value="/subscribejsp" />
<sf:form modelAttribute="subscription" action="${formUrl}">
<div>
<label for="email"><s:message code="subscription.email" />:
</label>
<sf:input path="email" />
</div>
<div>
<label><s:message code="subscription.type" />: </label>
<ul>
<c:forEach var="type" items="${allTypes}" varStatus="typeStatus">
<li>
<sf:radiobutton path="subscriptionType" value="${type}" />
<label for="subscriptionType${typeStatus.count}">
<s:message code="subscriptionType.${type}" />
</label>
</li>
</c:forEach>
</ul>
</div>
<div class="submit">
<button type="submit" name="save">
<s:message code="subscription.submit" />
</button>
</div>
</sf:form>
</body>
</html>
<DOCCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring MVC view layer: Thymeleaf vs. JSP</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all"
href="../../css/thvsjsp.css" th:href="@{/css/thvsjsp.css}"/>
</head>
<body>
<h2>This is a Thymeleaf template</h2>
<form action="#" th:object="${subscription}"
th:action="@{/subscribeth}">
<div>
<label for="email" th:text="#{subscription.email}">Email:</label>
<input type="text" th:field="*{email}" />
</div>
<div>
<label th:text="#{subscription.type}">Type: </label>
<ul>
<li th:each="type : ${allTypes}">
<input type="radio" th:field="*{subscriptionType}"
th:value="${type}" />
<label th:for="${#ids.prev('subscriptionType')}"
th:text="#{'subscriptionType.'+${type}}">First type</label>
</li>
<li th:remove="all"><input type="radio" />
<label>Second Type</label>
</li>
</ul>
</div>
<div class="submit">
<button type="submit" name="save" th:text="#{subscription.submit}">
Subscribe me!
</button>
</div>
</form>
</body>
</html>
Tecnología en crecimiento, promocionada directamente por spring.io
<table>
<thead>
<tr>
<th th:text="#{msgs.headers.name}">Name</th>
<th th:text="#{msgs.headers.price}">Price</th>
</tr>
</thead>
<tbody>
<tr th:each="prod : ${allProducts}">
<td th:text="${prod.name}">Oranges</td>
<td th:text="${#numbers.formatDecimal(prod.price,1,2)}">0.99</td>
</tr>
</tbody>
</table>
<table>
<tr data-th-each="user : ${users}">
<td data-th-text="${user.login}">...</td>
<td data-th-text="${user.name}">...</td>
</tr>
</table>
public interface IContext {
public VariablesMap<String,Object> getVariables();
public Locale getLocale();
...
}
public interface IWebContext extends IContext {
public HttpSerlvetRequest getHttpServletRequest();
public HttpSession getHttpSession();
public ServletContext getServletContext();
public VariablesMap<String,String[]> getRequestParameters();
public VariablesMap<String,Object> getRequestAttributes();
public VariablesMap<String,Object> getSessionAttributes();
public VariablesMap<String,Object> getApplicationAttributes();
}
WebContext ctx = new WebContext(request, response,
servletContext, request.getLocale());
Añade los atributos de la request al mapa de variables del contexto
Añade las siguientes variables de contexto:
<div th:text="${session.user.name}"/>
<li th:each="book : ${books}">
Se ejecutan sobre un contexto previamente definido
<span th:text="*{book.author}"/>
<div th:object="${book}">
<span th:text="*{title}"></span>
</div>
Se sustituyen por mensajes localizados, típicamente definidos en ficheros .properties
<span th:text="#{app.messages.title}"></span>
Reescritura de urls, con soporte para parámetros y URLs relativas o absolutas
<form th:action="@{/createOrder(id=${orderId})}">
<a href="main.html" th:href="@{../main.html}">
</form>
sustituyen el contenido de un tag por el resultado de evaluar la expresión que contenga el atributo.
home.welcome=Welcome to our <b>fantastic</b> grocery store!
<!-- escapando -->
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
<p>Welcome to our <b>fantastic</b> grocery store!</p>
<!-- sin escapar -->
<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
<p>Welcome to our <b>fantastic</b> grocery store!</p>
sustituye el valor de uno o más atributos del tag.
<input type="submit" value="Subscribe me!"
th:attr="value=#{subscribe.submit}"/>
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
<input type="submit" value="Subscribe me!"
th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
<li>
<a href="product/list.html" th:href="@{/product/list}">
Product List
</a>
</li>
<input type="button" class="btn"
th:attrappend="class=${' ' + cssStyle}" />
<input type="button" class="btn"
th:attrprepend="class=${cssStyle + ' '}" />
<tr th:each="prod : ${prods}" class="row"
th:classappend="${prodStat.odd}? 'odd'">
th:abbr th:accept th:accept-charset th:accesskey th:action th:align th:alt th:archive th:audio th:autocomplete th:axis th:background th:bgcolor th:border th:cellpadding th:cellspacing th:challenge th:charset th:cite th:class th:classid th:codebase th:codetype th:cols th:colspan th:compact th:content th:contenteditable th:contextmenu th:data th:datetime th:dir th:draggable th:dropzone th:enctype th:for th:form th:formaction th:formenctype th:formmethod th:formtarget th:frame th:frameborder th:headers th:height th:high th:href th:hreflang th:hspace th:http-equiv th:icon th:id th:keytype th:kind th:label
th:lang th:list th:longdesc th:low th:manifest th:marginheight th:marginwidth th:max th:maxlength th:media th:method th:min th:name th:optimum th:pattern th:placeholder th:poster th:preload th:radiogroup th:rel th:rev th:rows th:rowspan th:rules th:sandbox th:scheme th:scope th:scrolling th:size th:sizes th:span th:spellcheck th:src th:srclang th:standby th:start th:step th:style th:summary th:tabindex th:target th:title th:type th:usemap th:value th:valuetype th:vspace th:width th:wrap th:xmlbase th:xmllang th:xmlspace
<input type="checkbox" name="active" th:checked="${user.active}" />
<input type="checkbox" name="active" checked="checked" />
Otros atributos disponibles:
th:async th:autofocus th:autoplay th:checked th:controls
th:declare th:default th:defer th:disabled th:formnovalidate
th:hidden th:ismap th:loop th:multiple th:novalidate
th:nowrap th:open th:pubdate th:readonly th:required
th:reversed th:scoped th:seamless th:selected
ProductService productService = new ProductService();
List<Product> allProducts = productService.findAll();
WebContext ctx = new WebContext(request, servletContext,
request.getLocale());
ctx.setVariable("prods", allProducts);
templateEngine.process("product/list", ctx, response.getWriter());
<table>
<tr>
<th>NAME</th>
<th>PRICE</th>
<th>IN STOCK</th>
</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>
</table>
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<a href="comments.html"
th:href="@{/comments(prodId=${prod.id})}"
th:unless="${#lists.isEmpty(prod.comments)}">view</a>
<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>
<!DOCTYPE html SYSTEM
"http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
</body>
</html>
<body>
<div th:include="layout :: copy"></div>
</body>
<!DOCTYPE html SYSTEM
"http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<footer id="copy-id">
© 2011 The Good Thymes Virtual Grocery
</footer>
</body>
</html>
<body>
<div th:include="layout :: [#copy-id]"></div>
</body>
<!DOCTYPE html SYSTEM
"http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<body>
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
</body>
</html>
<body>
...
<div th:include="layout :: copy"></div>
<div th:replace="layout :: copy"></div>
</body>
<body>
...
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</body>
<div th:fragment="frag (onevar,twovar)">
<p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>
incluir un fragmento con parámetros:
<div th:include="::frag (${value1},${value2})">...</div>
<div th:include="::frag (onevar=${value1},twovar=${value2})">...</div>
<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>
</tbody>
Distintos modos de funcionamiento
<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" />
<property name="order" value="1" />
<property name="viewNames" value="*.html,*.xhtml" />
</bean>
...
@RequestMapping("/")
public String home() {
...
return "home";
}
...
<DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
</head>
<body>
<h2>Home!</h2>
</body>
</html>