We all find ourselves in this situation from time to time: we want to code a form that contains a “main” record and a collection of “nested” records. We want some JavaScript-powered form controls to add to and remove from that collection of nested records. Clicking the submit button then saves the whole thing.

There are 2 ways to approach this:

  1. Nested properties
  2. A tableless model with its own custom persistence logic

I’ll cover the first approach in this blog post.

When we’re working with a typical CFWheels application with server-rendered HTML, we have the advantage of making the form work with and without JavaScript present. You’ve probably heard this referred to as progressive enhancement. So let’s build it without JavaScript first and then enhance the experience with some jQuery love.

Model setup

Here are a couple models.

models/Contact.cfc:

models/Address.cfc:

The gist: a contact has many addresses. Contact has nested properties for the addresses association, which means that a contact form can accept fields for zero or many address records. The address model has an integer position property that denotes what order the addresses should be loaded in. (This is important with a nested hasMany association!)

Initial controller setup

Now we’ll code a form for creating and editing a contact record and its associated addresses.

The first step is to define our objects to bind in the form within the controller. I’ll setup a fairly standard controller at controllers/Contacts.cfc with new, create, edit, and update actions. Nothing special here yet.

However, note that our new action is specifying that the form for a new record should not load with any addresses. The user will be forced to add them to the form before filling in any fields for them.

Initial view setup

Then the form views at views/contacts/new.cfm and views/contacts/edit.cfm share the _form.cfm partial:

The partial at _form.cfm has some interesting elements.

First, you’ll see a call to #includePartial(contact.addresses)#. Because we’re passing it an array, CFWheels will loop over the array and run a file at views/contacts/_address.cfm for each address in the present in loop.

We can code up that file at views/contacts/_address.cfm:

There are quite a few things going on in this file! Let’s break it down:

  • Notice that arguments.current is referenced several times. CFWheels will provide that value whenever you loop through a collection using includePartial like we’re doing here. This basically tells the form helpers and your own custom code which element in the query or array you’re currently on.

  • The form helpers also use the association and position arguments to denote which associated object is being referenced and our current position in the array of objects.

  • We’re including hiddenFields for id and position. Nested properties will punish you severely if you don’t include these properties in the form on updates. Be warned.

  • The code is pretty ugly for errorMessageOn. Unfortunately, it doesn’t take association or position arguments, so you need to manually tell it which object to reference via the objectName argument.

When you run this as is, you’ll get a form with no addresses loaded. You may have noticed that there is a “New Address” button in there. That’s how we’ll get an address onto the form.

Adding a new address record without JavaScript

We can code this so that it’ll work without JavaScript but also won’t get in the way if we want to progressively enhance with some JavaScript later.

When you click the “New Address” button, it will post the entire form to the create or update action, depending on what form you’re posting.

What we want to do is add a filter to the controller that will intercept requests to the create and update actions and add the new address record to the form if the newAddress button was clicked.

You can see that the addAddress filter’s job is to take over if params.newAddress is present. (This is passed to the request by clicking the button in the _form.cfm partial listed above.) If the button was clicked, the filter loads the entire form post into either a new or fetched record, adds an address object to contact.addresses, and tells CFWheels to load the form again.

Calling renderPage like that at the end stops processing in the controller, so the create and update actions won’t run. If the addAddress button wasn’t clicked, then the create or update actions will run as usual.

Now when you click the button, you should see the entire page refresh with a new set of fields for an address record. You can click it 100 times if you’d like in order to add 100 more addresses. It’ll even preserve information that you’ve typed into other fields. You can then submit the form with your addresses and watch it save both the contact and the addresses.

Adding another filter to handle the “Remove Address” button

Before we get into JavaScript, we have one more server-side piece of functionality to build: the “Remove Address” button. This will work similarly to how the “Add Address” button works, but there is another catch that we’ll cover.

So let’s code up a new removeAddress controller filter in controllers/Contacts.cfc:

Because the logic is fairly specialized, I decided to move the functionality into an instance method in the Contact model, called removeAddressAt:

The new instance method deletes the selected Address record if it’s already saved in the database. Then the address is removed from the array.

The fun part is that CFWheels pretty much forces you to have a sortProperty defined on nested properties for hasMany associations (position in our case), but it doesn’t help you manage it. And if you have any positions out of sequence (e.g., 1, 2, 3, 5), it will cause bad things to happen when CFWheels tries to load the association for you. You basically get an array where slot 4 in our example is a Java null value, which causes pretty bad things to happen in ColdFusion in general.

So anyway, be sure to manage your sortProperty values whenever you’re manipulating records in a nested hasMany like this.

With that in place, you should be able to click the “Remove Address” button near any of the address fields and see them vanish after the form post loads. You should then still be able to submit the form and see your changes reflected in the database.

Congratulations! You now have much of the functionality that you need to get the form working. If you’re working in an agile manner, I say ship it!

Enhancing the “New Address” button with jQuery

Of course, we’re never done. We can always improve. So let’s take a moment to add more address fieldsets to the form using JavaScript.

In essence, this JavaScript does what the full form post is doing, except at the end of the form post, it intercepts the response and inserts it into the #contact-addresses container.

This does require that we slightly change how the server responds to the request. Here is a modified addAddress filter in the controller:

Here, we use CFWheels’s isAjax method to detect if the form was posted with AJAX. If so, we render the _address partial with the new object and hardcode the current argument to the position of the new address object.

This will force CFWheels to respond only with the contents of the partial for the newly-added object, and it will generate all of the crazy form variable naming and such for you. jQuery will then insert that into the appropriate part of the form. Pretty cool, huh?

Enhancing the “Remove Address” button with jQuery

When adding the ability to remove nested records on a form, I like to flag a record for deletion along with the form post and let the user know that the delete will happen on form submission.

Here is an example from an application that I maintain:

The example above has 2 “funding request” records, one marked for deletion. When someone clicks the delete button, it marks the record as “to be deleted,” replaces the set of fields with a message telling them that they need to save to persist the change, and even offers the user the opportunity to undo that before saving the record.

This is how I typically implement the JavaScript to make this sort of thing happen:

This implements a couple functions and ties it all together with a click handler way at the end:

  • addRemoveAddressHandler() is split out into a separate function so we can reuse it later. It basically accepts the address fields from the user and marks it for deletion. This is accomplished by setting the address record’s _delete property to true, which will then be picked up and handled by CFWheels when the form is submitted.

  • addRemovalNotice() is called by the addRemoveAddressHandler() function just mentioned. Its job is to add the removal notice and Undo link when the user clicks the delete button.

  • At the bottom is a click handler that binds the “Remove Address” button to the addRemoveAddressHandler() handler.

So I mention above that addRemoveAddressHandler() is split out intentionally so it can be used elsewhere. Well, when the user clicks the “New Address” button, the new address added needs to have its “Remove Address” button bound to this new handler. Here is the updated ajax call:

With the success callback updated, when a new addresses is added, its “Remove Address” button should work correctly.

Progressively enhanced

There you have it. If you implement the server side logic first, you have functionality that works even without JavaScript. But then with the JavaScript handling added, you’ve enhanced the user experience without skipping much of the functionality that was built on the server.

The code examples in this post were copied, pasted, and modified from existing production code, but I was unable to run it. If you find yourself having trouble running any of it, let me know in the comments.