Object Child Lists in Thymeleaf Forms and Spring MVC

We’re going to cover the issues involved with processing a list of Object Children in a Thymeleaf Form with Spring MVC as demonstrated in the NixMash Spring screenshot below. Put another way, we’re adding and removing Contact Phones which have a @OneToMany relationship to Contacts (one contact, many phones.)

We can click the “Add Phone” button and add a new blank Contact Phone row to the form as well as click the row’s trash can to remove it from the form instantly. Pretty cool, especially since we’re using nothing than straight-up Thymeleaf and Spring MVC for what appears to the user as all happening client-side.

The Thymeleaf

Let’s first look at the Thymeleaf where we see how to generate the list of child objects from the Spring MVC Model. You’ll notice we’re generating an array of ContactPhone child objects. We’re using the Thymeleaf *{contactPhone…} syntax to avoid using the full object names, for example contact.contactPhones.phoneNumber.

Also look at the <button /> code where we’re passing the contactPhoneId back to the Spring MVC Controller with a removeContactPhone parameter.

Here’s another look at the Id when passed back to the Controller among the other parameters of the Contact object model.

Okay, why is it a negative number? The new Contact Phone’s Id is negative because it’s a nifty trick to tell me if the record has been stored in the database. The blank record below is populated with a negative Id in the Controller.

Apart from assigning the Id, another important step in the addRow() method is to assign a Contact to the Contact Phone object, otherwise the Contact Phone would throw a null value on its Contact on Update().

With a negative ContactPhoneId temporarily assigned to the Contact.ContactPhone record we can perform a lookup in the Contact Service and when the phone is not found, save it to the database and add it to the Contact model.

Removing a Child Object from the List

Funny thing, removing a new ContactPhone record is more interesting than adding a new ContactPhone object. If you look again at the parameterMap() output above you’ll see our removeContactPhone Id value happens to be -427.

Let’s say the user added a new ContactPhone row but changed his mind. When sent back to the server, the child Contact Phone objects may or may not be in our database. That occurs when we click the “Update Contact” button. We added the new ContactPhone to the Contact model a second ago (previous screenshot in contact.getContactPhones().add(…) using our “Add Phone” button and now we remove it with “Remove Phone.” We’ll use the negative/positive Id value to determine if we need to make a trip to the repository after removing the Phone from the Contact Parent object.

Validation

We’re not done yet because we want to make sure we handle validation and send the error message back to the client. For that we’re going to use a Custom ContactFormValidator class and test all ContactPhone items, both new and existing, for any empty fields.

From our ContactFormValidator class.

We’ll @AutoWire the Validator in our ContactController Constructor and add it in @InitBinder for use in our Contact Update() method. If our Validator finds an empty Contact field it will create a GlobalError in the BindingResult and send it back to the client in the Thymeleaf area we created for global errors–as opposed to error messages with each field. Using a custom validator and Global Errors is also a clean way to quickly validate multiple children rather than add Error HTML wrappings for each child field in Thymeleaf.

The code referenced in this post can be found in the v0.1.8 branch of NixMash Spring on GitHub.