Progressively enhancing your CFWheels form with nested properties and jQuery
There are 2 ways to approach this:
- Nested properties
- A tableless model with its own custom persistence logic
I’ll cover the first approach in this blog post.
Here are a couple models.
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
Initial controller setup
Now we’ll code a form for creating and editing a
contact record and its associated
The first step is to define our objects to bind in the form within the controller. I’ll setup a fairly standard controller at
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/edit.cfm share the
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
There are quite a few things going on in this file! Let’s break it down:
arguments.currentis referenced several times. CFWheels will provide that value whenever you loop through a collection using
includePartiallike 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
positionarguments to denote which associated object is being referenced and our current position in the array of objects.
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
positionarguments, so you need to manually tell it which object to reference via the
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
When you click the “New Address” button, it will post the entire form to the
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
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.
renderPage like that at the end stops processing in the controller, so the
update actions won’t run. If the
addAddress button wasn’t clicked, then the
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
So let’s code up a new
removeAddress controller filter in
Because the logic is fairly specialized, I decided to move the functionality into an instance method in the
Contact model, called
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
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 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
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
clickhandler that binds the “Remove Address” button to the
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
success callback updated, when a new addresses is added, its “Remove Address” button should work correctly.
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.