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.