cfWheels Nested Properties with One to Many and Many to Many Relationships Part 2

Right, following on from part 1…

We should at this point be getting the _emailaddress partial loaded in our form. What I want to do, is be able to add additional email addresses, and get Wheels to update the nested properties appropriately when I submit the form. Oh, I’m assuming you’re using jQuery too.

Firstly, a disclaimer. Javascript isn’t my strong suit, at all, in any way, whatsoever. The following will undoubtedly be able to be condensed down into something much more efficient. Also, this isn’t my javascript – this is unashamedly nicked from Ben Nadel (see http://www.bennadel.com/blog/1375-Ask-Ben-Dynamically-Adding-File-Upload-Fields-To-A-Form-Using-jQuery.htm). Yet again, I find myself standing on the shoulders of giants.

In order to understand what I’m doing, it’s probably best to look at the generated code which wheels makes for our email address partial.

As a reminder, here’s what the cfWheels code is:

<!---_emailaddress.cfm--->
<cfoutput>
#textField(objectName="contact", association="emailaddresses", position=arguments.current, property="email", label="Email Address", size=62, class="email")#
#select(objectName="contact", association="emailaddresses", position=arguments.current, property="type", label="Type", options="Home,Work")#
#checkbox(objectName="contact", association="emailaddresses", position=arguments.current, property="preferred", label="Preferred")#
</cfoutput>

So this creates:

<label for="contact-emailaddresses-1-email">Email Address</label>
<input type="text" value="" size="62" name="contact[emailaddresses][1][email]" maxlength="500" id="contact-emailaddresses-1-email" class="email valid">

<label for="contact-emailaddresses-1-type">Type</label>
<select name="contact[emailaddresses][1][type]" id="contact-emailaddresses-1-type">
<option value="Work" selected="selected">Work</option><option value="Home">Home</option>
</select>

<label for="contact-emailaddresses-1-preferred" class="checkboxLabel">Preferred</label>
<input type="checkbox" value="1" name="contact[emailaddresses][1][preferred]" id="contact-emailaddresses-1-preferred" class="checkbox" checked="checked">
<input type="hidden" value="0" name="contact[emailaddresses][1][preferred]($checkbox)" id="contact-emailaddresses-1-preferred-checkbox">

What we want to do is replicate this output on the fly using Javascript, and increment the counter (i.e, all the 1’s in the above example).

In order to do this, I need to do a few things. Firstly, I need to make sure I can reference the existing set of email(s), which are loaded when the page loads, then I need to be able to clone this set of fields, incrementing the count as I go, and finally, I need to be able to have appropriate add and remove buttons/handlers to deal with manipulating the DOM itself.

Once that’s done, I then can submit the form and do the update: since writing part one, I’ve come accross a small catch with this approach which requires a extra line or two of code (oh noes!) which I’ll get to later.)

Creating theDOM Template

So, in addition to my email address partial, I’m going to create another, called _emailaddressTemplate – Warning, bad code ahead…

<!--- Dynamic Email Field Template--->
<div id="email-templates" class=" clearfix" style="display: none ;">
    <div id="::FIELD1::" class="emailtemplate clearfix">
      <div class="span-11">
        <div class="field">
          <label for="contact-emailaddresses-::FIELD2::-email">Email Address</label>
          <input type="text" value="" size="62" name="contact[emailaddresses][::FIELD12::][email]" maxlength="500" id="contact-emailaddresses-::FIELD3::-email" class="email">
        </div>
      </div>
      <div class="span-3">
        <div class="field">
          <label for="contact-emailaddresses-::FIELD4::-type">Type</label>
          <select name="contact[emailaddresses][::FIELD5::][type]" id="contact-emailaddresses-::FIELD6::-type">
            <option value="Work">Work</option>
            <option value="Home">Home</option>
          </select>
        </div>
      </div>
      <div class="span-3">
        <label for="contact-emailaddresses-::FIELD7::-preferred" class="checkboxLabel">Preferred</label>
        <div class="checkbox">
          <input type="checkbox" value="1" name="contact[emailaddresses][::FIELD8::][preferred]" id="contact-emailaddresses-::FIELD9::-preferred" class="checkbox">
          <input type="hidden" value="0" name="contact[emailaddresses][::FIELD10::][preferred]($checkbox)" id="contact-emailaddresses-::FIELD11::-preferred-checkbox">
        </div>
      </div>
      <div class="span-3 last prepend-top">
        <p><a class="button negative removeemail" href="">Remove</a></p>
      </div>
    </div>
</div>

<script>
// Another bit of JS nicked from Ben Nadel.
// When the DOM has loaded, init the form link.
$(
function addemail(){
var jAddNewRecipient = $( "#addnewemail" );
  jAddNewRecipient
.attr( "href", "javascript:void( 0 )" )
.click(
function( objEvent ){
AddNewUpload();
  objEvent.preventDefault();
return( false );
}
);
}
)

$('.removeemail').live('click',function() {
    $(this).parents("div.emailtemplate:first").remove();
return( false );
});


function AddNewUpload(){
var jFilesContainer = $( "#emails" );
  var jUploadTemplate = $( "#email-templates div.emailtemplate" );
var jUpload = jUploadTemplate.clone();
var strNewHTML = jUpload.html();
var intNewFileCount = (jFilesContainer.find( "div.emailtemplate" ).length + 1);
jUpload.attr( "id", ("emailedit[" + intNewFileCount + "]") );
  strNewHTML = strNewHTML
.replace(
new RegExp( "::FIELD1::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD2::", "i" ),
intNewFileCount
)
  .replace(
new RegExp( "::FIELD3::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD4::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD5::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD6::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD7::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD8::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD9::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD10::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD11::", "i" ),
intNewFileCount
)
.replace(
new RegExp( "::FIELD12::", "i" ),
intNewFileCount
)
 
;
 
jUpload.html( strNewHTML );
  jFilesContainer.append( jUpload );
}
</script>

So what’s going on here? At the top, I’ve got a template, using Ben’s ::field:: references. Underneath I’ve got the JS to replicate the template and insert it in the appropriate place, and increment the counter.

This partial needs to be include *OUTSIDE* the form: this is important: otherwise these oddly named form fields will get into your params and cause problems.

Also note, I’ve got an anchor tag with class of .removeemail – this allows me to remove the parent div element onclick, thus removing it from the the form.

Back in my edit.cfm, I’m going to add these includes, and add another anchor tag to add the additional form fields. So it now looks something like this:

<cfoutput>
<cfif params.action EQ "add">
    <h2>Add a New Contact</h2>
    #startFormTag(class="generic", id="contact-edit", action="create")#
<cfelse>
    <h2>Editing Contact</h2>
    #startFormTag(class="generic", id="contact-edit", action="update", key=params.key)#
</cfif>
    
    #errorMessagesFor("contact")#
        #select(objectName="contact", property="prefix", includeBlank=true, options=application.oii_contacts.prefixes, label="Prefix", title="Optional prefix, such as Dr, Professor etc")#
        #textField(objectName="contact", property="firstname", label="First Name *", class="required", minlength="2", title="First Name, required, needs as least 2 chars")#
        #textField(objectName="contact", property="middlename", label="Middle Name", title="Middle Name, optional")#
        #textField(objectName="contact", property="lastname", label="Last Name *", class="required", minlength="2", title="Last Name, required, needs as least 2 chars")#
<!--- snip... --->

        <div id="emails">
         #includePartial(contact.emailaddresses)#
         </div>
         <a href="" id="addnewemail" class="button">Add Another Email</a>

         <!--- Categories ---->
<cfloop query="categoryTypes">
#hasManyCheckBox(label=name, objectName="contact", association="categories", keys="#contact.key()#,#categoryTypes.id#")#
</cfloop>
        #submitTag(class="edit", value="Update Contact")#
     #endFormTag()#
     
     <!---Hidden DOM Templates --->
     #includePartial("emailaddressTemplate")#
</cfoutput>

So important to note, my DOM template partial is outside the form.

The catch I mentioned earlier comes when updating this: as is stands, I’ve not got a way of telling wheels which email addresses to delete etc. so when I loaded the contact model in my update function (see part 1), it would update and not replace the nested entries. I’ve done the following to simply replace them: More disclaimers – I can bet there’s something I’ve missed, or a better way of doing this: cfWheels gurus please do enlighten me!!

My new update() function in Contacts.cfc controller:

<cffunction name="update">
    <cfloop from=1 to="#arraylen(contact.emailaddresses)#" index="i">
     <cfset contact.emailaddresses[i].delete()>
    </cfloop>
        <cfset contact.update(params.contact)>
        <cfif contact.hasErrors()>
         <cfset renderPage(action="edit")>
        <cfelse>
<cfset flashInsert(success="The contact was updated successfully.")>
<cfset redirectTo(action="view", key=contact.id)>
        </cfif>
    </cffunction>

This works for me, but as you can see, there’s a fair bit of tidying up to be done, especially on the JS end.

 

cfWheels Nested Properties with One to Many and Many to Many Relationships Part 1

I’ve just started development on a new cfWheels application, and since 1.1.2 has been released, I’ve been meaning to dig down into some of the newer features and functions, such as nested properties. This isn’t a small topic, but once you get the gist of what cfWheels is doing behind the scenes, you may well sit there with your jaw on the floor for a little bit.

So, Nested Properties – where to start? Well, what’s one of the most common things about any ‘relational database based’ (try saying that three times quickly) application? Join tables: updating, deleting and adding those joins – done traditionally, it’s a bit of a chore, let’s be honest.

So how does Wheels do it?

Let’s take a simple, real world example.

You have a Contact. They have multiple email addresses, and that contact might also be classed into multiple categories.

Our email addresses are unique to each contact, so this is a ‘one to many’ relationship. One contact, multiple addresses.

Our contact may be in multiple categories, i.e this could be something like Alumni, Funder etc. the point is with these values, you don’t want to repeat them for each contact – the data is repeated: additionally, these categories are predefined, so having them as a database table is more convienient. So this is a many to many relationship: a Contact may have multiple categories, and each category can encompass multiple contacts.

So let’s look at the models and database table for what we’ve got so far.

Database Tables:

contacts (Our main contacts object)

emailaddresses (our email address storage)

categorytypes (our list of categories)

categories (our join table)

Models:

<!---Contact.cfc--->
<cfcomponent extends="Model" output="false">
<cffunction name="init">
<cfset property(name="createdBy", defaultValue=session.currentuser.id)>
<cfset property(name="updatedBy", defaultValue=session.currentuser.id)>
<cfset hasMany(name="emailaddresses", dependent="deleteAll")>
<cfset hasMany(name="categories", dependent="deleteAll")>
<cfset nestedProperties(associations="emailaddresses,categories", allowDelete=true)>
</cffunction>
</cfcomponent>

<!---EmailAddress.cfc--->
<cfcomponent extends="Model" output="false">
<cffunction name="init">
<cfset belongsTo("contact")>
</cffunction>
</cfcomponent>

<!---CategoryType.cfc--->
<cfcomponent extends="Model" output="false">
<cffunction name="init">
<cfset hasMany(name="categories")>
</cffunction>
</cfcomponent>

<!---Category.cfc--->
<cfcomponent extends="Model" output="false">
<cffunction name="init">
<cfset belongsTo("contact")>
<cfset belongsTo("categoryType")>
</cffunction>
</cfcomponent>

So now we’ve got the basics setup, let’s look at actually using these associations in a meaningful way.

 

Adding a new contact

My Contacts.cfc controller will be handling all the CRUD operations for the contact model. Because of our nested properties, when we create the initial contacts object, we need to also create the email address and categories objects *as part of* the contacts object.

<!---Contacts.cfc--->
<cffunction name="init">
<cfset filters(through="getCategoryTypes", only="add,edit,update")>
<cfset filters(through="getCurrentContact", only="view,edit,update")>
<cfset verifies(only="getCurrentContact", params="key", paramsTypes="integer")>
</cffunction>

<cffunction name="add" hint="Add a New Contact">
<cfset var newEmailaddress[1]=model("emailaddress").new()>
<cfset var newCategory[1]=model("category").new()>
<cfset contact=model("contact").new(emailaddresses=newEmailaddress,categories=newCategory)>
<cfset renderPage(action="edit")>
</cffunction>

<cffunction name="update">
<cfset contact.update(params.contact)>
<cfif contact.hasErrors()>
<cfset renderPage(action="edit")>
<cfelse>
<cfset flashInsert(success="The contact was updated successfully.")>
<cfset redirectTo(action="view", key=contact.id)>
</cfif>
</cffunction>

<cffunction name="getCategoryTypes" access="private">
<cfset categoryTypes=model("categoryTypes").findAll()>
</cffunction>
   
<cffunction name="getCurrentContact" access="private">
<cfset contact=model("contact").findone(where="id=#params.key#", include="emailaddresses,categories")>
</cffunction>

Where possible, I try and reuse the edit/add forms, as this means you’re not repeating yourself (hence the renderPage bit).

You’ll notice two private functions: one just gets the predefined values for Categories, and the other gets the Current Contact, and *includes* our email addresses and categories. By using a filter, I don’t need to repeat myself later when we’ve got the view functions, and I also don’t need an entry for edit or view. Additionally, I’m verifying the getCurrentContact method, checking it always has params.key as an integer.

Also, not that I’ve created newCategory and newEmailAddress as private vars (this is just to keep it self contained), but i’ve also created them as one dimensional arrays: the new Objects go in the first position in these arrays – keep this in the back of your mind.

My main edit form will end up looking something like this:

<!---Edit.cfm--->
<cfif params.action EQ "add">
    <h2>Add a New Contact</h2>
    #startFormTag(action="create")#
<cfelse>
    <h2>Editing Contact</h2>
    #startFormTag(action="update", key=params.key)#
</cfif>

#errorMessagesFor("contact")#

<!--- Basic info for contact --->
#textField(objectName="contact", property="firstname", label="First Name *", class="required")#
#textField(objectName="contact", property="middlename", label="Middle Name")#
<!--- Snip Etc…--->

<!--- Email Addresses --->
#includePartial(contact.emailaddresses)#

<!--- Categories ---->
<cfloop query="categoryTypes">
#hasManyCheckBox(label=name, objectName="contact", association="categories", keys="#contact.key()#,#categoryTypes.id#")#
</cfloop>

#submitTag()#
#endFormTag()#

Immediately, there will probably be two bits which raise an eyebrow: 1) the ‘includePartial’ call for email addresses, and 2) the HasManyCheckBox.

Let’s take the categories first as it’s a bit simpler.

The categoryTypes loop loops over each checkbox. Wheels then checks for an association named categories on the contact object. A composite key is used to look for the existence of the record in the join table. If it exists, it’ll appear checked. The best part is, that as we’re looping over the categoryTypes query, we’ve got access to the actual values of the table, so in this example, the categoryTypes.name column will appear as the label. Nice.

The Email Address includePartial:

This is ‘slightly’ more complicated, but not much. If you remember, we created the contacts object with two additional nested objects (in arrays).
The partial loops over the contacts.emailaddresses entry, and seeing an array, loops over that too. Cunning.

<!---_emailaddress.cfm--->
<cfoutput>
#textField(objectName="contact", association="emailaddresses", position=arguments.current, property="email", label="Email Address", size=62, class="email")#
#select(objectName="contact", association="emailaddresses", position=arguments.current, property="type", label="Type", options="Home,Work")#
#checkbox(objectName="contact", association="emailaddresses", position=arguments.current, property="preferred", label="Preferred")#
</cfoutput>

So the important part here is the association, and the position. The position is used by Wheels to say which iteration of the loop you’re on, and the association helps point back to the main contact model.

In Part 2 I’ll look at how we can leverage some javascript to ‘add and remove’ email addresses with this setup.