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.

 

Incoming search terms:

  • cfwheels
  • cfwheels associations sample
  • http://www.henke.ws Mike Henke

    I might be missing it but don’t see the create action on either blog entries. Could you post it? Thanks

  • Tom

    @Mike, it would be very similar to the update function, just using create() instead.









  • http://www.azoln.com chris passmore

    I’m trying to work with nested sets right now and am having a bit of trouble (Rails seems like it’s able to deal with this stuff more ‘automagically’).

    Anyway, instead of getting a nice number to add to for additional fields, wheels (1.1.3) is spitting out a id like this:
    warrantyClaim[claimFailures][1307564205613][failureid]

    Now, is there some error that’s not showing itself that’s causing this? SHOULD it start out with a [1] value? Instead of nadel’s duplication approach (used in the past) i’m using .clone() method (jquery) so i suppose I could grab the number and add to that, but seems a bit awkward.

    Thanks for any help you can offer.

  • http://www.oxalto.co.uk Tom K

    Without knowing the context, it’s a little hard to say: Might be worth an ask on the cfwheels list?

  • http://www.dcolumbus.net dcolumbus

    Tom, thanks for taking the time to put these two tutorials together. I’m struggling with the CREATION of a many-to-many relationship. Say, if your relationship was:

    TABLE users
    id
    name

    TABLE groups
    id
    name

    TABLE usertogroups
    userId
    groupId

    How would you CREATE a new “group” and then connect the user within “usertogroups”?

  • Tom

    So this particular post doesn’t quite deal with that – I’m assuming (to use your example) that groups would be pre-created (or created elsewhere).

    The nested property in this case is simply the relationship between groups and users (usertogroups).

    I think you’d need some logic to parse an ‘other’ field to add a new group in this example, which you’d do outside the create/update process.

    There’s no doubt a way to do this, I’ve just not got there myself yet !

  • Troy Murray

    Hey Tom, thanks for the blog posts. Anyway you could put this code up on GitHub so I could download and work with it to learn how this all works?

  • Tom

    This example was sort of ripped from an existing app – but I’ll see what I can do..

  • David

    @chris passmore Looks like they’re using tick counts to track instead of simple IDs: http://code.google.com/p/cfwheels/source/detail?r=4794

    Per the diff here: http://code.google.com/p/cfwheels/source/diff?spec=svn4794&r=4794&format=side&path=/trunk/wheels/model/nestedproperties.cfm it looks like we should be able to put in any type of tickcount and have that new model generated in the appropriate position.

    Off to try this on my site