More articles about Ruby on Rails

More articles about User Interface

Using Yahoo's User Interface Library Treeview in Rails

By Christopher Haupt

Published April 23, 2008  |   4 comments

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.


Add your comment on this article






Reader comments on this article

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: RAILS_ROOT/public/stylesheets/treeview-menu.css, RAILS_ROOT/public/stylesheets/fonts-min.css, RAILS_ROOT/public/javascripts/yahoo-min.js, RAILS_ROOT/public/javascripts/event-min.js, RAILS_ROOT/public/javascripts/treeview-min.js, RAILS_ROOT/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


Creative Commons License

This article is licensed under a Creative Commons Attribution-NoDerivs 3.0 License