ColdFusion 8 - Tapping Into the Power of the YUI

Under the hood ColdFusion 8 uses the Yahoo! User Interface Library (YUI) for a lot of its cool new AJAX UI elements. That is great news for CF developers because the YUI is very well documented and has lots of excellent examples. Using these examples and documentation we can easily extend the capabilities of ColdFusion's new AJAX components beyond what Adobe has built for us.

For this post I'm going to show how easy it is to add context menus to the new AJAX version ColdFuison 8's cftree. This is basically a combination of an example on page 915 of the ColdFusion 8 Developer's Guide, Populating the tree using a bind expression (Example 1), and the "Adding A Context Menu To A TreeView" example from the YUI site. (Thanks to HostMySite you can see a working example here.)

First some code. We have two files, the first of which uses cfform to create a tree control:

<cfajaximport tags="cfmenu" />

<html>
<head>
   <title>Example: Adding a Context Menu to a Tree View</title>
   <script language="JavaScript">
   
   YAHOO.example.onTreeViewAvailble = function() {

      var oContextMenu,    // The YAHOO.widget.ContextMenu instance
      oCurrentTextNode = null; // The YAHOO.widget.TextNode instance whose "contextmenu" DOM event triggered the display of the context menu
      function editNodeLabel() {
         var sLabel = window.prompt("Enter a new label for this node:", oCurrentTextNode.getLabelEl().innerHTML);
         if(sLabel && sLabel.length > 0) {
            oCurrentTextNode.getLabelEl().innerHTML = sLabel;
         }
      }
            
      function onTriggerContextMenu(p_oEvent, p_oMenu) {
         /*
            Returns a TextNode instance that corresponds to the DOM
            element whose "contextmenu" event triggered the display
            of the context menu.
         */

    function GetTextNodeFromEventTarget(p_oTarget) {
       if(p_oTarget.tagName.toUpperCase() == "A" && p_oTarget.className == "ygtvlabel") {
               return ColdFusion.objectCache[p_oTarget.id];
            } else {
             if(p_oTarget.parentNode) {
                  return GetTextNodeFromEventTarget(p_oTarget.parentNode);
               }
            }
         }

         var oTextNode = GetTextNodeFromEventTarget(this.contextEventTarget);
         
         if(oTextNode) {
            oCurrentTextNode = oTextNode;
         } else {
            this.cancel();
         }
         
      }

      oContextMenu = new YAHOO.widget.ContextMenu(
         "mytreecontextmenu",
         {
            trigger: "t1",
            lazyload: true,
            itemdata: [
               { text: "Edit Node Label", onclick: { fn: editNodeLabel } }
            ]
         }
      );

      oContextMenu.triggerContextMenuEvent.subscribe(onTriggerContextMenu, oContextMenu, true);
   
   };
                     
   YAHOO.util.Event.onAvailable("t1", YAHOO.example.onTreeViewAvailble);      
      
   ColdFusion.Tree.loadNodes = (function (old) {
      return function (nodesArray, params) {
      
      var tempNode;
      
      if (typeof old == 'function') old.apply(this,arguments);
                         
      var tree = ColdFusion.Tree.getTreeObject('t1');
               
            
      // now we are going to loop over the tree nodes and put them in a cache          
      for (var i = 0; i < YAHOO.widget.TreeView.nodeCount; i++){
         tempNode = tree.getNodeByIndex(i);
         if (tempNode) {
            ColdFusion.objectCache[tempNode.labelElId] = tempNode;
         }
      }
                               
   };
   })(ColdFusion.Tree.loadNodes);
         
   </script>
</head>
<body>
   <h1>Example: Adding a Context Menu to a Tree View</h1>
   <cfform name="testform">
      <cftree name="t1" format="html">
         <cftreeitem bind="cfc:makeTree.getNodes({cftreeitemvalue},{cftreeitempath})">
      </cftree>
   </cfform>
</body>
</html>

The second is a simple component which returns nodes for our tree:

<cfcomponent>
   <cffunction name="getNodes" returnType="array" output="no" access="remote">
      <cfargument name="nodeitemid" required="true" />
      <cfargument name="nodeitempath" required="true" />
      
      <cfset var nodeArray = ArrayNew(1) />
      <cfset var element = StructNew() />
      <cfset var i = "" />
      
      <!--- the initial value of the top level is the empty string --->
      <cfif nodeitemid IS "">
         <cfset nodeitemid =0>
      </cfif>
      
      <!--- create a array with elements defining the child nodes --->
      <cfloop from="1" to="#RandRange(1,4)#" index="i">
         <cfset StructClear(element) />
         <cfset element.value = "#nodeitemid#.#i#" />
         <cfset element.display = "Node #element.value#" />
         <cfset element.expand = "false" />
         <cfset element.href = "index.cfm" />         
         <cfset element.leafnode = "false" />
         <cfset element.target = "_blank" />
         <cfset nodeArray[i] = Duplicate(element) />
      </cfloop>
      <cfreturn nodeArray />
   </cffunction>
</cfcomponent>

So what is going on here? Well first we use the cfajaximport tag to import the menu related JavaScript libraries. We do this because we are not using the cfmenu tag on this page, so we need to tell ColdFusion to load these libraries for us. (Note that because we are not using any ColdFusion menu features we don't really need cfmenu.js, one of the files cfajaximport loads. In this case it may actually be better to load the necessary libraries manually but I wanted to keep the example simple as possible so went ahead and used the cfajaximport tag.)

Next we have the "custom" code in the JavaScript block needed to add the context menu to the tree. Most of this code is straight from the Yahoo! example. I've removed the JavaScript code which generates the and populates the tree as we are relying on the cftree tag to generate our tree and our maketree CFC for tree values. I've also removed a couple of the context menu items and their associated functions just to keep the example simple.

Toward the bottom of the JavaScript block is the only real custom code I wrote for this example, a JavaScript closure which I use to overwrite the default ColdFusion.Tree.loadNodes function. ColdFusion.Tree.loadNodes is actually the callback function which gets called everytime we get data back from our makeTree CFC. This function uses the data returned from the CFC to create new YAHOO.widget.TextNode instances which it then inserts into the tree. The Yahoo! example expects the the TextNode objects used to populate the tree to be cached in an object called oTextNodeMap. For some reason Adobe decided not to cache the TextNode objects created in ColdFusion.Tree.loadNodes, so that is what this function does. Looking at the code, it first calls the original loadNodes function defined in the Adobe library. Next it gets the tree, which is an instance of the YAHOO.widget.TreeView object, using the built in getTreeObject function. Finally it loops over every node in the tree and adds them to the cache. Note that it uses the existing ColdFusion.objectCache here instead of the oTextNodeMap object for caching. The Adobe libraries create this object so I decided to go ahead and use it rather than creating a separate caching object for our TextNodes.

Finally we have the cfform and cftree tags which are taken straight from the ColdFusion documentation. The cftreeitem tag uses the bind attribute which allows our tree to be populated via AJAX calls to the makeTree.cfc.

So that is it. With just a few lines of custom JavaScript, most of which was provided for us by an existing YUI example, we were able to add context menus to the html tree generated by the cftree tag.

If you want to find out more about what the YUI can do beyond what has been pre-built into CF 8 definitely check out the YUI site.

Comments
Dan Wilson's Gravatar Great post Nathan. Thanks for showing some of the cool new features of ColdFusion 8.
# Posted By Dan Wilson | 6/5/07 2:42 PM
Mark's Gravatar I copied your example and get a javascript error.

missing variable name

function editNodeLabel()
# Posted By Mark | 11/4/07 6:13 AM
Nathan Mische's Gravatar @Mark, it seems if you copy and paste the following lines get compressed into one line:

var oContextMenu, // The YAHOO.widget.ContextMenu instance
oCurrentTextNode = null; // The YAHOO.widget.TextNode instance...

To fix the error you are seeing either remove the JavaScript comments, or break the statement up into two lines as displayed.
# Posted By Nathan Mische | 11/14/07 5:19 PM
keelee's Gravatar I know this is an old posting but, I would like to see the working copy that you provided on HostMySite .
# Posted By keelee | 1/16/09 2:19 PM
keelee's Gravatar I no longer need the visual example. I got that part working! I changed the code to reflect my cfc. However when I edit the Node Label, I would like tosvae thto the DB. How Can I do this? Need to have a function that does an update on the selected node. Help!
# Posted By keelee | 1/16/09 3:28 PM
Mischa's Gravatar Hello Nathan, thank you for posting this! Your code solved exactly my problem (adding a Tooltip to an element in the tree). However, I'm hoping you (or any other reader) can help me understand this:
If I replace in your code (which works fine for me btw) this line:

<cfset element.leafnode = "false" />

with this:

<cfset element.leafnode = "false" />
<cfif len(arguments.nodeitempath) GT 5>
<cfset element.leafnode = "true" />
</cfif>

Which basically limits the "depth" of the tree.

Unfortunately, the side effect becomes that the line

return function (nodesArray, params)

and everything that is in that function, is executed once for every leaf node. That means that if a branch contains 30 leaves, 900 iterations are done in javascript (30 times a loop over all 30 nodes).

I have some branches that contain over 100 items that causes the browser to peg the system for about 20 seconds. Any thoughts?

Thanks!
Mischa.
# Posted By Mischa | 1/26/09 3:25 PM
Saul's Gravatar I've been looking for several days at examples of using a flash file uploader. I was after single or multiple file uploads, a progress bar and the ability to add "vanilla" form fields. The most promising I found was the YUI uploader from those nice people at Yahoo http://developer.yahoo.com/yui/examples/uploader/u......

I got it working fine submitting to a simple CF page

<cfif structkeyexists(form,"Filedata")>
<cffile action="UPLOAD" filefield="Filedata" destination="#expandpath(".")#" nameconflict="OVERWRITE">
</cfif>

<cfif structkeyexists(form,"var1") and structkeyexists(form,"var2")>
<cffile action = "append" file = "#expandpath(".")#\log.txt" output = "var1 = #var1# var2 = #var2#">
</cfif>

I'm a complete novice with Javascript, PHP, flash in fact anything other than CF, so there's a bit I don't understand (well several ... but this one in particular!)

On the example in the YUI documentation they say they are using this PHP scrip to handle server side

1 <?php
2 foreach ($_FILES as $fieldName => $file) {
3 move_uploaded_file($file['tmp_name'], "./" . $file['name']);
4 echo (" ");
5 } ?>

The echo bit seems to "bounce back" the post data which is then picked up by the onuploadresponse(event). In CF how would I pass back some response to the calling page to fire this event?
# Posted By Saul | 3/2/09 4:19 AM
Geng's Gravatar What a great example, Thank you! I copied your code and it works.

I tried to add more items in the context menu like in the Yahoo! example, e.g. add a child, and I found some problems: when I added a node to a tree as following:

var nodeobj = { label: sLabel, expanded: false};
          
oChildNode = new YAHOO.widget.TextNode(nodeobj, oCurrentTextNode);
oCurrentTextNode.refresh();
oCurrentTextNode.expand();
      
ColdFusion.objectCache[oChildNode.labelElId] = oChildNode;

I can not set node properties like "img", "imgopen", "leafnode" as in cftreeitem, because nodes are Yahoo.widget.TextNode objects, there's no such properties
in TextNode class. Is there a way to solve this problem?
# Posted By Geng | 5/21/09 3:36 PM
Josh's Gravatar I am also having the same issue that Mischa is having, where if I set <cfset element.leafnode = "true" /> the function loadNodes is called repeatedly for each leafNode that is currently being displayed in the tree. Is there another function that can be used that only runs once on load of the tree, or how can I make this function only run once?
# Posted By Josh | 9/29/10 8:13 AM
Mischa's Gravatar Josh, I came up with a solution that basically determined whether the current node is being expanded and if it is, don't loop over the leaves to add tooltips. I obtain the unique identifier for the node that is currently being expanded like so:
CurrentNodeBeingExpanded = params.parent.parent.data.id;

and after that, it's basically keeping track of that value, check if it has changed, etc.
Hope this helps!
Mischa.
# Posted By Mischa | 9/29/10 9:00 AM
Josh's Gravatar Thanks Mischa, Your comment sent me in the correct direction and I have it working now.
# Posted By Josh | 9/29/10 2:46 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.8.001.