Keep form field values and errors after redirection in Spring MVC controller

Ways for processing form submit

When you are processing a form submit using Spring MVC, if there are validation errors, you can:

  1. Return the errors in the response to the form submit request (POST request usually).
  2. Redirect the user to the form and return the result from this new request.

The first option has a drawback that if the user refreshes the page after we return with the response, the form will be resubmitted the same way as before (also he’ll see the confirmation popup asking if it is okay to do that). On one hand, most of the users won’t even understand the situation, on the other hand if the user decides to resubmit the form, sometimes unwanted situations might happen.

Also, another case to consider, if the user just wants to refresh the page to get an empty form, he’ll not succeed because he’ll still see the errors and filled values from the previous submission.

Because of these reasons the second option might be preferred, let’s see how to do that

Retaining filled values and errors upon redirect

First let’s see the whole code of a controller that implements the solution:

package com.devsphinx.web.controller.user;

import com.devsphinx.web.controller.BaseController;
import com.devsphinx.web.model.user.CreateAccountModel;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;

@Controller
public class CreateAccountController extends BaseController {

    @RequestMapping(value = "/create-account", method = RequestMethod.GET)
    public String getCreateAccount(Model model) {

        if (!model.containsAttribute("createAccountModel")) {
            model.addAttribute("createAccountModel", new CreateAccountModel());
        }

        return "create-account/create-account";
    }

    @RequestMapping(value = "/create-account", method = RequestMethod.POST)
    public String postCreateAccount(
            @Valid CreateAccountModel createAccountModel,
            BindingResult result, RedirectAttributes redirectAttributes) {

        if (result.hasErrors()) {
            redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.createAccountModel", result);
            redirectAttributes.addFlashAttribute("createAccountModel", createAccountModel);
            return "redirect:/create-account";
        }
        
        // Success case omitted...
    }
}

You can see that we have here a standard spring controller with two methods. The first method serves the GET request for the account creation page and the second one serves the POST request.

What we wanted to achieve is if the POST request is performed and there are validation errors, then the request is redirected to the GET handler with the errors and form field values still populated.

In the POST handler method

if (result.hasErrors()) {
    redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.createAccountModel", result);
    redirectAttributes.addFlashAttribute("createAccountModel", createAccountModel);
    return "redirect:/create-account";
}

In this, we have to populate these to the RedirectAttributes instance.

  • For the BindingResult you have to specify the class with the full package name and contatenate the model’s name to the end.
  • The model must be added with the same name as you were using in this method.

In the GET handler method

@RequestMapping(value = "/create-account", method = RequestMethod.GET)
public String getCreateAccount(Model model) {

  if (!model.containsAttribute("createAccountModel")) {
    model.addAttribute("createAccountModel", new CreateAccountModel());
  } 

  return "create-account/create-account";
}

If we redirect from the POST handler, because we added the flash attributes, the model in the GET handler will be automatically populated with these.

However, if we reach the GET handler with just a regular page request, without anything added as flash attributes, we need to manually add the our form backing object (createAccountModel) to the model.

Specifying the tab order of HTML elements using the tabindex attribute

As you probably know you can use the TAB key to go through certain elements (most commonly forms fields) on a web page. By default, each HTML page has a tabbing order that matches the order of the elements in your markup. So if an element on the page gets focus and you press the TAB key, the next element that receives focus will be the one that is next to it in the markup and is considered tabbable by default by the browser.

The tabindex attribute can be used to change this order. In HTML5 it can be specified on any element, however it is not necessarily useful in all cases. Let’s see a simple exampale where the tabindex attribute is used to change the order of tabbing on a simple form:

<form>
    First name:<br>
    <input type="text" name="firstname" tabindex="1">
    <br>
    Last name:<br>
    <input type="text" name="lastname" tabindex="-1">
    <br>
    Email:<br>
    <input type="text" name="email" tabindex="3">
    <br>
    Password:<br>
    <input type="text" name="password" tabindex="2">
</form>

By default, tabbing would take you through the input fields in the order they are defined. We changed this order by specifying the tabindex attribute. At first the “First name” field will be focused, then the “Password”, and lastly the “Email”. The “Last name” field won’t be focused. To understand the reason for this behaviour, lets see how the tabindex is taken into account.

If you specify the tabindex attribute on at least one element on the page, the tabbing will occur in the following manner:

  1. The elements that have a positive tabindex specified will be selected in increasing tabindex order.
  2. If we reach the last element that has a tabindex, then next the elements without tabindex will be focused in the order they are defined in the markup.
  3. The elements that are not tabbable by default (e.g. a div) or have a tabindex of -1 will not be selected at all.

A lot of HTML elements are not tabbable by default, but by specifying a tabindex for them they will get included in the tabbing order.

When you are specifying tabindex for your elements it’s a good idea to leave some room between each index. This way, if you have to insert some new elements in the tab order, you don’t have to adjust the tabindex of other elements. So from this perspective the tabindexes 10,20,30,40 are better than 1,2,3,4.

How to create a custom Spring form errors tag

Spring has a tag library that helps you in building forms. It also has a tag called errors that lets you output validation errors in a formatted way.  However, this tag has some limitations. When multiple errors are displayed at the same time, you can only control the styling of the outside container and not the styling of each of the errors. So for example it is not possible to display the error messages as an unordered list.

This tutorial describes how to create a custom errors tag that has some extra customization attibutes compared to the basic Spring form errors tag.

The steps we will take include the following:

  • Creating a Tag Library with our custom tag definition in it.
  • Creating a custom class that specifies how our new tag will work.
  • Include the tag library in a sample JSP and use it to display errors.

Creating the Tag Library

A tag library definition is contained in a .tld file and also each tag has a class which contains the logic that results in some kind of output when we use the tag. We are using the basic Spring error tag’s definition as a starting point, but add two extra attributes to it. The added attributes are highlighted in the code below.

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>Jtuts common tags</short-name>
    <uri>http://jtuts.com/taglib/tags</uri>
    <tag>
          <description>Renders field errors wrapped in a specified tag.</description>
          <name>errors</name>
          <tag-class>jtuts.tag.CustomSpringFormErrorsTag</tag-class>
          <body-content>JSP</body-content>
          <variable>
               <name-given>messages</name-given>
               <variable-class>java.util.List</variable-class>
          </variable>
          <attribute>
               <description>Path to errors object for data binding</description>
               <name>path</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Standard Attribute</description>
               <name>id</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>Enable/disable HTML escaping of rendered values.</description>
               <name>htmlEscape</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>Specifies the HTML element that is used to render the individual errors.</description>
               <name>innerElement</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
               <attribute>
               <description>Specifies the CSS class that the element has that is used to render the individual errors.</description>
               <name>innerElementCssClass</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>Delimiter for displaying multiple error messages. Defaults to the br tag.</description>
               <name>delimiter</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>Equivalent to "class" - HTML Optional Attribute</description>
               <name>cssClass</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>Equivalent to "style" - HTML Optional Attribute</description>
               <name>cssStyle</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Standard Attribute</description>
               <name>lang</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Standard Attribute</description>
               <name>title</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Standard Attribute</description>
               <name>dir</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Standard Attribute</description>
               <name>tabindex</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onclick</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>ondblclick</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onmousedown</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onmouseup</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onmouseover</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onmousemove</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onmouseout</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onkeypress</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onkeyup</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>HTML Event Attribute</description>
               <name>onkeydown</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <attribute>
               <description>Specifies the HTML element that is used to render the enclosing errors.</description>
               <name>element</name>
               <required>false</required>
               <rtexprvalue>true</rtexprvalue>
          </attribute>
          <dynamic-attributes>true</dynamic-attributes>
     </tag>
</taglib>

One of the new attributes is innerElement. It can be used to specify an HTML element that will be used to wrap every single error message individually. The other new attribute is the innerElementCssClass. This specifies the CSS class or classes that will be applied to each of the inner wrapping elements.

The other attributes are just copied from the original Spring form errors tag.

Creating the tag class

The tag class specifies what kind of output will the tag produce based on it’s attribute values. Here is the full class for the new tag.

package jtuts.tag;

import javax.servlet.jsp.JspException;

import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.tags.form.ErrorsTag;
import org.springframework.web.servlet.tags.form.TagWriter;


/**
 * The Spring form:errors tag extended with attributes which allow to use an inner element with a
 * css class which wraps every individual error item.
 */
public class CustomSpringFormErrorsTag extends ErrorsTag {

    private static final long serialVersionUID = 24723583545L;

    private String innerElement;
    private String innerElementCssClass;

    @Override
    protected void renderDefaultContent(TagWriter tagWriter) throws JspException {
        tagWriter.startTag(getElement());
        writeDefaultAttributes(tagWriter);
        String delimiter = ObjectUtils.getDisplayString(evaluate("delimiter", getDelimiter()));
        String[] errorMessages = getBindStatus().getErrorMessages();

        for (int i = 0; i < errorMessages.length; i++) {
            String errorMessage = errorMessages[i];
            if (i > 0) {
                tagWriter.appendValue(delimiter);
            }

            if (innerElement != null)
                tagWriter.appendValue(startTagForInnerElement());

            tagWriter.appendValue(getDisplayString(errorMessage));

            if (innerElement != null)
                tagWriter.appendValue(endTagForInnerElement());
        }
        tagWriter.endTag();
    }

    private String startTagForInnerElement() {
        StringBuilder stringBuilder = new StringBuilder("<" + innerElement);

        if (innerElementCssClass != null)
            stringBuilder.append(" class=\"" + innerElementCssClass + "\"");

        stringBuilder.append(">");

        return stringBuilder.toString();
    }

    private String endTagForInnerElement() {
        return "</" + innerElement + ">";
    }

    public String getInnerElement() {
        return innerElement;
    }

    public void setInnerElement(String innerElement) {
        this.innerElement = innerElement;
    }

    public String getInnerElementCssClass() {
        return innerElementCssClass;
    }

    public void setInnerElementCssClass(String innerElementCssClass) {
        this.innerElementCssClass = innerElementCssClass;
    }

}

You can see that it extends the basic error tag (org.springframework.web.servlet.tags.form.ErrorsTag).  We override the renderDefaultContent method that creates the output of the tag. Anything that you pass to this method will be appended to the output produced when someone uses this tag.

As you can see the extra logic that we have added is pretty simple. We just print an opening and closing tag around each error if the user provided the innerElement attribute and we also add the CSS classes to the opening tag if the innerElementCssClass attribute is specified.

We have also set up some extra methods, but they are just there to simplify the renderDefaultContent method.

The outer element that encloses all the errors is just displayed the same way as it was in the super implementation (startTag, endTag, writeDefaultAttributes calls).

Using the tag

For demonstration purposes I have created a very simple registration form that uses the newly created tag.

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="jtuts" uri="/WEB-INF/tags/jtuts.tld" %>

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Registration form with a custom errors tag.</title>
    <style>
        .error-message { color: #CC3344; }
    </style>
</head>
<body>
    <h1>Register</h1>
    <form:form commandName="userForm" action="${pageContext.request.contextPath}/register" method="post">
        <jtuts:errors path="*" element="ul" innerElement="li" delimiter="" cssClass="error-message" />
        <table>
            <tr>
                <td>First Name:</td>
                <td><form:input path="firstName" /></td>
            </tr>
            <tr>
                <td>Last Name:</td>
                <td><form:input path="lastName" /></td>
            </tr>
            <tr>
                <td>User Name:</td>
                <td><form:input path="userName" /></td>
            </tr>
            <tr>
                <td>Email Address:</td>
                <td><form:input path="emailAddress" /></td>
            </tr>
            <tr>
                <td colspan="2">
                    <input type="submit" value="Save User" />
                </td>
            </tr>
        </table>
    </form:form>
</body>
</html>

Notice the taglib definition with the taglib directive and the usage of our new jtuts:errors tag.

The POM, the Controller, the form class and validation

For the sake of completeness I also show you the remaining parts of the sample application.

The pom.xml looks like this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>how-to-create-a-custom-spring-form-errors-tag-20150126</groupId>
    <artifactId>how-to-create-a-custom-spring-form-errors-tag-20150126</artifactId>
    <packaging>war</packaging>
    <version>0.0.1-SNAPSHOT</version>
    <name>how-to-create-a-custom-spring-form-errors-tag-20150126 Maven Webapp</name>
    <url>http://maven.apache.org</url>

    <properties>
        <spring.version>4.1.4.RELEASE</spring.version>
        <javax.validation.validation-api.version>1.1.0.Final</javax.validation.validation-api.version>
        <hibernate-validator.version>5.1.3.Final</hibernate-validator.version>
        <servlet-api.version>2.5</servlet-api.version>
        <jsp-api.version>2.2</jsp-api.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>${javax.validation.validation-api.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>${hibernate-validator.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>${servlet-api.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>${jsp-api.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <path>/jtuts</path>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

I just included the minimum dependencies required for this project. I also used the Tomcat Maven plugin to be able to deploy the app easily.

The Controller class:

package jtuts.controller;

import javax.validation.Valid;

import jtuts.form.UserForm;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;


@Controller
public class RegisterController {
 
    @RequestMapping(value="register", method = RequestMethod.GET) 
    public ModelAndView registerGet() {
        ModelAndView model = new ModelAndView();
        
        model.setViewName("register");
        model.addObject("userForm", new UserForm());
        
        return model;
    }

    @RequestMapping(value="register", method = RequestMethod.POST) 
    public ModelAndView registerPost(@Valid @ModelAttribute("userForm") UserForm userForm, BindingResult result) {
        ModelAndView model = new ModelAndView();
        
        model.setViewName("register");
        
        return model;
    }
}

In the controller we are just running the form through some basic validation. The validation rules are set up in the form class which looks like this:

package jtuts.form;

import org.hibernate.validator.constraints.NotEmpty;

public class UserForm {

    @NotEmpty(message="The first name may not be empty.")
    private String firstName;
    
    @NotEmpty(message="The last name may not be empty.")
    private String lastName;
    
    @NotEmpty(message="The user name may not be empty.")
    private String userName;
    
    @NotEmpty(message="The email address may not be empty.")
    private String emailAddress;

    
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }
}

The Spring configuration for the validator to work:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd">
         
    <context:component-scan base-package="jtuts" />
    <mvc:annotation-driven validator="validator" />
  
    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
  
</beans>

The working application

This is what the working application looks like when we try to submit an empty form. Notice that we are displaying the error messages as an unordered list which was not possible with the original version of the errors tag.

how-to-create-a-custom-spring-form-errors-tag-20150126-registration-form

Download

You can download the whole working source code from here.