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.




missing variable name
function editNodeLabel()
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.
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.
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?
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?