With a little side of applesauce...

Tuesday, December 30, 2008

cfajaxproxy / jquery - finally

I have been able to rely on Coldfusion's built-in AJAX functionality for most of my workflow project, but finally hit an instance where I needed to allow a document owner to delete a document listed in their queue. The queue is a simple tabular list of documents with enough information to help them decide which documents they want to edit/review. Here is the HTML for the table row:

          <tr id="docchoice_<cfoutput>#iter#</cfoutput>">
<!--- 1. query ldap for docobject information, and then format the following line with the objectid, full name, datetime, form submitted --->
<cfset arrLdapString = cfcUtilities.splitString( name )>
<cfset ldapString = arrLdapString[1]>
<cfset objectInfo = cfcWorkflowLdap.getDocumentObject( ldapString, 'ou=documents,#Session.AppBaseDn#')>
<cfoutput query="objectInfo">
<td width="7"><input type="radio" name="docchoice" value="<cfoutput>#docid#</cfoutput>" onclick="this.form.submit();" /></td>
<td id="docId_<cfoutput>#iter#</cfoutput>"><center><cfoutput>#docId#</cfoutput></center></td>
<td id="docFormName_<cfoutput>#iter#</cfoutput>"><center><cfoutput>#Replace( docFormName, "_", " ", "ALL")#</cfoutput></center></td>
<td id="docOwnerFullName_<cfoutput>#iter#</cfoutput>"><center><cfoutput>#docOwnerFullName#</cfoutput></center></td>
<td id="docCreationTime_<cfoutput>#iter#</cfoutput>"><center><cfoutput>#ListGetAt( docCreationTime, 1, '.')#</cfoutput></center></td>
<td><center>
<a id="delete-docchoice_<cfoutput>#iter#</cfoutput>">Delete</a>
</center></td>
</cfoutput>
</tr>


I wanted them to be able to delete each document, as necessary, so I have used jQuery to define a .click action to call RemoveDocChoice(), which performs some user interaction, then calls a cfc to remove the documentObject attribute from their state, (which basically unhooks the document from the workflow). Here is the javascript and cfajaxproxy definition:

<!--- let's open up our cfajaxproxy to cfcWorkflowLdap --->
<cfajaxproxy cfc="workflow.class.AjaxGeneral" jsclassname="setRemoveDocObjectFromState">

<script type="text/javascript">
$(
function(){
// Get any a tag with an 'id' starting with 'delete-docchoice'.
var jDeleteDocChoice = $( "a[id^='delete-docchoice']" );

// Hook up the click event.
jDeleteDocChoice
.attr( "href", "javascript:void( 0 )" )
.click(
function( objEvent ){
// on a click event, call the RemoveDocChoice function
RemoveDocChoice( objEvent);

// Prevent the default action.
objEvent.preventDefault();
return( false );
}
)
;

}
)

// This removes the doc object choice from the listdocumentsforcurrentstate.cfm.
function RemoveDocChoice( objEvent ){
// get the table row container that we would like to manipulate
var jTableRowContainer = $( "#docchoice_" + objEvent.target.id.split('_')[1] );

// get the text value for docId, so that we don't confuse the end-user with the array element,
// (which doesn't usually match the actual document id number).
var docId = $( "#docId_" + objEvent.target.id.split('_')[1] ).text();

// pop-up a confirmation box for the end-user to OK, or cancel
var answer = confirm("Would you like to delete Form ID " + docId + "?" );

// If the user has answered OK, then we remove the document object from its state
if (answer) {
// instantiate a javascript instance of our cfajaxproxy object
setRemoveDocObjectFromState = new setRemoveDocObjectFromState();

// set variables needed by our CFC to remove the document from its state
var stateDn = "<cfoutput>#Session.State#</cfoutput>";
var docOuBaseDn = "docObjectName=" + docId + ",ou=documents,<cfoutput>#Session.appBaseDn#</cfoutput>";

// we bypass setRemoveDocObjectFromState.setCallbackHandler(docObjectRemovalComplete_Handler) so that we still have
// our table row container within scope. (There is no to pass anything more than the returnStruct from our CFC
// as far as I can tell).
var returnStruct = setRemoveDocObjectFromState.setDelDocObjectFromState(stateDn=stateDn, docOuBaseDn=docOuBaseDn);

// if returnStruct.SUCCESS exists, then the LDAP removal was successful and we remove the container visually
// from the browser. Otherwise, pop-up an alert message and do nothing.
if ( returnStruct.SUCCESS )
jTableRowContainer.fadeOut(100);
} else {
msg = "We were unable to delete your document."
msg += "\n\nThe Server Replied: " + returnStruct.MESSAGE;
alert(msg);
}
}
}
</script>
<!--- end of js and cf inclusions --->


There are others who have a greater knowledge of jQuery and Coldfusion, but I wanted to point out a few things that were interesting to me, and may not have been documented in other places:

1.
<cfajaxproxy cfc="workflow.class.AjaxGeneral" jsclassname="setRemoveDocObjectFromState">


- there are many examples of CFCs being called from the same directory as the cfm which is calling it, but not many examples of loading CFCs in a different directory.
a. Login to the Coldfusion Administrator and choose "Server Settings -> Mappings".
b. Create a new Coldfusion Mapping. I map mine to the webroot of my webserver.
Logical Path - /
Directory Path - /var/www

The path to your CFC starts at the logical path, (your webroot), and ends with the CFC filename:
If path to CFC = /var/www/workflow/class/AjaxGeneral.cfc
then cfc="workflow.class.AjaxGeneral"

If path to CFC = /var/www/workflow/test/class/AjaxGeneral.cfc
then cfc="workflow.test.class.AjaxGeneral"

2. Using a jQuery selector to match dynamic "id" attributes.

- Since these are rows in a table, I have the above HTML wrapped in a cfloop, and simply add the index of the loop to the variables names so that they will be unique. All that we know is that each .click-able element will start with string "delete-docchoice":

<a id="delete-docchoice_<cfoutput>#iter#</cfoutput>">Delete</a>


jQuery has given powerful matching tools, (called selectors), to help us grab the correct elements.

var jDeleteDocChoice = $( "a[id^='delete-docchoice']" );


"a[id^='delete-docchoice']" -- our selector tells jQuery to look for an "a" tag with an "id" attribute starting with 'delete-docchoice'. Check out the following link, http://docs.jquery.com/Selectors, for a listing of options you can use to empower your element searches.

3. objEvent is the event object which is passed into the javascript, and the methods which are used to access information about the object or modify behaviors are documented here.

4. Since our table row container is unique within the table, we need to split the 'id' attribute from the event object and split the number off of the end, (which is the unique number of this row). We will use this number to obtain the correct table row container from the DOM.

var jTableRowContainer = $( "#docchoice_" + objEvent.target.id.split('_')[1] ); 


By using the objEvent.target method, we are able to grab the 'id' attribute from the event object, split it on the underscore, and use the second element, [1], of the array returned by split().

5. I included the Javascript within the listdocumentsforcurrentstate.cfm, (as opposed to the normal .js include file), as I needed access to the Coldfusion Session variables for my CFC call:

        // set variables needed by our CFC to remove the document from its state
var stateDn = "<cfoutput>#Session.State#</cfoutput>";
var docOuBaseDn = "docObjectName=" + docId + ",ou=documents,<cfoutput>#Session.appBaseDn#</cfoutput>";


6. I didn't set a setCallbackHandler, as I needed to keep the jTableRowContainer variable in scope, and didn't want to spend time figuring how to pass it into the CFC and then back out again in the returnStruct. Also, notice that you can pass any number of variables into your CFC function:

            var returnStruct = setRemoveDocObjectFromState.setDelDocObjectFromState(stateDn=stateDn, docOuBaseDn=docOuBaseDn);


7. Here is our CFC function in /var/www/workflow/class/AjaxGeneral.cfc:

    <cffunction name="setDelDocObjectFromState"
access="remote"
return="struct"
hint="deletes an object from the state, returns a struct to javascript
@return result struct">
<cfargument name="stateDn" type="string">
<cfargument name="docOuBaseDn" type="string">

<cfset var returnStruct = structNew() />
<cfset returnStruct.success = true />

<cftry>
<cfset cfcWorkflowLdap.delDocObjectFromState( stateDn, docOuBaseDn )>
<cfcatch type="any">
<cfset returnStruct.success = false />
<cfset returnStruct.message = cfcatch.message />
</cfcatch>
</cftry>
<cfreturn returnStruct />
</cffunction>



It simply calls cfcWorkflowLdap.delDocObjectFromState() to do the work. By default, returnStruct.success equals 'true', but if the call to LDAP errors for any reason, it changes that value to 'false'. NOTE: Any advice on how to make this function more secure from outside calls would be very welcome.

8. Notice that returnStruct.SUCCESS becomes uppercase in transit from Coldfusion to Javascript. Remember this very important note from Charlie Griefer's post, "cfajaxproxy - the other white meat", "javascript is case-sensitive, and ColdFusion converts structure key names to all uppercase".

        // if returnStruct.SUCCESS exists, then the LDAP removal was successful and we remove the container visually 
// from the browser. Otherwise, pop-up an alert message and do nothing.
if ( returnStruct.SUCCESS )
jTableRowContainer.fadeOut(100);

...


9. With jQuery, we make the table row magically disappear:

      jTableRowContainer.fadeOut(100);


Thanks to Charlie Griefer for the following blog posting, "cfajaxproxy - the other white meat", which helped me on my way with cfajaxproxy.

2 comments:

Anonymous said...

Great post.
I need to do something similar and this helps.

Shannon Eric Peevey said...

I'm glad it was helpful. Let me know if you post your items, as I would be excited to see how you did it :)