It's great to show some data to a user like in the previous chapter, but we also need to receive data in our application so users can enter the name of the movies they want to watch. We’ll do this by implementing a web form.
What's a web form?
Web forms are very common. If you ever have signed up on a website, you have used one.
![Ever filled out one of these? That's a web form!](https://user.oc-static.com/upload/2019/05/25/15587976233583_AdobeStock_89752597.jpg)
Implementing web forms usually consists of two main steps:
Displaying a prepopulated or empty form to the user.
Receiving the submitted data and redirecting it to another page.
Each one of these stages needs its own controller method. Let’s see how it all works in the code!
Display the form
Imagine a web form for a second. What is it? It’s a bunch of fields on an HTML page, right? They could be prepopulated or empty when the form loads depending on if the user wants to create or update. Then the user enters some values and finally presses the submit/save button.
Now imagine a simple Java class with some attributes and the standard getter/setter methods. Wouldn’t it be cool if we dealt with the data on a web form as a Java object? Well, that’s exactly what a command object is.
Use command objects on both stages of development of a web form, i.e., when the form loads, and when it's submitted. Before a web form loads, put a command object (either empty or prepopulated) in the model. In our application, we are going to use the WatchlistItem
class that we already have as the command object. As always, it all will make much more sense when you see it in the code.
We need another controller method to handle the request for showing the web form. This controller method needs to add an empty WatchlistItem
instance as the command object to the model, and then we need to add the static HTML page to the project and make it dynamic by adding the relevant Thymeleaf elements to it.
So, open WatchlistController
, and add a new method to it called showWatchlistItemForm
.
@GetMapping("/watchlistItemForm")
public ModelAndView showWatchlistItemForm() {
String viewName = "watchlistItemForm";
Map<String,Object> model = new HashMap<String,Object>();
model.put("watchlistItem", new WatchlistItem());
return new ModelAndView(viewName,model);
}
Add the watchlistItemForm-static.html
file from the static pages code repository to src/main/resources/templates
folder and rename it to watchlistItemForm.html
. Also, add Thymeleaf namespace at the top like before. Now it's time to change the <form>
tag. Specify the target URL that the form will be submitted to. That is done using th:action
element then you need to specify the command object that backs your form using th:object
.
Next, define the associations between the form fields and the command object attributes using th:field
on each field of the form. Your template file will look like this:
<html xmlns:th="http://www.thymeleaf.org">
<head>
<!-- Meta tags -->
<meta charset = "utf-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, shrink-to-fit = no">
<link rel = "stylesheet" href = "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
<title>Watchlist App</title>
</head>
<body>
<div class = "container">
<nav class = "navbar navbar-expand-sm navbar-dark bg-secondary">
<a class = "navbar-brand" href = "homepage-static.html">Watchlist App</a>
<button class = "navbar-toggler" type = "button" data-toggle = "collapse"
data-target = "#navbarSupportedContent"
aria-controls = "navbarSupportedContent" aria-expanded = "false"
aria-label = "Toggle navigation">
<span class = "navbar-toggler-icon"></span>
</button>
<div class = "collapse navbar-collapse" id = "navbarSupportedContent">
<ul class = "navbar-nav mr-auto">
<li class = "nav-item">
<a class = "nav-link" href = "/">Home</a>
</li>
<li class = "nav-item">
<a class = "nav-link" href = "/watchlist">Watchlist</a>
</li>
<li class = "nav-item active">
<a class = "nav-link" href = "/watchlistItemForm">Submit an item</a>
</li>
</ul>
</div>
</nav>
<form action="#" method="post" th:action="@{/watchlistItemForm}" th:object="${watchlistItem}">
<h2 class = "mt-4">Submit an item</h2>
<hr/>
<div class = "form-group row ">
<label for = "title" class = "col-sm-2 col-form-label">Title</label>
<div class = "col-sm-4">
<input th:field="*{title}" type = "text" class = "form-control" placeholder = "Mandatory">
</div>
</div>
<div class = "form-group row ">
<label for = "rating" class = "col-sm-2 col-form-label mr-0">Rating</label>
<div class = "col-sm-4">
<input th:field="*{rating}" type = "text" class = "form-control" placeholder = "5.0 < Number < 10.0">
</div>
</div>
<div class = "form-group row ">
<label for = "priority" class = "col-sm-2 col-form-label mr-0">Priority</label>
<div class = "col-sm-4">
<input th:field="*{priority}" type = "text" class = "form-control" placeholder = "Low|Medium|High">
</div>
</div>
<div class = "form-group row">
<label for = "comments" class = "col-sm-2 col-form-label">Comments</label>
<div class = "col-sm-4">
<textarea th:field="*{comment}" class = "form-control" rows = "3" placeholder = "Max. 50 chars"></textarea>
</div>
</div>
<div class = "form-group row">
<div class = "col-sm-10">
<button type = "submit" class = "btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
<script src = "https://code.jquery.com/jquery-3.3.1.slim.min.js"> </script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" > </script>
<script src = "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" > </script>
</body>
</html>
Now restart the application in Eclipse, and go to http://localhost:8080/watchlistItemForm
in your web browser. The form should show up. If you press the Submit button, you'll get an error page because we haven't implemented submission handler method in our controller yet. Let's fix that!
Handle form submissions
In HTTP protocol, there's always a client that sends requests to a server. These requests are done with something called an HTTP method. For example, the GET
method is usually used when data is received from the server. Whenever you type an address in the address bar of your browser, a GET
request is sent to the server. There's also another HTTP request method called POST
, which is usually used when data is sent from the client to the server; for example, when a web form like ours is submitted.
For our form, we need to create a handler method in the controller class that handles POST
requests coming to the /watchlistItemForm
URL. The annotation that will achieve that is @PostMapping("/watchlistItemForm")
.
You will see that our form submission handler method has another subtle difference with the other handlers we have created so far. In the other handlers, we returned a model and a view, but our form handler will return a RedirectView. The reason for that is a form submission changes the state of data stored in our application, so it's considered a good practice to send the user to another page for a fresh view of the data.
Open the controller class and add a new method called submitWatchlistItemForm()
. The method is supposed to handle form submissions with POST
method coming to the/watchlistItemForm
URL, so add a @PostMapping
annotation for that. Also, add the WatchlistItem
as the parameter. Now, all you have to do is add the item to the watchlistItems
list. Your new form handler method would look like this:
@PostMapping("/watchlistItemForm")
public ModelAndView submitWatchlistItemForm(WatchlistItem watchlistItem) {
watchlistItem.setId(index++);
watchlistItems.add(watchlistItem);
RedirectView redirectView = new RedirectView();
redirectView.setUrl("/watchlist");
return new ModelAndView(redirectView);
}
Now you can get rid of the hard-coded items in the getWatchlist()
method and make it much cleaner. It will look like this:
@GetMapping("/watchlist")
public ModelAndView getWatchlist() {
String viewName = "watchlist";
Map<String, Object> model = new HashMap<String, Object>();
model.put("watchlistItems", watchlistItems);
model.put("numberOfMovies", watchlistItems.size());
return new ModelAndView(viewName , model);
}
Now start the server and navigate to your form and enjoy entering some data and seeing the result in the Watchlist page! You deserve it! 😉
Handle updates
Our client wants his users to be able to add new items and update the existing ones. That’s a fair request, isn’t it? When the users press the Update button on the Watchlist page, he wants them to get redirected to the Watchlist item form so they can apply and submit the changes they want.
To implement the update flow, we need to change some bits and pieces in the creative flow to cater to the update operation. The id field of WatchlistItem
will play a key role in this process:
When the Update button on the Watchlist page is pressed, the id of the item will be passed from the
showWatchlistItemForm(..)
handler method to the preloaded item.When the form is submitted to update an existing item, the id will be sent to the form handler method to apply the submitted data to an existing item in the
watchlistItems
list.When the form is submitted to create a new item, the id that is sent to the form handler will be null.
Let's start by making the changes to display the form with the values of an existing item.
First, change the showWatchlistItemForm()
method to receive the id of a watchlistItem
when the Update button on the list is pressed. To do that, add an optional id parameter to it using a @RequestParam(required=false)
annotation. Now, you could prepopulate the form if an item with this id already exists, or otherwise show an empty form like before.
@GetMapping("/watchlistItemForm")
public ModelAndView showWatchlistItemForm(@RequestParam(required=false) Integer id) {
Map<String, Object> model = new HashMap<String, Object>();
WatchlistItem watchlistItem = findWatchlistItemById(id);
if (watchlistItem == null) {
model.put("watchlistItem", new WatchlistItem());
} else {
model.put("watchlistItem", watchlistItem);
}
return new ModelAndView("watchlistItemForm" , model);
}
private WatchlistItem findWatchlistItemById(Integer id) {
for (WatchlistItem watchlistItem : watchlistItems) {
if (watchlistItem.getId().equals(id)) {
return watchlistItem;
}
}
return null;
}
Now, in watchlist.html
, change the Update button to pass the id of the WatchlistItem
on that row to the controller:
<td><a href="#" class="btn btn-info" role="button" th:href="@{/watchlistItemForm(id=${watchlistItem.id})}">Update</a></td>
Now the form is loaded successfully with the values of an item from Watchlist table, but our form submission handler method cannot tell the difference between a save and update. Let's fix that!
Change the submitWatchlistItemForm(..)
method to create a new item if an existing item cannot be found based on the WatchlistItem
id that comes in as the parameter. This could be because the id is null or an invalid value in which case we could create a new item.
@PostMapping("/watchlistItemForm")
public ModelAndView submitWatchlistItemForm(WatchlistItem watchlistItem) {
WatchlistItem existingItem = findWatchlistItemById(watchlistItem.getId());
if (existingItem == null) {
watchlistItem.setId(index++);
watchlistItems.add(watchlistItem);
} else {
existingItem.setComment(watchlistItem.getComment());
existingItem.setPriority(watchlistItem.getPriority());
existingItem.setRating(watchlistItem.getRating());
existingItem.setTitle(watchlistItem.getTitle());
}
RedirectView redirect = new RedirectView();
redirect.setUrl("/watchlist");
return new ModelAndView(redirect);
}
And finally, change watchlistItemForm.html
file to map a hidden input field to the id of the WatchlisItem
that could be updated on the form. Add this to the form right above the Submit button:
<input type="hidden" th:field="*{id}" />
That’s it. You can restart your server and enjoy updating existing items as well as creating new ones! 😎
Test Spring MVC controllers
Now we have a Spring MVC web application that does two operations of any web application: showing data to users and receiving data from them. However, even if our application has all the best features in the world, nobody is going to use it if it has bugs or breaks easily when we make changes to it. That’s why we need automated tests. Spring MVC provides many different ways to test Spring MVC apps. In this part, we will focus on writing unit tests for controllers to make sure they perform their basic functionalities; such as returning the right model and view, or redirecting as expected. Let's get started!
Create a new test class called in the src/test/java
folder called WatchlistControllerTest
. To make it capable of testing Spring MVC features, you need to add two annotations to it: @RunWith(SpringRunner.class)
and @WebMvcTest
. Now create two test methods with @Test annotations to test displaying and submitting the Watchlist item form. Then use perform()
and expect()
methods of MockMvc
API to call the end points and check if they return expected results. Your test class will end up looking something like this:
package com.openclassrooms.watchlist;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest
@RunWith(SpringRunner.class)
public class WatchlistControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void testShowWatchlistItemForm() throws Exception {
mockMvc.perform(get("/watchlistItemForm"))
.andExpect(status().is2xxSuccessful())
.andExpect(view().name("watchlistItemForm"))
.andExpect(model().size(1))
.andExpect(model().attributeExists("watchlistItem"));
}
@Test
public void testSubmitWatchlistItemForm() throws Exception {
mockMvc.perform(post("/watchlistItemForm"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/watchlist"));
}
}
Right-click on any white area on the editor of your test class and select Run as and then choose JUnit Test. You will see the tests passing with a green status.
Try it out challenge!
Writing tests takes practice. Go ahead and write a test for the getWatchlist()
method that we created before to get some extra practice!
Let’s recap!
Now, before you move on to the next chapter, ask yourself if you remember the steps for:
Displaying an empty or prepopulated form.
Receiving and processing the data that is submitted from a form.
Writing unit tests for Spring MVC controllers using JUnit and Spring MockMvc.
If you can recall these steps, then you're ready to start validating some forms in the next chapter!