What You'll Learn

This will be our first "post a form" tutorial. We're going to make it possible to add a new charity. You can probably work out that this is going to require a number of changes across the whole stack.

Note that in tag 016-refactor-repository the repository was extended to include a saveCharity method so the first step on this change has been taken.

Since we've changed the repository, we'll work bottom-up.

These changes shouldn't require much explanation. The following classes are changed:

This offers a DTO-based method for saving a charity.

We will need to add a number of new elements to the codebase. We will need a form with multiple fields to enter the charity details. We will need an object to receive that form (again think here about the mapping from the web world to the OO world) and we'll need a method in a controller that is mapped to handle the request (which will be a POST request).

The full new HTML form template can be found at charity-form.html. This is the key form element within it.

<form th:action="@{/admin/charity}" th:method="post" th:object="${charityForm}">
    <div class="mb-3">
        <label class="form-label" for="name">Name:</label>
        <input class="form-control" th:field="*{name}" type="text"/>
        <span th:errors="*{name}" th:if="${#fields.hasErrors('name')}">Name Error</span>
    </div>
    <div class="mb-3">
        <label class="form-label" for="acronym">Acronym:</label>
        <input class="form-control" th:field="*{acronym}" type="text"/>
        <span th:errors="*{acronym}" th:if="${#fields.hasErrors('acronym')}">Acronym Error</span>
    </div>
    <div class="mb-3">
        <label class="form-label" for="registration">Registration:</label>
        <input class="form-control" th:field="*{registration}" type="text"/>
        <span th:errors="*{registration}" th:if="${#fields.hasErrors('registration')}">Name Error</span>
    </div>
    <div class="mb-3">
        <label class="form-label" for="missionStatement">Mission Statement:</label>
        <textarea class="form-control" th:field="*{missionStatement}"/>
        <span th:errors="*{registration}" th:if="${#fields.hasErrors('missionStatement')}">Name Error</span>
    </div>
    <div class="mb-3">

        <button class="btn btn-primary">Submit</button>
    </div>

    <div>
        <div class="mb-3">
            <a class="btn btn-primary" href="/">Cancel</a>
        </div>
    </div>

</form>

Let's go through the changes.

The form element

In the form element, we introduce the th:action attribute, the th:object attribute and the th:method attribute. The last attribute is pretty self-explanatory, but the others need some explanation.

<form th:action="@{/admin/charity}" th:method="post" th:object="${charityForm}">

Let's examine those two attributes:

A term used in other frameworks is "Backing Bean". The idea is that the object "backs" the form. The HTML and the class go hand-in-hand and the data is mapped (by name) between the two. For example, if the HTML form field has a name attribute of "acronym" and the class has a field called "acronym" (with a getter and setter) then these can be mapped "by name".

The next step is to change the text elements, making sure we refer to the name field of the form object.

<div class="mb-3">
    <label class="form-label" for="name">Name:</label>
    <input class="form-control" th:field="*{name}" type="text"/>
    <span th:errors="*{name}" th:if="${#fields.hasErrors('name')}">Name Error</span>
</div>

In the input field, we now use a th:field attribute. Note, that we prefix the field value with "*" rather than "$". This is because we want to refer to a field within the object. This notation is basically saying "map the field called ‘name' within the object defined at the form level to this input element."

We also can replace the for attribute with th:for attribute. In this example we have not. Thymeleaf will expand th:field to standard HTML attributes (you can check this by looking at the page source in the browser). If we understand how that works, we can mix Thmyeleaf and HTML attributes.

We also have a span with th:errors and th:if attributes in it. These won't be useful yet, but will come into play when we add validation.

The submit button doesn't need to change, but you can research how you might go about changing the button's label using Thymeleaf.

Our backing bean or form class is going to be a separate class which we will call "CharityForm". We can't use the DTO because it is immutable and Spring needs to be able to map the web request into it and therefore it requires setters - and we don't want to use the domain class to handle form processing as we don't want to tie the domain processing to any particular user interface.

The CharityForm class looks like this:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CharityForm {
    private String name;
    private String registration;
    private String acronym;
    private String missionStatement;
}

The one thing that this class recognises is that the web requests will provide textual data. If the class had non-String fields, Spring would try to do conversions where that was possible.

The controller layer needs to be extended. We could add methods to our existing controller, but we can also add a new controller class. Since this is an administration task, we'll create a new controller called CharityAdminController.

@Controller
@RequestMapping("/admin")
public class CharityAdminController {

There's a new annotation here...@RequestMapping and it is at the class level. This tells Spring that the value of the annotation will be the first part of the URL on all of the subsequent mappings on the methods of the controller.

Get the form

Firstly, we need to handle the route to the form. We used to do this in the router, but now we need to add an empty object to the model before we route to the form template.

@GetMapping("/new-charity")
public String newCharity(Model model) {
    CharityForm charityForm = new CharityForm();

    model.addAttribute("charityForm", charityForm);

    return "admin/charity-form.html";
}

We create a new CharityForm object, put it on the model and then redirect to the HTML template (note the use of admin/charity-form.html). This method will be called when a browser makes a GET request to /admin/new-charity (i.e. combines the @RequestMapping with the @GetMapping)

Process the form submission

Now we need to add a method that handles the post.

@PostMapping("/charity")
   public String charityFormPost(CharityForm charityForm, Model model) {
       CharityDTO charityDTO = new CharityDTO(charityForm.getName(),
                                              charityForm.getRegistration(),
                                              charityForm.getAcronym(),
                                              charityForm.getAcronym(),
                                              charityForm.getMissionStatement(),
                                              Boolean.FALSE);
       charitySearch.saveCharity(charityDTO);
       return "redirect:/charities";
   }

The first parameter on the method is a CharityForm object. Spring maps the POST request to the object by name. Again, by the time the developer starts coding the method, they are working with objects not HTTP constructs.

The method then creates a DTO from the form and calls the method on the service to save the charity.

At the end of the method, it returns a string, but this is not the name of the template. In this case, we want to trigger a HTTP redirect. This is because we want to follow the POST-REDIRECT-GET pattern. This can help reduce problems with double submissions. Spring interprets the string, sees "redirect:" at the start and then issues a GET redirect to the provided URL (i.e. the user is sent back to the home page).

What if?

You're asking a load of questions. What if the fields are blank? What if the acronym already exists? These are validations and we'll look at those shortly. Remember the idea of story splitting...we can ignore some rules and implement them later.

Reload the server and try out the form. Try to enter a new charity with a unique acronym.

We have linked the form processing to a backing object and fired that through the process end-to-end. It's important to keep taking the time to appreciate what is being done by the framework before the processing reaches our code and what is done afterwards. Remember, that idea of the "Hollywood Principle".

Let's move onto tag 018-update-form and see if we can reuse the form to update an existing charity. The change is remarkably small. We just need to create a method that populates the form with a populated CharityForm object.

@GetMapping("/update-charity/{furl}")
    public String updateCharity(
            @PathVariable(value = "furl", required = true) String furl
            , Model model) {

        // TODO should do error handling here...
        CharityDTO charityDTO = charitySearch.findByAcronym(furl).get();

        CharityForm charityForm = new CharityForm(charityDTO);

        model.addAttribute("charityForm", charityForm);

        return "admin/charity-form.html";

    }

We use a path variable to get the required charity, then create a form object form the DTO and pass that populated form object to the model (as opposed to an empty one that we did for new charities).

We've added a new constructor to the "CharityForm class that takes a DTO as the parameter.

Nothing else has to change (since it's hard-coded and a "put" method will overwrite the existing value if a duplicate key is provided).