More articles about Ruby on Rails
More articles about User Interface
Using Yahoo's User Interface Library Treeview in Rails
By
Our BuildingWebApps.com site organizes much of its information by assigning one or more categories from a domain specific taxonomy to each article, link, or other piece of content. A site visitor can browse the information in several ways, but one quick approach is via a category browser widget that appears on most content pages. The hierarchical tree view is implemented by embedding Yahoo’s Open Source User Interface Library Treeview (aka YUI treeview, or just treeview here) into our RHTML. As of this writing, YUI is available under a BSD License and is part of a much larger, rich set of Javascript widgets and utility classes.

The treeview widget is implemented in Javascript, has many visual and interaction behaviors that can be tuned, it can be skinned, and it can work with static or dynamic data sources.
In our case, since our category data changes infrequently, we decided to implement a static data collection that we can cache, and which is loaded with the page in its entirety.
Here is how I did it.
Step One: Grab The Source From Yahoo
After downloading the latest libary, I dropped the default Javascript and CSS files into our Rails app’s public/javascripts and public/stylesheets directories, respectively. The YUI widget Javascript comes in several implementations: a debug version (treeview-debug.js) which is fully documented and instrumented, a production version (treeview.js), and an obfuscated, slimmed down version (treeview-min.js).
A couple of supporting libraries are also required for utility code and event handling (yahoo.js and event.js, both available with -min.js variants.)
Similarly, there are a number of stylesheets that are needed depending on the visual and interaction choices you make for your widget use. On the BuildingWebApps.com site, I wanted the control to act as a menu of sorts, such that only one category can be selected at once, and when I select a category in one part of the tree, I want any other prior choice to deselect. The treeview-menu.css contained the necessary defaults for this look and feel.
Like the Javascript, you can grab minimal versions of some of these CSS files (labeled with -min postfixes) when available.
One thing to note is that I customized some of our CSS to tweak the appearance for the site and snuck those snippets in our site CSS file instead.
Finally, I needed to prepare the visual assets for the disclosure triangle and bullet graphics. The library comes with a default set of sprite GIFs that you can open in any image editor and customize as needed. After some quick editing, I installed our GIFs into our public/images directory and tweaked the CSS to point to the correct place.
Step 2: Creating My Views
Next, I proceeded to get my view environment ready for widget usage. First, I plugged my stylesheet and javascript tags into my common layout’s header:
<%= stylesheet_link_tag 'treeview-menu' %> <%= javascript_include_tag 'yahoo-min.js'%> <%= javascript_include_tag 'event-min.js'%> <%= javascript_include_tag 'treeview-min.js'%>
I then created a partial template file _nav_widget to hold the treeview setup:
<h2>Browse <img src="/images/icons-browse.gif" width="25" height="25" align="absmiddle" /></h2>
<div id="contentTreeDiv"></div>
<script type="text/javascript">
(function() {
<%= category_tree_data_builder('category_data') %>
<%= yui_tree_builder('contentTreeDiv', 'category_data', item) %>
})();
</script>
When the page this code is embedded on loads, the anonymous Javascript function fires, and proceeds to run the Javascript that gets embedded in place here by my helper functions. This technique effectively name-spaces any of the code inside the scope of the anonymous function, protecting me from any inadvertent collisions. Once the Javascript produced by _yui_tree_builder_ executes, it replaces the div with id “contentTreeDiv” with the final treeview widget.
With my partial ready to go, I am able to start using it in my various pages by the normal render techniques:
<% content_for :right_middle do %>
<%= render :partial => '/categories/nav_widget', :locals => {:item => @category } %>
<% end %>
The mysterious local variable “item” permits me to hint to the treeview where I am in the display hierarchy so I can start the treeview open and expanded to that category.
As you can see, all of the magic is hidden in my helpers, _category_tree_data_builder_, which is responsible for constructing an array of data that is consumed by _yui_tree_builder_, which creates the Javascript that builds the widget.
Step 3: Inside The Helpers
My technique is inspired by Sonjaya Tandon’s HOW-TO: Integrate a YUI tree with Rails article. I’ve made some changes to start to support multiple treeviews and permit that “hinting” of where the user is when she navigates from one page with the widget to another (so the tree starts in an expanded mode at the appropriate category node).
_category_tree_data_builder__ constructs the array of data from our taxonomy of categories. You would change this code to walk your model as appropriate. For BuildingWebApps.com, the Category model is built with the acts_as_tree model extension, so walking the tree is simple. This implementation is slightly unwound to make it easier to follow:
def walk_category_recursive(level, parent)
ret = ""
parent.children.each do |node|
ret += "#{level}, \'"
ret += yield level, node
ret += "\',\n"
ret += walk_category_recursive(level + 1, node) { |lvl, n| yield lvl, n } unless node.children.empty?
end
ret
end
def category_tree_data_builder(tree_data)
data = "var #{tree_data} = [" +
walk_category_recursive(0, Category.root) {|lvl, node| link_to(h(truncate(node.title, (30 - lvl))), {:controller => 'topic', :action => 'show', :id => node}) }
# chop off the last trailing comma
data.chomp!.chop!
return data + "];\n"
end
Finally, we construct our Javascript code that builds the actual treeview widget:
def yui_tree_builder(tree_div_id, tree_data, expanded_label)
expanded_label = "NONE" if expanded_label.nil?
return "var tree; \n" +
"function #{tree_div_id}Init() {\n"+
"tree = new YAHOO.widget.TreeView(\"#{tree_div_id}\");\n"+
"addChildrenNodes(0, 0, tree.getRoot());\n"+
"tree.draw();\n"+
"};\n"+
"function addChildrenNodes(currLevel, nodeIndex, parent) { \n" +
"var lastNode;\n"+
"var level = currLevel;\n"+
"while (nodeIndex < #{tree_data}.length) {\n"+
"var level = #{tree_data}[nodeIndex];\n"+
"if (level == currLevel) {\n"+
"nodeIndex++;\n"+
"if (#{tree_data}[nodeIndex].indexOf(\"/#{expanded_label}-\") < 0) {\n" +
"lastNode = new YAHOO.widget.HTMLNode({html: \"<div class='browserNaviNotSelected'>\" + #{tree_data}[nodeIndex] + \"</div>\"}, parent, false, true);\n"+
"lastNode.multiExpand = false;\n"+
"lastNode.nowrap = true;\n"+
"} else {\n" +
"lastNode = new YAHOO.widget.HTMLNode({html: \"<div id='browserNaviCenter'><div id='browserNaviItem'>\" + #{tree_data}[nodeIndex] + \"</div></div>\"}, parent, true, true);\n"+
"lastNode.multiExpand = false;\n"+
"lastNode.nowrap = true;\n"+
"var p = parent;\n"+
"while (p != null) {\n"+
"p.expanded = true;\n"+
"p = p.parent;\n"+
"}\n"+
"}\n"+
"nodeIndex++;\n"+
"} else if (level < currLevel) {\n"+
"return nodeIndex;\n"+
"} else {\n"+
"nodeIndex = addChildrenNodes(level, nodeIndex, lastNode);\n"+
"}\n"+
"}\n"+
"return nodeIndex;\n"+
"};\n"+
"YAHOO.util.Event.onDOMReady(#{tree_div_id}Init);\n"
end
That’s pretty much it. Most of the tweaking that I’ve done since putting this code together has been on the style side of things to improve the appearance. Peeking inside of the YUI code reveals a number of node types, and if none of them fit your needs, it is easy to derive your own functionality in no time.
I will likely change the solution presented here if we ever dramatically grown our category taxonomy. Downloading the data en-masse isn’t ideal from a scalability and bandwidth usage point of view. When the time comes, it would be relatively simple to partition the data set into levels or layers of the tree, and dynamically load what is needed when certain nodes are exposed. In the mean-time, the existing solution is simple.
Reader comments on this article
From: Vivek Date: 09/21/08 03:15 PM
Subject: Intresting
Thanks,
From: hellebelle Date: 08/29/08 09:21 PM
Subject: adding strings
A string in ruby can span several lines (includes newlines too), so you only need two quotation marks and the JS in between. Or use the <<-TEXT_HERE construct.
From: Christopher Haupt Date: 04/09/08 09:09 AM
Subject: YUI File Location
The YUI files were placed like this: RAILSROOT/public/stylesheets/treeview-menu.css, RAILSROOT/public/stylesheets/fonts-min.css, RAILSROOT/public/javascripts/yahoo-min.js, RAILSROOT/public/javascripts/event-min.js, RAILSROOT/public/javascripts/treeview-min.js, RAILSROOT/public/images/sprite-menu.gif. I also updated treeview-menu.css to make sure it referenced the sprite-menu.gif graphic in the images directory.
From: george Date: 04/09/08 12:00 AM
Subject: great
i have been looking for such an example for some time! Let me try it out. Thanks
From: ishka Date: 04/08/08 11:11 AM
Subject: Dropping files
Could you explain me where are you copying the yui files exactly, please?
Join Our List
And we'll let you know when we post major new site updates.
We’ll never share your email address with anyone else.
Related Content
from around the Web
Documentation
This article is licensed under a Creative Commons Attribution-NoDerivs 3.0 License
From: mehedi Date: 09/21/08 07:19 PM
Subject: use ajax
thanks for such a nice tutorial.
but one thing i need to know that is i want to use link_to_remote instead of link_to method in the category_tree_data_builder method.
anyone can help me on this….