Our application is already great but it seriously leaves something to be desired in terms of aesthetics. You may have heard of material design. It is Google's take on flat design.
We will use Materialize (http://materializecss.com), a great looking responsive CSS and JavaScript library, just like Bootstrap.
We will now get to use WebJars. Add jQuery and Materialize CSS to our dependencies:
compile 'org.webjars:materializecss:0.96.0' compile 'org.webjars:jquery:2.1.4'
The way a WebJar is organized is completely standardized. You will find the JS and CSS files of any library in /webjars/{lib}/{version}/*.js
.
For instance, to add jQuery to our page, the following to a web page:
<script src="/webjars/jquery/2.1.4/jquery.js"></script>
Let's modify our controller so that it gives us a list of all tweet objects instead of simple text:
package masterSpringMvc.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.twitter.api.SearchResults; import org.springframework.social.twitter.api.Tweet; import org.springframework.social.twitter.api.Twitter; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @Controller public class TweetController { @Autowired private Twitter twitter; @RequestMapping("/") public String hello(@RequestParam(defaultValue = "masterSpringMVC4") String search, Model model) { SearchResults searchResults = twitter.searchOperations().search(search); List<Tweet> tweets = searchResults.getTweets(); model.addAttribute("tweets", tweets); model.addAttribute("search", search); return "resultPage"; } }
Let's include materialize CSS in our view:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head lang="en"> <meta charset="UTF-8"/> <title>Hello twitter</title> <link href="/webjars/materializecss/0.96.0/css/materialize.css" type="text/css" rel="stylesheet" media="screen,projection"/> </head> <body> <div class="row"> <h2 class="indigo-text center" th:text="|Tweet results for ${search}|">Tweets</h2> <ul class="collection"> <li class="collection-item avatar" th:each="tweet : ${tweets}"> <img th:src="${tweet.user.profileImageUrl}" alt="" class="circle"/> <span class="title" th:text="${tweet.user.name}">Username</span> <p th:text="${tweet.text}">Tweet message</p> </li> </ul> </div> <script src="/webjars/jquery/2.1.4/jquery.js"></script> <script src="/webjars/materializecss/0.96.0/js/materialize.js"></script> </body> </html>
The result already looks way better!
The last thing we want to do is to put the reusable chunks of our UI into templates. To do this, we will use the thymeleaf-layout-dialect
dependency, which is included in the spring-boot-starter-thymeleaf
dependency of our project.
We will create a new file called default.html
in src/main/resources/templates/layout
. It will contain the code we will repeat from page to page:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no"/> <title>Default title</title> <link href="/webjars/materializecss/0.96.0/css/materialize.css" type="text/css" rel="stylesheet" media="screen,projection"/> </head> <body> <section layout:fragment="content"> <p>Page content goes here</p> </section> <script src="/webjars/jquery/2.1.4/jquery.js"></script> <script src="/webjars/materializecss/0.96.0/js/materialize.js"></script> </body> </html>
We will now modify the resultPage.html
file so it uses the layout, which will simplify its contents:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout/default"> <head lang="en"> <title>Hello twitter</title> </head> <body> <div class="row" layout:fragment="content"> <h2 class="indigo-text center" th:text="|Tweet results for ${search}|">Tweets</h2> <ul class="collection"> <li class="collection-item avatar" th:each="tweet : ${tweets}"> <img th:src="${tweet.user.profileImageUrl}" alt="" class="circle"/> <span class="title" th:text="${tweet.user.name}">Username</span> <p th:text="${tweet.text}">Tweet message</p> </li> </ul> </div> </body> </html>
The layout:decorator="layout/default"
will indicate where our layout can be found. We can then inject content into the different layout:fragment
sections of the layout. Note that each template are valid HTML files. You can also override the title very easily.
We have a nice little tweet display application, but how are our users supposed to figure out that they need to supply a "search" request parameter?
It would be nice if we added a little form to our application.
Let's do something like this:
First, we need to modify our TweetController
to add a second view to our application. The search page will be available directly at the root of our application and the result page when hit enter in the search
field:
@Controller public class TweetController { @Autowired private Twitter twitter; @RequestMapping("/") public String home() { return "searchPage"; } @RequestMapping("/result") public String hello(@RequestParam(defaultValue = "masterSpringMVC4") String search, Model model) { SearchResults searchResults = twitter.searchOperations().search(search); List<Tweet> tweets = searchResults.getTweets(); model.addAttribute("tweets", tweets); model.addAttribute("search", search); return "resultPage"; } }
We will add another page to the templates
folder called the searchPage.html
file. It will contain a simple form, which will pass the search term to the result page via the get
method:
<!DOCTYPE html> <html xmlns:th="http://www.w3.org/1999/xhtml" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout/default"> <head lang="en"> <title>Search</title> </head> <body> <div class="row" layout:fragment="content"> <h4 class="indigo-text center">Please enter a search term</h4> <form action="/result" method="get" class="col s12"> <div class="row center"> <div class="input-field col s6 offset-s3"> <i class="mdi-action-search prefix"></i> <input id="search" name="search" type="text" class="validate"/> <label for="search">Search</label> </div> </div> </form> </div> </body> </html>
This is very simple HTML and it works perfectly. You can try it now.
What if we wanted to disallow some search result? Let's say we want to display an error message if the user types in struts
.
The best way to achieve this would be to modify the form to post the data. In the controller, we can then intercept what is posted and implement this business rule accordingly.
First, we need to change the form in the searchPage
, which is as follows:
<form action="/result" method="get" class="col s12">
Now, we change the form to this:
<form action="/postSearch" method="post" class="col s12">
We also need to handle this post on the server. Add this method to the TweetController
:
@RequestMapping(value = "/postSearch", method = RequestMethod.POST) public String postSearch(HttpServletRequest request, RedirectAttributes redirectAttributes) { String search = request.getParameter("search"); redirectAttributes.addAttribute("search", search); return "redirect:result"; }
There are several novelties here:
POST
.RedirectAttributes
.The RedirectAttributes
is a Spring model that will be specifically used to propagate values in a redirect scenario.
Redirect/Forward are classical options in the context of a Java web application. They both change the view that is displayed on the user's browser. The difference is that Redirect
will send a 302 header that will trigger navigation inside the browser, whereas Forward
will not cause the URL to change. In Spring MVC, you can use either option simply by prefixing your method return strings with redirect:
or forward:
. In both cases, the string you return will not be resolved to a view like we saw earlier, but will instead trigger navigation to a specific URL.
The preceding example is a bit contrived, and we will see smarter form handling in the next chapter. If you put a breakpoint in the postSearch
method, you will see that it will be called right after a post in our form.
So what about the error message?
Let's change the postSearch
method:
@RequestMapping(value = "/postSearch", method = RequestMethod.POST) public String postSearch(HttpServletRequest request, RedirectAttributes redirectAttributes) { String search = request.getParameter("search"); if (search.toLowerCase().contains("struts")) { redirectAttributes.addFlashAttribute("error", "Try using spring instead!"); return "redirect:/"; } redirectAttributes.addAttribute("search", search); return "redirect:result"; }
If the user's search terms contain "struts", we redirect them to the searchPage
and add a little error message using flash attributes.
These special kinds of attributes live only for the time of a request and will disappear when the page is refreshed. This is very useful when we use the POST-REDIRECT-GET
pattern, as we just did.
We will need to display this message in the searchPage
result:
<!DOCTYPE html> <html xmlns:th="http://www.w3.org/1999/xhtml" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorator="layout/default"> <head lang="en"> <title>Search</title> </head> <body> <div class="row" layout:fragment="content"> <h4 class="indigo-text center">Please enter a search term</h4> <div class="col s6 offset-s3"> <div id="errorMessage" class="card-panel red lighten-2" th:if="${error}"> <span class="card-title" th:text="${error}"></span> </div> <form action="/postSearch" method="post" class="col s12"> <div class="row center"> <div class="input-field"> <i class="mdi-action-search prefix"></i> <input id="search" name="search" type="text" class="validate"/> <label for="search">Search</label> </div> </div> </form> </div> </div> </body> </html>
Now, if users try to search for "struts2" tweets, they will get a useful and appropriate answer: