Upload
broadleaf-commerce
View
1.788
Download
7
Tags:
Embed Size (px)
DESCRIPTION
Broadleaf's Andre Azzolini presents Thymeleaf at July 2014 Fort Worth Java Users Group (FW JUG) meeting.
Citation preview
A modern Java templating language
Thymeleaf Andre Azzolini!Broadleaf Commerce!FW JUG - July 2nd, 2014
IntroductionWho 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><b>My Bolded Header</b></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!