Wednesday, June 25, 2014

Spring MVC - Thymeleaf - Bootstrap - I

Source Code | See Application
You can see the full description of the aplication and the other two entries related here

Initial configuration

Web.xml

In this file we define that the Spring DispatcherServlet will be in charge of all the requests that have the ".htm" suffix
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <servlet>
        <servlet-name>twitterFlightExample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>twitterFlightExample</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
</web-app>

Spring Web Application Context - twitterFlightExample-servlet.xml

For default Spring search the webapplication file with the pattern: [servlet-name]-servlet.xml in this case the file will be: twitterFlightExample-servlet.xml
<beans>
    <import resource="classpath*:META-INF/spring/applicationContext*.xml"/>

    <context:component-scan base-package="org.anotes.example.twitterflight.*"/>

    <bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
        <property name="prefix" value="/pages/"/>
        <property name="suffix" value=".html"/>
        <property name="templateMode" value="HTML5"/>
        <property name="cacheable" value="false"/>
    </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"/>
    </bean>
</beans>

Notes:

In the templateResolver we define:
  • The path where all of thymeleaf template files:/pages/  will be found 
  • The extension of this templates: .html
  • The template mode: HTML5
  • The cacheable property must be false for developement

Controller

We will use the following controller:
@Controller
public class MainController {
    @Autowired
    ProductService productService;
    @Autowired
    MainInfo mainInfo;

    @RequestMapping("/main")
    public ModelAndView main(HttpServletRequest request) {
        Map requestParams = request.getParameterMap();
        updateMainInfoBasedOn(requestParams);
        ProductFlt productFlt = mainInfo.getFilter();
        SummaryInfo summaryInfo = mainInfo.getSummaryInfo();
        List<Product> products = productService.findProducts(productFlt, mainInfo.getPageInfo());
        Map model = new HashMap<>();
        model.put("products", products);
        model.put("pageInfo", mainInfo.getPageInfo());
        model.put("filterList", productFlt.getFilterEntryList());
        model.put("brandList", summaryInfo.getBrandSummaryList());
        return new ModelAndView("main", model);
    }

    @RequestMapping("/getProductInfo")
    public ModelAndView getProductInfo(@RequestParam("product") Long productGkey) {
        logger.debug("Getting product info for:{}", productGkey);
        List<ProductPriceHistory> productPriceHistoryList = productService.getProductPricesHistoryFor(productGkey);
        Map model = new HashMap();
        model.put("productPriceHistoryList", productPriceHistoryList);
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm");
        model.put("displayDateFormatter", dateFormat);
        return new ModelAndView("productDetail", model);
    }

Notes:

  • ProductService: will be the facade to all of the domain functionality that we need.
  • MainInfo: will be the session object that mantain all the information needed; for instance: the current filter applied, the current page and so on.
  • Methods:
    • main(HttpServletRequest request) It renders the main page; and this will be called also when the filters or the page changes.
    • getProductInfo(@RequestParam("product") Long productGkey) It returns the html fragment that contains the price information of the product. It is called from javascript ajax call, when the user click on the "Expand" link.

View:

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org"
  class="csstransforms csstransforms3d csstransitions">
  <head>
    ....
        <link rel="stylesheet" href="/bower_components/twitterflightexample/twitterflighexample.css"/>
        <!-- Latest compiled and minified CSS -->
        <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap.min.css"/>
        <link rel="stylesheet" href="/bower_components/bootstrap/css/bootstrap-responsive.min.css"/>
        <link rel="stylesheet" href="/bower_components/nprogress/nprogress.css"/>
  </head>
<body>
.....
    <div class="panel-collapse collapse in" th:each="brand : ${brandList}">
        <ul class="list-group">
        <li class="list-group-item">
            <a href="#"
               th:href="@{main.htm?(filter=brand,value=${brand.id})}"
               th:text="${brand.description}">Accesorias y Belleza</a>
            <span class="badge" th:text="${brand.nbr}">42</span>
        </li>
        </ul>
    </div>
.....
            <ol id="breadcrumb-zone" class="breadcrumb">
                <li th:each="filterEntry : ${filterList}"
                    th:class="${filterEntryStat.count}==${filterEntryStat.size}?'active'">
                    <a href="#" th:href="@{main.htm?(filter=breadcrumbIdx,value=${filterEntryStat.count}-1)}"
                       th:text="${filterEntry.value}"
                       th:if="${filterEntryStat.count}!=${filterEntryStat.size}">
                        Home</a>
                <span th:text="${filterEntry.value}"
                      th:if="${filterEntryStat.count}==${filterEntryStat.size}">
                    Name2</span>
                </li>
            </ol>
.....
            <a class="pull-left cursorpointer"
           th:onclick="'javascript:showMoreInfo(this,\'' + ${product.gkey} + '\');'">
            <span th:id="spn+${product.gkey}">Expand</span>
           </a>
.....

            <li>Price:
                <strong th:text="${product.price}? ${#numbers.formatDecimal(product.price, 0, 'COMMA', 2, 'POINT')}">123456</strong>
            </li>
.....                
    <script src="/bower_components/jquery/jquery.min.js"></script>
    <script src="/bower_components/bootstrap/js/bootstrap.min.js"></script>
    <script src="/bower_components/bootstrap/js/bootstrap-paginator.min.js"></script>
    <script src="/bower_components/nprogress/nprogress.js"></script>
    <script th:inline="javascript">
/*<![CDATA[*/
var options = {
    bootstrapMajorVersion: 3,
    currentPage: [[${pageInfo.page}]],
    totalPages: [[${pageInfo.totalPages}]],
    numberOfPages: 10,
    onPageClicked: function (e, originalEvent, type, page) {
        window.location.href = "/main.htm?page=" + (page - 1);
    }
};
/*]]>*/
</script>
</body>

Notes:

  • xmlns:th="http://www.thymeleaf.org" This is needed in order to use thymeleaf
  • bootstrap-paginator: is used to implement the pagination
  • nprogress: is used to show the progress bar at the top during the execution of ajax

Thymeleaf

  • Loop: th:each="brand : ${brandList}"
  • Href replace: th:href="@{main.htm?(filter=brand,value=${brand.id})}"
  • Simple text replace: th:text="${brand.description}"
  • Class replace: th:class="${filterEntryStat.count}==${filterEntryStat.size}?'active'"
  • Setting onclick event: th:onclick="'javascript:showMoreInfo(this,\'' + ${product.gkey} + '\');'"
  • Painting zones conditionally th:if="${filterEntryStat.count}!=${filterEntryStat.size}"
  • Strings concatenation th:src="'/img/' +${product.imageUrl}"
  • Format Numbers th:text="${product.price}? ${#numbers.formatDecimal(product.price, 0, 'COMMA', 2, 'POINT')}"
  • Calling methods of objects of the model
    • Adding "displayDateFormatter" to the model:
      public ModelAndView getProductInfo....
          Map model = new HashMap();
          ...
          SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm");
          model.put("displayDateFormatter", dateFormat);
          return new ModelAndView("productDetail", model);
      }
      
    • Calling a method "format" of the object "displayDateFormatter"
      <td th:text="${displayDateFormatter.format(prod.created)}">2.41</td>
  • Inline javascript
    <script th:inline="javascript">
        currentPage: [[${pageInfo.page}]],
        totalPages: [[${pageInfo.totalPages}]],
    </script>
    

0 comments:

Post a Comment