Howto use the Ext JS Treeview (Ext.tree) with Ruby on Rails

January 26th, 2008

ExtJS Tree Sample ScrenshotIn this little tutorial I am going to show you, how to connect a Ext.tree component (from the remarkable Ext JS Javascript framework) to a Ruby on Rails backend.

For starters, we need to download and extract the Ext framework to public/ext in our Rails project folder. We are going to use Rails 2.1.x for this Tutorial. That’s not a particular requirement - but you would have to adapt certain Rails 2 concepts should you be using an older version.

To model a tree in Rails, we are going to use the acts_as_nested_set plugin - install it using script/plugin install acts_as_nested_set now.

To demonstrate the Ext.tree I will be using a Category model, having a root category with recursive sub-categories. We can use the Rails resource generator to initialize all neccessary files for us:

script/generate resource Category parent_id:integer lft:integer rgt:integer text:string

As you can see, each of our categories is just carrying a text attribute to store its name and the required attributes for the nested set (parent_id, lft, rgt).

To use the Category model, we just have to add the acts_as_nested_set decorator to the Category class, so it looks like this:

class Category < ActiveRecord::Base
  # For Rails 2.1: override default of include_root_in_json
  # (the Ext.tree.TreeLoader cannot use the additional nesting)
  Category.include_root_in_json = false if Category.respond_to?(:include_root_in_json)

  acts_as_nested_set
end

We can now create some sample data using script/console (you did run rake db:migrate already, didn’t you? ;-) ):

r = Category.create(:text => 'Frameworks')
r.add_child(c1 = Category.create(:text => 'Ruby on Rails'))
c1.add_child(Category.create(:text => 'Model'))
c1.add_child(Category.create(:text => 'View'))
c1.add_child(Category.create(:text => 'Controller'))
r.add_child(c2 = Category.create(:text => 'Ext JS'))
c2.add_child(c21 = Category.create(:text => 'tree'))
c21.add_child(Category.create(:text => 'TreePanel'))
c21.add_child(Category.create(:text => 'AsyncTreeNode'))
c21.add_child(Category.create(:text => 'TreeLoader'))

On to our CategoriesController. We just need an index method that will initially deliver a (rather static) index.html.erb view. The same method can then be used to provide JSON data to be consumed by Ext.tree like this:

class CategoriesController < ApplicationController
  def index(id = params[:node])
    respond_to do |format|
      format.html # render static index.html.erb
      format.json { render :json => Category.find_children(id) }
    end
  end
end

We are giving the index method a node query parameter (assigned to a id variable) that Ext will later use to dynamically request sub-trees as they are extended in the UI.

Obviously we will have to add more code to our Category model as it currently does not respond to the find_children method. Let’s use the following code to provide it - when no id or zero is given, the root node(s) of the tree will be returned:

  # add to model/category.rb
  def self.root_nodes
    find(:all, :conditions => 'parent_id IS NULL')
  end

  def self.find_children(start_id = nil)
    start_id.to_i == 0 ? root_nodes : find(start_id).direct_children
  end

After starting script/server we can now fire up our browser and request http://localhost:3000/categories.json. Comparing the generated JSON with the format expected by Ext.tree.TreeLoader reveals that each element should supply a boolean attribute called leaf telling the tree if it can be further expanded or is a final leaf of the tree. To keep our controller skinny as we like it, we will add further methods to the Category model enabling its standard to_json method to also supply the required leaf attribute.

  # add to model/category.rb
  def leaf
    unknown? || children_count == 0
  end

  def to_json_with_leaf(options = {})
    self.to_json_without_leaf(options.merge(:methods => :leaf))
  end
  alias_method_chain :to_json, :leaf

Now that we have the complete backend code in place, we just need to put some Ext Javascript into categories/index.html.erb to try the tree live:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title>Ext.tree with Ruby on Rails Example</title>
  <%= stylesheet_link_tag "../ext/resources/css/ext-all.css" %>
  <%= javascript_include_tag :defaults %>
  <%= javascript_include_tag "../ext/adapter/prototype/ext-prototype-adapter.js" %>
  <%= javascript_include_tag "../ext/ext-all.js" %>
</head>
<body>
  <div id="category-tree" style="padding:20px"></div>
  <% javascript_tag do -%>
    Ext.onReady(function(){
      // create initial root node
      root = new Ext.tree.AsyncTreeNode({
        text: 'Invisible Root',
        id:'0'
      });
      // create the tree
      new Ext.tree.TreePanel({
        loader: new Ext.tree.TreeLoader({
          url:'/categories',
          requestMethod:'GET',
          baseParams:{format:'json'}
        }),
        renderTo:'category-tree',
        root: root,
        rootVisible:false
      });
      // expand invisible root node to trigger load
      // of the first level of actual data
      root.expand();
    });
  <% end -%>
</body>
</html>

Now it is time to look at the final view (http://localhost:3000/categories). If you use Firebug or something similar you can track the AJAX requests made by Ext.tree to populate the tree dynamically when you expand nodes. Nice, huh?

Recommend Martin Rehfeld on Working With RailsIf you like this tutorial, please consider recommending me on Working with Rails. Thank you!

Related Posts

Entry Filed under: Ruby on Rails, Howto, Ext JS

20 Comments Add your own

  • 1. Joerg Battermann  |  January 27th, 2008 at 3:15 pm

    Now that’s nice… cool! Thanks for the tutorial :)

    Quick Q: does this also support/work with drag ‘n’ dropingto change the hierarchy?

  • 2. martin.rehfeld  |  January 27th, 2008 at 3:26 pm

    @Joerg: Nope, there’s no code for D&D in the tutorial. Ext.tree does support drag & drop, of course. So hack away and feel free to submit your solution as a follow-up comment.

  • 3. Joerg Battermann  |  January 28th, 2008 at 12:01 am

    Will do :)

  • 4. Vipin  |  January 29th, 2008 at 10:30 am

    hey its really cool… thanks for the plug in :) .

    i have a small problem - i want to show the details of the node when i click nodes (view / model/ controller etc…). for that i added another column in the db - “href”. its working too. but the page is refreshing because action goes like. how can i make this call in ajax?

  • 5. martin.rehfeld  |  January 29th, 2008 at 11:07 am

    @Vipin/#4: You should explicitly declare a SelectionModel and attach a listener to i.e. the beforeselect event. You can make your AJAX request from there. You should rename your href attribute as Ext will automatically pick that up as you noticed.

  • 6. James  |  January 29th, 2008 at 11:36 am

    Thanks for the plugin, its real cool.
    I have also got the same problem as Vipin above had mentioned.
    Say I have the tree display on the left side of my page and clicking on each node, I need to display the details of each node on the right side of the page. How do I get the action to work with AJAX?
    It would be helpful if you could help me out on this one with a more detailed or descriptive explanation or any tutorials that you have come across that could guide me.
    Thanks in advance.

  • 7. martin.rehfeld  |  January 29th, 2008 at 12:06 pm

    @James/#6: Here is a little article I recently came accross outlining a different approach on node click handlers than proposed in comment #5.

  • 8. James  |  January 31st, 2008 at 11:54 am

    Got it to work finally. Did some code integration between the two and it finally worked. Thanks for the link.

  • 9. (e)  |  February 6th, 2008 at 11:10 pm

    One update I think should be included is to change the leaf() method to the following


    def leaf
    unknown? || children_count == 0
    end

    I was getting an error when multiple root nodes were in the database with no subchildren assigned yet. Thus the lft and rgt columns were nil. children_count() fails in those cases because nil can’t subtract.

    the unknown?() instance method of ActsAsNestedSet allows that method to proceed and not fail.

  • 10. martin.rehfeld  |  February 6th, 2008 at 11:21 pm

    @(e)/#9: Thanks for pointing this out - I just updated the sample code as suggested.

  • 11. Tom  |  February 11th, 2008 at 9:50 am

    Thank you very much for your tutorial!
    Does anybody know how to change the fact that I need a “text”-column in my database? I would like to have a column called “name” to be loaded in order to display a tree’s node. I haven’t been able to find those config parameters.

    Thank you very much in advance,
    Tom

  • 12. martin.rehfeld  |  February 11th, 2008 at 12:05 pm

    @Tom/#11: I’d probably just define a alternate getter named text for your name attribute in category.rb and then change the body of to_json_with_leaf to read self.to_json_without_leaf(options.merge(:methods => [:leaf, :text])). HTH

  • 13. Vipin  |  February 19th, 2008 at 12:18 pm

    Hi All,

    i have another problem. am loading the tree from db. when am adding a new node to the tree, i need to display it immediately. but when am refreshing the page my tree is being collapsed and root node is visible. tree.expandAll(); method expands all the nodes but i need a selected branch only (where am added the new node). how can i do it? please help me.

    Thanks in advance.
    Vipin

  • 14. martin.rehfeld  |  February 19th, 2008 at 1:15 pm

    @#13/Vipin: Maybe you could look into using a Ext state provider, i.e.
    Ext.state.Manager.setProvider(new Ext.state.CookieProvider()); and make the tree stateful (add stateful:true to its options). I am not sure if this works out of the box but it should enable you to keep the expansion state between page reloads (if it does not work out of the box, look into tweaking tree.stateEvents)

  • 15. Alex Tugarev  |  June 21st, 2008 at 8:26 am

    Thank you for this tutorial!

    I’ve just started using Ext with Rails and I like it. But I had some stones on my way following this tutorial. Because I’ve started my last app with the latest 2.1 version of Rails and the output was broken I had to make some research for differences of Rails versions.

    You have to insert this code line

    # add to model/category.rb
    Category.include_root_in_json = false

    to run on Rails 2.1.

    Or maybe you have a better idea how to use the changed to_json methode?

    Thanks
    Alex

  • 16. jaigouk  |  July 8th, 2008 at 4:06 pm

    WOW. Thanks for this great tutorial.
    And as Alex said, that one line is needed to run on Rails2.1.

  • 17. Pauli  |  July 22nd, 2008 at 11:50 pm

    The following tutorial is EXT JS vs CakePHP, but it implements drag and drop — it will be helpful for most Rubists to read it, because the corresponding Ruby should be relatively easy to figure out.

    http://blogs.bigfish.tv/adam/2008/02/12/drag-and-drop-using-ext-js-with-the-cakephp-tree-behavior/

    PS. I’m not affiliated with the site, just found it first, before this one

  • 18. Phillip Oertel  |  August 14th, 2008 at 1:03 am

    regarding Rails 2.1 and Category.include_root_in_json:

    including the root node in the JSON serialization is a new default for rails (see config/initializers/new_rails_defaults). but that breaks the example, so this default has to be set to false.

    i think the cleanest way to do this is adding
    ActiveRecord::Base.include_root_in_json = false
    to environment.rb.

    phillip

  • 19. baddog  |  October 12th, 2008 at 8:12 pm

    This howto is almost completely our of date. addChild has been deprecated. The id returned by the tree control is not an integer, so the rest of the code doesn’t work. Is there an update somewhere that I missed?
    -bd

  • 20. Martin Rehfeld  |  October 12th, 2008 at 8:56 pm

    @baddog/#19: I think you spoke a bit too soon. As Alex and Phillip already pointed out in comment #15 and #18, the newly introduced Rails default of ActiveRecord::Base.include_root_in_json broke the original code as the JSON gets nested one level deeper than the TreeLoader can work with. I have just updated the above model code as Alex suggested, so we are good to go even with Rails 2.1’s defaults now.
    Regarding a potential deprecation of add_child in acts_as_nested set: That’s incorrect information - the whole act_as_nested_set has been moved out of Rails core into a separate plugin, which might be the reason why certain API references list that method as deprecated.

    HTH,
    Martin

Leave a Comment

Required

Required, hidden

Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>

Trackback this post  |  Subscribe to the comments via RSS Feed


Most Recent Posts