A modern Java templating language
Thymeleaf
Andre Azzolini!
Broadleaf Commerce!
FW JUG - July 2nd, 2014
Introduction
Who am I?!
‣ Graduated from The University of Texas at Austin (Computer Science)!
‣ Consultant at Credera!
‣ Senior Engineer at Broadleaf Commerce!
!
What is Broadleaf Commerce?!
‣ Open source Java enterprise eCommerce framework!
‣ Focus on extensibility and scalability!
‣ Based on Spring, Hibernate, Thymeleaf
‣ Why use Thymeleaf?!
‣ Thymeleaf Basics!
‣ Intermediate Thymeleaf!
‣ Existing Ecosystem!
‣ Broadleaf Use Cases
Agenda
‣ Not compiled —> short feedback loop!
‣ Natural templating —> closer to designers’ HTML!
‣ Modular architecture —> hooks for customization
Why use Thymeleaf?
Thymeleaf Basics
Installation in a Spring application
<bean id="templateResolver"
class="org.thymeleaf...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.spring3.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
!
<bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<property name="order" value="1" />
<property name="viewNames" value="*.html" />
</bean>
Outputting Values
———— HomeController.java ————
!
@RequestMapping("/")
public String viewHomepage(Model model) {
model.addAttribute("header", "My Header Message");
return "index";
}
!
———— /WEB-INF/templates/index.html ————
!
<span th:text="${header}">A Default Header</span>
!
———— Browser rendering ————
!
<span>A Default Header</span>
!
———— Thymeleaf rendering ————
!
<span>My Header Message</span>
Outputting Values (i18n)
———— /WEB-INF/templates/index.html ————
!
<span th:text="#{homepage.header}">A Default Header</span>
!
———— messages_en.properties ————
!
homepage.header=My Header Message
!
———— messages_es.properties ————
!
homepage.header=Mi Mensaje de Cabecera
!
———— Thymeleaf rendering (en) ————
!
<span>My Header Message</span>
!
———— Thymeleaf rendering (es) ————
!
<span>Mi Mensaje de Cabecera</span>
Outputting Values (i18n cont.)
———— HomeController.java ————
!
@RequestMapping("/")
public String viewHomepage(Model model) {
model.addAttribute("header", "homepage.header");
return "index";
}
!
———— /WEB-INF/templates/index.html ————
!
<span th:text="#{${header}}" />
Outputting Raw Values
———— HomeController.java ————
!
@RequestMapping("/")
public String viewHomepage(Model model) {
model.addAttribute("header", "<b>My Bolded Header</b>");
return "index";
}
!
———— Rendered with th:text ————
!
<span>&lt;b&gt;My Bolded Header&lt;/b&gt;</span>
!
———— Rendered with th:utext ————
!
<span><b>My Bolded Header</b></span>
Outputting Values Inline
———— index.html ————
!
<span th:inline="text">
[[${address.user?.firstName}]] [[${address.user?.lastName}]]<br />
[[${address.line1}]] <br />
[[${address.line2}]] <br />
[[${address.city}]], [[${address.state}]] [[${address.zipcode}]]
</span>
Scoping available variables
———— index.html ————
!
<span th:inline="text" th:object="${address}">
[[*{user?.firstName}]] [[${user?.lastName}]]<br />
[[*{line1}]] <br />
[[*{line2}]] <br />
[[*{city}]], [[*{state}]] [[*{zipcode}]]
</span>
Attribute Manipulation
———— index.html ————
!
<span th:class="${isEven ? 'even' : 'odd'}"
th:classappend="${isError ? 'error'}"
th:attr="data-row-id=${row.id}"
th:text="${row.description}" />
———— Rendered index.html ————
!
<span class="odd error" data-row-id="5">Row 5 Description</span>
Conditionals
———— index.html ————
!
<span th:if="${user.loggedIn}" th:text="${'Hi ' + user.name}" />
<span th:unless="${user.loggedIn}">Welcome guest</span>
!
———— messages_en.properties ————
!
header.greeting.user=Hi {0}
header.greeting.anonymous=Welcome guest
!
———— index.html (i18n) ————
!
<span th:if="…" th:text="#{header.greeting.user(${user.name})}" />
<span th:unless="…" th:text="#{header.greeting.anonymous}" />
Loops and Links
———— HomeController.java ————
!
@RequestMapping("/products")
public String viewProductListing(Model model) {
List<Product> products = productService.findAll();
model.addAttribute("products", products);
return "productListing";
}
!
———— productListing.html ————
!
<ul>
<li th:each="product : ${products}" th:object="${product}">
<img th:src="@{*{mainImage}}" /> <br />
<a th:href="@{*{link}}" th:text="*{name}" />
</li>
</ul>
Includes
———— productListing.html ————
!
<ul>
<li th:each="product : ${products}"
th:object="${product}"
th:include="components/productBlock" />
</ul>
!
———— components/productBlock.html ————

!
<img th:src="@{*{mainImage}}" /> <br />
<a th:href="@{*{link}}" th:text="*{name}" />
Variable Expressions
———— HomeController.java ————
!
@RequestMapping("/products")
public String viewProductListing(Model model) {
List<Product> products = productService.findAll();
model.addAttribute("products", products);
return "productListing";
}
!
———— productListing.html ————
!
<span th:text="${'Products Found: ' + #lists.size(products)}" />
‣ format(date, 'dd/MMM/yyyy HH:mm')!
‣ day(date), month(date), dayOfWeek(date), etc!
‣ create(year, month, day)!
‣ createNow(), createToday()
#dates Variable Expression
‣ formatInteger(num, 3, 'POINT')!
‣ sequence(from, to)!
‣ sequence(from, to, step)
#numbers Variable Expression
‣ isEmpty!
‣ contains!
‣ indexOf, substring, replace!
‣ prepend, append!
‣ toLowerCase, toUpperCase, capitalize, capitalizeWords!
‣ escapeXml, unescapeJava
#strings Variable Expression
‣ isEmpty, size!
‣ contains, containsAll!
‣ sort
#arrays, #lists, #sets, #maps Variable Expressions
#aggregates Variable Expression
———— Order.java ————
!
public class Order {
protected List<OrderLine> lines;
}
!
class OrderLine {
protected BigDecimal price;
protected int qty;
}
!
———— order.html ————
!
<span th:text="${#aggregates.sum(order.lines.{price * qty})}" />
Intermediate Thymeleaf
Custom Variable Expressions
———— SystemPropertyVariableExpression.java ————
!
@Resource protected SystemPropertyService service;
!
public String getName() {
return "sp";
}
!
public boolean getAsBoolean(String prop) {
return service.resolveAsBoolean(prop);
}
!
———— index.html ————
!
<span th:if="${#sp.getAsBoolean('pagination.enabled')}"
th:include="components/paginator" />
Spring Bean Direct Access
———— MyService.java ————
!
@Service
public class MyService {
!
public boolean isPalindrome(String str) {
return str.equals(StringUtils.reverse(str));
}
!
}
!
———— index.html ————
!
<span th:if="${@myService.isPalindrome('tacocat')}" />
Custom Processors
———— PriceProcessor.java ————
!
public class PriceProcessor extends
AbstractTextChildModifierAttrProcessor {
public PriceTextDisplayProcessor() {
super("price");
}
!
protected String getText(Arguments arguments, Element element,
String attributeName) {
Expression expression = ...
Object result = expression.execute(...);
if (result instanceof Money) {
return ((Money) result).getFormattedValue();
} else {
return result.toString();
}
}
}
Custom Processors (cont.)
———— MyCustomDialect.java ————
public class MyCustomDialect extends AbstractDialect {
private Set<IProcessor> processors = new HashSet<IProcessor>();
@Override public String getPrefix() {
return "mcd";
}
}
!
———— applicationContext.xml ————
!
<bean id="myCustomDialect" class="com.myco.MyCustomDialect">
<property name="processors">
<set>
<bean id="priceProcessor" class="com.myco.PriceProcessor">
</set>
</property>
</bean>
Custom Processors (cont.)
———— applicationContext.xml (cont.) ————
!
<bean id="templateEngine" class="...">
<property name="additionalDialects">
<set>
<bean class="com.myco.MyCustomDialect"/>
</set>
</property>
</bean>
!
———— components/productBlock.html ————

!
<img th:src="@{*{mainImage}}" /> <br />
<a th:href="@{*{link}}" th:text="*{name}" />
<span mcd:price="*{price}" />
Custom Processors (cont.)
public class FormProcessor extends AbstractElementProcessor {
!
protected ProcessorResult processElement(Arguments a, Element el) {
String method = el.getAttributeValue("method");
if (!"GET".equals(method)) {
String csrfToken = protectionService.getCsrfToken();
Element csrfNode = new Element("input");
csrfNode.setAttribute("type", "hidden");
csrfNode.setAttribute("name", "csrf-token");
csrfNode.setAttribute("value", csrfToken);
el.addChild(csrfNode);
}
!
Element newForm = el.cloneElementNode(...);
el.getParent().insertAfter(el, newForm);
el.getParent().removeChild(el);
return ProcessorResult.OK;
}
}
Custom Processors (cont.)
———— login.html ————
!
<mcd:form>
<input type="text" name="username" />
<input type="password" name="pass" />
</mcd:form>
!
———— Rendered login page ————
!
<form>
<input type="text" name="username" />
<input type="password" name="pass" />
<input type="hidden" name="csrf-token" value="L9ThxnotKPzthJ" />
</form>
Spring Form Binding
———— UserRegistrationForm.java ————
!
public class UserRegistrationForm {
!
protected String username;
protected String password;
protected String confirmPassword;
protected String email;
!
... getters / setters ...
!
}
Spring Form Binding (cont.)
———— UserRegistrationController.java ————
!
public class UserRegistrationController {
!
@RequsetMapping("/register", method = RequestMethod.GET)
public String showRegisterForm(Model model,
@ModelAttribute UserRegistrationForm registerForm) {
return "components/userRegistrationForm";
}
!
@RequsetMapping("/register", method = RequestMethod.POST)
public String showRegisterForm(Model model,
@ModelAttribute UserRegistrationForm registerForm) {
// register the user
return "redirect:/";
}
!
}
Spring Form Binding (cont.)
———— components/userRegistrationForm.html ————
!
<form th:action="@{/register}" th:object="${registerForm}"
method="POST">
<input type="text" th:field="*{username}" />
<input type="password" th:field="*{password}" />
<input type="password" th:field="*{confirmPassword}" />
<input type="text" th:field="*{email}" />
</form>
Existing Ecosystem
‣ Code completion for out of box processors!
‣ Content assist inside expressions!
‣ Ability to provide completion for custom processors
Eclipse IDE Plugin
‣ Use Thymeleaf templates in Tiles definitions!
‣ Mix JSP and Thymeleaf templates!
‣ Optional Spring MVC 3 and Spring Web Flow 2.3
integrations
Thymeleaf + Apache Tiles 2
‣ Lightweight dialect approach instead of Tiles!
‣ Uses decorators and fragments within the templates, so
there is no need for a Tiles definition file!
‣ Created by a core Thymeleaf contributor!
‣ I think it's more intuitive than the Tiles plugin
Layout Dialect (unofficial)
‣ sec:authorize processor (accepts normal Spring Security
expressions like hasRole('ROLE_ADMIN'))!
‣ Grab the current authentication object in expressions:
${#authentication.name}!
Spring Security 3
‣ Allows you to specify a cache attribute on a DOM
element!
‣ Caches the resulting HTML with the provided name!
‣ Doesn't require Thymeleaf to process the DOM for that
node when rendering
Cache Dialect (unofficial)
<ul cache:name="productsList" cache:ttl="60">
<li th:each="product : ${products}"
th:include="components/productBlock" />
</ul>
‣ JavaScript library for natural templating!
‣ Can process th:include without executing in an
application!
‣ Provides features for evaluating conditionals while
statically prototyping
Thymol (unofficial)
Broadleaf Use Cases
‣ Serve individual files in development, but a bundle in
production!
‣ Handle expiration of bundles!
‣ Dynamically modify contents of certain resources
JS / CSS Bundling
<blc:bundle name="admin.js"
files="BLC.js,
BLC-system-property.js,
blc-dates.js" />
‣ Intelligent auto-generation of cache keys!
‣ Provide ability to alter a portion of the cached,
generated HTML!
‣ Invalidate cache elements when things change in the
admin
Advanced Caching Strategy
‣ Allow users to modify templates through the admin
tool!
‣ Serve templates directly from the database!
‣ No need for compiling the templates like we would
have to with JSP
Database Template Resolution
Thanks!

Broadleaf Presents Thymeleaf

  • 1.
    A modern Javatemplating language Thymeleaf Andre Azzolini! Broadleaf Commerce! FW JUG - July 2nd, 2014
  • 2.
    Introduction Who am I?! ‣Graduated from The University of Texas at Austin (Computer Science)! ‣ Consultant at Credera! ‣ Senior Engineer at Broadleaf Commerce! ! What is Broadleaf Commerce?! ‣ Open source Java enterprise eCommerce framework! ‣ Focus on extensibility and scalability! ‣ Based on Spring, Hibernate, Thymeleaf
  • 3.
    ‣ Why useThymeleaf?! ‣ Thymeleaf Basics! ‣ Intermediate Thymeleaf! ‣ Existing Ecosystem! ‣ Broadleaf Use Cases Agenda
  • 4.
    ‣ Not compiled—> short feedback loop! ‣ Natural templating —> closer to designers’ HTML! ‣ Modular architecture —> hooks for customization Why use Thymeleaf?
  • 5.
  • 6.
    Installation in aSpring application <bean id="templateResolver" class="org.thymeleaf...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.spring3.SpringTemplateEngine"> <property name="templateResolver" ref="templateResolver" /> </bean> ! <bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver"> <property name="templateEngine" ref="templateEngine" /> <property name="order" value="1" /> <property name="viewNames" value="*.html" /> </bean>
  • 7.
    Outputting Values ———— HomeController.java———— ! @RequestMapping("/") public String viewHomepage(Model model) { model.addAttribute("header", "My Header Message"); return "index"; } ! ———— /WEB-INF/templates/index.html ———— ! <span th:text="${header}">A Default Header</span> ! ———— Browser rendering ———— ! <span>A Default Header</span> ! ———— Thymeleaf rendering ———— ! <span>My Header Message</span>
  • 8.
    Outputting Values (i18n) ————/WEB-INF/templates/index.html ———— ! <span th:text="#{homepage.header}">A Default Header</span> ! ———— messages_en.properties ———— ! homepage.header=My Header Message ! ———— messages_es.properties ———— ! homepage.header=Mi Mensaje de Cabecera ! ———— Thymeleaf rendering (en) ———— ! <span>My Header Message</span> ! ———— Thymeleaf rendering (es) ———— ! <span>Mi Mensaje de Cabecera</span>
  • 9.
    Outputting Values (i18ncont.) ———— HomeController.java ———— ! @RequestMapping("/") public String viewHomepage(Model model) { model.addAttribute("header", "homepage.header"); return "index"; } ! ———— /WEB-INF/templates/index.html ———— ! <span th:text="#{${header}}" />
  • 10.
    Outputting Raw Values ————HomeController.java ———— ! @RequestMapping("/") public String viewHomepage(Model model) { model.addAttribute("header", "<b>My Bolded Header</b>"); return "index"; } ! ———— Rendered with th:text ———— ! <span>&lt;b&gt;My Bolded Header&lt;/b&gt;</span> ! ———— Rendered with th:utext ———— ! <span><b>My Bolded Header</b></span>
  • 11.
    Outputting Values Inline ————index.html ———— ! <span th:inline="text"> [[${address.user?.firstName}]] [[${address.user?.lastName}]]<br /> [[${address.line1}]] <br /> [[${address.line2}]] <br /> [[${address.city}]], [[${address.state}]] [[${address.zipcode}]] </span>
  • 12.
    Scoping available variables ————index.html ———— ! <span th:inline="text" th:object="${address}"> [[*{user?.firstName}]] [[${user?.lastName}]]<br /> [[*{line1}]] <br /> [[*{line2}]] <br /> [[*{city}]], [[*{state}]] [[*{zipcode}]] </span>
  • 13.
    Attribute Manipulation ———— index.html———— ! <span th:class="${isEven ? 'even' : 'odd'}" th:classappend="${isError ? 'error'}" th:attr="data-row-id=${row.id}" th:text="${row.description}" /> ———— Rendered index.html ———— ! <span class="odd error" data-row-id="5">Row 5 Description</span>
  • 14.
    Conditionals ———— index.html ———— ! <spanth:if="${user.loggedIn}" th:text="${'Hi ' + user.name}" /> <span th:unless="${user.loggedIn}">Welcome guest</span> ! ———— messages_en.properties ———— ! header.greeting.user=Hi {0} header.greeting.anonymous=Welcome guest ! ———— index.html (i18n) ———— ! <span th:if="…" th:text="#{header.greeting.user(${user.name})}" /> <span th:unless="…" th:text="#{header.greeting.anonymous}" />
  • 15.
    Loops and Links ————HomeController.java ———— ! @RequestMapping("/products") public String viewProductListing(Model model) { List<Product> products = productService.findAll(); model.addAttribute("products", products); return "productListing"; } ! ———— productListing.html ———— ! <ul> <li th:each="product : ${products}" th:object="${product}"> <img th:src="@{*{mainImage}}" /> <br /> <a th:href="@{*{link}}" th:text="*{name}" /> </li> </ul>
  • 16.
    Includes ———— productListing.html ———— ! <ul> <lith:each="product : ${products}" th:object="${product}" th:include="components/productBlock" /> </ul> ! ———— components/productBlock.html ————
 ! <img th:src="@{*{mainImage}}" /> <br /> <a th:href="@{*{link}}" th:text="*{name}" />
  • 17.
    Variable Expressions ———— HomeController.java———— ! @RequestMapping("/products") public String viewProductListing(Model model) { List<Product> products = productService.findAll(); model.addAttribute("products", products); return "productListing"; } ! ———— productListing.html ———— ! <span th:text="${'Products Found: ' + #lists.size(products)}" />
  • 18.
    ‣ format(date, 'dd/MMM/yyyyHH:mm')! ‣ day(date), month(date), dayOfWeek(date), etc! ‣ create(year, month, day)! ‣ createNow(), createToday() #dates Variable Expression
  • 19.
    ‣ formatInteger(num, 3,'POINT')! ‣ sequence(from, to)! ‣ sequence(from, to, step) #numbers Variable Expression
  • 20.
    ‣ isEmpty! ‣ contains! ‣indexOf, substring, replace! ‣ prepend, append! ‣ toLowerCase, toUpperCase, capitalize, capitalizeWords! ‣ escapeXml, unescapeJava #strings Variable Expression
  • 21.
    ‣ isEmpty, size! ‣contains, containsAll! ‣ sort #arrays, #lists, #sets, #maps Variable Expressions
  • 22.
    #aggregates Variable Expression ————Order.java ———— ! public class Order { protected List<OrderLine> lines; } ! class OrderLine { protected BigDecimal price; protected int qty; } ! ———— order.html ———— ! <span th:text="${#aggregates.sum(order.lines.{price * qty})}" />
  • 23.
  • 24.
    Custom Variable Expressions ————SystemPropertyVariableExpression.java ———— ! @Resource protected SystemPropertyService service; ! public String getName() { return "sp"; } ! public boolean getAsBoolean(String prop) { return service.resolveAsBoolean(prop); } ! ———— index.html ———— ! <span th:if="${#sp.getAsBoolean('pagination.enabled')}" th:include="components/paginator" />
  • 25.
    Spring Bean DirectAccess ———— MyService.java ———— ! @Service public class MyService { ! public boolean isPalindrome(String str) { return str.equals(StringUtils.reverse(str)); } ! } ! ———— index.html ———— ! <span th:if="${@myService.isPalindrome('tacocat')}" />
  • 26.
    Custom Processors ———— PriceProcessor.java———— ! public class PriceProcessor extends AbstractTextChildModifierAttrProcessor { public PriceTextDisplayProcessor() { super("price"); } ! protected String getText(Arguments arguments, Element element, String attributeName) { Expression expression = ... Object result = expression.execute(...); if (result instanceof Money) { return ((Money) result).getFormattedValue(); } else { return result.toString(); } } }
  • 27.
    Custom Processors (cont.) ————MyCustomDialect.java ———— public class MyCustomDialect extends AbstractDialect { private Set<IProcessor> processors = new HashSet<IProcessor>(); @Override public String getPrefix() { return "mcd"; } } ! ———— applicationContext.xml ———— ! <bean id="myCustomDialect" class="com.myco.MyCustomDialect"> <property name="processors"> <set> <bean id="priceProcessor" class="com.myco.PriceProcessor"> </set> </property> </bean>
  • 28.
    Custom Processors (cont.) ————applicationContext.xml (cont.) ———— ! <bean id="templateEngine" class="..."> <property name="additionalDialects"> <set> <bean class="com.myco.MyCustomDialect"/> </set> </property> </bean> ! ———— components/productBlock.html ————
 ! <img th:src="@{*{mainImage}}" /> <br /> <a th:href="@{*{link}}" th:text="*{name}" /> <span mcd:price="*{price}" />
  • 29.
    Custom Processors (cont.) publicclass FormProcessor extends AbstractElementProcessor { ! protected ProcessorResult processElement(Arguments a, Element el) { String method = el.getAttributeValue("method"); if (!"GET".equals(method)) { String csrfToken = protectionService.getCsrfToken(); Element csrfNode = new Element("input"); csrfNode.setAttribute("type", "hidden"); csrfNode.setAttribute("name", "csrf-token"); csrfNode.setAttribute("value", csrfToken); el.addChild(csrfNode); } ! Element newForm = el.cloneElementNode(...); el.getParent().insertAfter(el, newForm); el.getParent().removeChild(el); return ProcessorResult.OK; } }
  • 30.
    Custom Processors (cont.) ————login.html ———— ! <mcd:form> <input type="text" name="username" /> <input type="password" name="pass" /> </mcd:form> ! ———— Rendered login page ———— ! <form> <input type="text" name="username" /> <input type="password" name="pass" /> <input type="hidden" name="csrf-token" value="L9ThxnotKPzthJ" /> </form>
  • 31.
    Spring Form Binding ————UserRegistrationForm.java ———— ! public class UserRegistrationForm { ! protected String username; protected String password; protected String confirmPassword; protected String email; ! ... getters / setters ... ! }
  • 32.
    Spring Form Binding(cont.) ———— UserRegistrationController.java ———— ! public class UserRegistrationController { ! @RequsetMapping("/register", method = RequestMethod.GET) public String showRegisterForm(Model model, @ModelAttribute UserRegistrationForm registerForm) { return "components/userRegistrationForm"; } ! @RequsetMapping("/register", method = RequestMethod.POST) public String showRegisterForm(Model model, @ModelAttribute UserRegistrationForm registerForm) { // register the user return "redirect:/"; } ! }
  • 33.
    Spring Form Binding(cont.) ———— components/userRegistrationForm.html ———— ! <form th:action="@{/register}" th:object="${registerForm}" method="POST"> <input type="text" th:field="*{username}" /> <input type="password" th:field="*{password}" /> <input type="password" th:field="*{confirmPassword}" /> <input type="text" th:field="*{email}" /> </form>
  • 34.
  • 35.
    ‣ Code completionfor out of box processors! ‣ Content assist inside expressions! ‣ Ability to provide completion for custom processors Eclipse IDE Plugin
  • 36.
    ‣ Use Thymeleaftemplates in Tiles definitions! ‣ Mix JSP and Thymeleaf templates! ‣ Optional Spring MVC 3 and Spring Web Flow 2.3 integrations Thymeleaf + Apache Tiles 2
  • 37.
    ‣ Lightweight dialectapproach instead of Tiles! ‣ Uses decorators and fragments within the templates, so there is no need for a Tiles definition file! ‣ Created by a core Thymeleaf contributor! ‣ I think it's more intuitive than the Tiles plugin Layout Dialect (unofficial)
  • 38.
    ‣ sec:authorize processor(accepts normal Spring Security expressions like hasRole('ROLE_ADMIN'))! ‣ Grab the current authentication object in expressions: ${#authentication.name}! Spring Security 3
  • 39.
    ‣ Allows youto specify a cache attribute on a DOM element! ‣ Caches the resulting HTML with the provided name! ‣ Doesn't require Thymeleaf to process the DOM for that node when rendering Cache Dialect (unofficial) <ul cache:name="productsList" cache:ttl="60"> <li th:each="product : ${products}" th:include="components/productBlock" /> </ul>
  • 40.
    ‣ JavaScript libraryfor natural templating! ‣ Can process th:include without executing in an application! ‣ Provides features for evaluating conditionals while statically prototyping Thymol (unofficial)
  • 41.
  • 42.
    ‣ Serve individualfiles in development, but a bundle in production! ‣ Handle expiration of bundles! ‣ Dynamically modify contents of certain resources JS / CSS Bundling <blc:bundle name="admin.js" files="BLC.js, BLC-system-property.js, blc-dates.js" />
  • 43.
    ‣ Intelligent auto-generationof cache keys! ‣ Provide ability to alter a portion of the cached, generated HTML! ‣ Invalidate cache elements when things change in the admin Advanced Caching Strategy
  • 44.
    ‣ Allow usersto modify templates through the admin tool! ‣ Serve templates directly from the database! ‣ No need for compiling the templates like we would have to with JSP Database Template Resolution
  • 45.