e x p r e s s i c a

ruby on rails, business and technicalities

No public Twitter messages.

  • RSS
  • Facebook
  • Twitter
  • Linkedin

Ajaxified Drag Drop Tree in RoR
CASE STUDY

I m providing a very generalized use case where the tree fits in a good position. Here it is…

Consider a model Item, a controller Items. Item model is using a fabulous acts_as_tree and we are going to put a seed for Item to grow it in an ajax tree ;-) … Ok no more non-code talk. So, lets start the code now…

==========================================================
I have also incorporated the code into a sample application which you can directly check out and try the tree yourself if you find it a
headache to add the following code in a number of described files.
So, here is the Sample Tree Application

or you can try to code yourself as…

Create a sample rails application say treeapp by running

rails treeapp

from the command prompt.
Now simply change your directiry into just created treeapp and make sure that you are in the directory treeapp

Now configure the database settings for this application by modifying the file /config/database.yml as …

development:
  adapter: mysql
  database: tree_dev
  username: root
  password: root
  host: localhost

Here it simply shows that you have a mysql database named tree_dev and a user root with password root can access this database. So make sure about these settings.

From the command prompt in application root(i.e. you are in the directory treeapp) run this command to generate the model Item

treeapp> ruby script/generate model item

Add the following code to the file app/models/itme.rb

class Item < ActiveRecord::Base

  acts_as_tree
  validates_presence_of :name
  attr_accessor :style

  def self.roots
    self.find(:all, :conditions=>["parent_id = ?", 0])
  end

  def level
    self.ancestors.size
  end
end

This simply shows that you should have a table named tems in your database…
so why we havnt mentioned it earlier ?
Thats the thing which will make you feel an agile web development.
Now look at the directory db/migrateand a you will find a file named as db/migrate/001_create_items.rb
Add the following code to this file 001_create_items.rb
Here we are creating our database table and also adding some initial data to work with.

class CreateItems < ActiveRecord::Migration
  def self.up
      create_table "items", :force => true do |t|
         t.column "name", :string
         t.column "created_at", :datetime
         t.column "parent_id", :integer, :default => 0, :null => false
      end
      %w(item1 item2 item3 item4 item5).each do |name|
            parent = Item.new(:name=>name)
            parent.save
            Item.create(:name=>name+".1", :parent_id=>parent.id)
            Item.create(:name=>name+".2", :parent_id=>parent.id)
            Item.create(:name=>name+".3", :parent_id=>parent.id)
       end
 end

 def self.down
   drop_table :items
 end

end

Now from the command line from the root of your application run the following command to have a table named Item in your database with some initial data.

  treeapp> rake db:migrate

Before we start handling our views and controller part just have a smart small image named as drag.gif in your public/images directory that we will use as a handle to drag the nodes. So, now you can see a small image at public/images/drag.gif, cool !.
Now from the command line from the root of your application run the following command to create a controller …

treeapp> ruby script/generate controller items show

Make sure that now you have the files app/controllers/items_controller.rb and app/views/items/show.rhtml.
Add the following code in the file app/controllers/items_controller.rb

class ItemsController < ApplicationController

  def show
    @items = Item.find(:all)
    @item = Item.find(:first)
    # select according to your choice...
    #this item will be selected node by default in the tree when it will first be loaded.
  end

  def display_clicked_item
    # this action will handle the two way syncronization...all the tree nodes(items) will be linked
    # to this action to show the detailed item on the left of the tree when the item is clicked
    # from the tree
    if request.xhr?
      @item = Item.find(params[:id]) rescue nil
      if @item
        # the code below will render all your RJS code inline and
        # u need not to have any .rjs file, isnt this interesting
        render :update do |page|
          page.hide "selected_item"
          page.replace_html "selected_item", :partial=>"items/item", :o bject=>@item
          page.visual_effect 'toggle_appear', "selected_item"
        end
      else
        return render :nothing => true
      end
    end
  end

  def sort_ajax_tree
    if request.xhr?
      if @item = Item.find(params[:id].split("_").first) rescue nil
        parent_item = Item.find(params[:parent_id])
        render :update do |page|
          @item.parent_id = parent_item.id
          @item.save
          @items=Item.find(:all)
          page.replace_html "ajaxtree", :partial=>"items/ajax_tree", :o bject=>[@item,@items]
          page.hide "selected_item"
          page.replace_html "selected_item", :partial=>"items/item", :o bject=>@item
          page.visual_effect 'toggle_appear', "selected_item"
        end
      else
        return render :nothing => true
      end
    end
  end

end

Add the following code in the file app/views/items/show.rhtml

<h2>Ajax Tree Application</h2>

<div id="ajaxtree" style="width:40%;float:left;">
 <%= render :partial=>'items/ajax_tree', :o bject=>[@item,@items] %>
</div>

<div id="selected_item">
 <%= render :partial=>'items/item', :o bject=>@item %>
</div>

Add the following code in the file app/views/items/_item.rhtml

<% if @item %>
  <h2>Selected Item is <%=h @item.name%> </h2>
<% else %>
  Item not found
<% end %>

Add the following code in the file app/views/items/_ajax_tree.rhtml

<script type="text/javascript">

function toggleDiv()
{
  Element.toggle('mytree');
  Element.toggle('expanded');
  Element.toggle('collapsed');
  return false;
}
function showDrag()
{
  var drag_images = $$('img.drag_image');
  drag_images.all(function(value,index){return value.style.display='inline';});
  Element.toggle('done');
  Element.toggle('reorder');
  return false;
}
function hideDrag()
{
  var drag_images = $$('img.drag_image');
  drag_images.all(function(value,index){return value.style.display='none';});
  Element.toggle('done');
  Element.toggle('reorder');
  return false;
}
</script>

<style>

.mytree{padding:0 0 0 0px;}

.mytree li {padding:2 0 0 3px;}

.outer_tree_element{margin:0 0 0 10px;}

.inner_tree_element{margin:5px 0 0 10px;}

.mytree a{text-decoration:none; font-size:13px; color:black;}

.mytree a:hover{background-color:lightblue;}

.mytree label{font-weight:normal;}

.highlighted{background-color:lightblue;}

.normal{background-color:white;}

.drag_image{border:0px;}

</style>

<div id="mytree" class="mytree">

  <% @ancestors = @item.ancestors.collect{|parent| parent.id} if @item.has_parent? %>
  <% @items = Item.find(:all) %>
  <%= get_tree_data(@items, 0){|n|
      link_to_remote(n.name,
      :url=>{:controller=>'items', :action=>'display_clicked_item', :id=>n.id},
  :loading=>"Element.show('tree_indicator')",
  :complete=>"Element.hide('tree_indicator')"
  )}
  %>

  <% @items.each do |node| %>
  <%= draggable_element node.id.to_s+'_tree_div',:revert=>true,:snap=>false, :handle=>"'#{node.id.to_s}_drag_image'" %>
  <%= drop_receiving_element node.id.to_s+'_tree_div',
      :accept=>'inner_tree_element',
  :url=>{:controller=>'items',:action=>'sort_ajax_tree', :parent_id=>node.id,:id=>nil},
  :loading=>"Element.show('sort_tree_indicator')",
  :complete=>"Element.hide('sort_tree_indicator')"
  %>

  <% end %>

  <%= image_tag 'indicator.gif', :id=>'tree_indicator', :style=>'display:none' %>
  <%= image_tag 'indicator.gif', :id=>'sort_tree_indicator', :style=>'display:none' %>
</div>

<script type="text/javascript">

  var selected_el = document.getElementById('<%=@item.id%>_tree_item');
  selected_el.className='highlighted';

  function toggleMyTree(id)
  {
  Element.toggle(id+'collapsed');
  Element.toggle(id+'expanded');
  Element.toggle(id+'children');
  return false;
  }
  function toggleBackground(el)
  {
  // using collection proxies to change the background
  var highlighted_el = $$("span.highlighted");
  highlighted_el.all(function(value,index){return value.className='normal'});

  el.className='highlighted';
  selected_el = el;
  return false;
  }
  function openMyTree(id)
  {
  Element.hide(id+'collapsed');
  Element.show(id+'expanded');
  Element.show(id+'children');
  return false;
  }

</script>

As you can see in the above file we have used some indicator and toggle images. So you will be required to have three more images in the directory public/images/.
Here is the small description about these images…

  • An indicator image that will be displayed at the bottom of the tree whenever a tree node is clicked. You can select from a number of Ajax Inidicatorsavailable on the web. Select one indicator image and save in your app with the name indicator.gif. So, now make sure that you can see the image at public/images/indicator.gif
  • Second, we need to have a small image with + sign. That will be used to toggle the tree. save it as public/images/collapsed.gif
  • Similarly, an image with - sign. Save it as public/images/expanded.gif

We have to include the prototype and scriptaculous javascript libraries in the application.
So just manually create a layout file app/views/layouts/application.rhtml and add the following code in the file application.rhtml

<html>
  <head>
    <%= javascript_include_tag :defaults %>
  </head>

  <body>
    <%= @content_for_layout %>
  </body>

</html>

Now the last but the most importatnt…The recursion to obtain the tree.
Add the following code in the file app/helpers/application_helper.rb

module ApplicationHelper

  def get_tree_data(tree, parent_id)
    ret = "<div class='outer_tree_element' >"
    tree.each do |node|
      if node.parent_id == parent_id
        node.style = (@ancestors and @ancestors.include?(node.id))? 'display:inline' : 'display:none'
        display_expanded = (@ancestors and @ancestors.include?(node.id))? 'inline' : 'none'
        display_collapsed = (@ancestors and @ancestors.include?(node.id))? 'none' : 'inline'
        ret += "<div class='inner_tree_element' id='#{node.id}_tree_div'>"
        if node.has_children?
          ret += "<img id='#{node.id.to_s}expanded' src='/images/expanded.gif' onclick='javascript: return toggleMyTree(\"#{node.id}\"); ' style='display:#{display_expanded}; cursor:pointer;'  />  "
          ret += "<img style='display:#{display_collapsed}; cursor:pointer;'  id='#{node.id.to_s}collapsed' src='/images/collapsed.gif' onclick='javascript: return toggleMyTree(\"#{node.id.to_s}\"); '  />  "
        end

        ret += " <img src='/images/drag.gif' style='cursor:move' id='#{node.id}_drag_image' align='absmiddle' class='drag_image' /> "

        ret += "<span id='#{node.id}_tree_item'>"
        ret += yield node
        ret += "</span>"
        ret += "<span id='#{node.id}children' style='#{node.style}' >"
        ret += get_tree_data(node.children, node.id){|n| yield n}
        ret += "</span>"
        ret += "</div>"
      end
    end
    ret += "</div>"
    return ret
  end
end

Now you can check the tree functionality at http://localhost:3000/items/show.. assuming that you are running your server on port 3000. njoy!!

26 Responses so far.

  1. [...] This tree works very fine in my application and hope it will help u also. Check out the Source Code of the tree. [...]

  2. SUR says:

    Hi Source Required !!
    Check out the Source Code

  3. Rana says:

    Hello
    Thanks for the code
    although i am still having problems adding it to my project

    Showing app/views/items/_ajax_tree.rhtml where line #70 raised:

    can’t convert Array into String

    70:

  4. Sur Max says:

    Hi Rana !!
    I am figuring it out where the problem is exactly by trying it in a new test application. I will post the corrected one soon.

  5. eastviking says:

    can’t convert Array into String

    Yes,I got the error too.

    and:

    Add the following code in the file app/views/items/_item.rhtml

    Selected Item is

    Item not found

    should be:
    Add the following code in the file app/views/items/_item.rhtml

    Selected Item is

    Item not found

    Add the following code in the file app/views/items/show.rhtml

    Ajax Tree Application

    \’items/ajaxtree\’, :o bject=>[@item,@items] %>

    should be:
    Add the following code in the file app/views/items/show.rhtml

    Ajax Tree Application

    \’items/ajax_tree\’, :o bject=>[@item,@items] %>

    _ajax_tree.rhtml

    {:controller=>\’items\’,:action=>\’display_clicked_item\’,:id=>n.id}
    :loading=>\”Element.show(\’tree_indicator\’)\”,
    :complete=>\”Element.hide(\’tree_indicator\’)\”,
    }
    should be:

    {:controller=>\’items\’,:action=>\’display_clicked_item\’,:id=>n.id},
    :loading=>\”Element.show(\’tree_indicator\’)\”,
    :complete=>\”Element.hide(\’tree_indicator\’)\”
    }

  6. Alex says:

    Hello
    Thanks for the code
    but i am also have

    can’t convert Array into String

    Showing app/views/items/_ajax_tree.rhtml where line #70

    in just created, clear project

  7. Sur Max says:

    Hello everyone !!
    I am correcting the code and will upload it by tomorrow and will post a comment thereby.

  8. Sur Max says:

    Hello Everyone !!
    Sorry for the delay…
    Hi Alex, Eastviking, Rana, Eric…
    I was through with the code this weekend and i found some of my stupid mistakes, sorry for that… anwaz
    I have uploaded the modified corrected code. I have also tested it in a fresh newly created application and it is working fine.
    Thanks.

  9. schmii says:

    Programmers inhumanity to man
    Its amazing how such a simple code fragment has spanned 3-months to rectify the sample code.

    Could you please direct me to the latest source code.
    thanks
    schmii

  10. Sur Max says:

    Hi schmii !!
    sorry to say but i am disappointed by ur invalid perception.
    The code was running fine before November, it was broken after it when i make it a bit generalized… so my maths says that it has taken around 15 days not 3 months and that too coz i was busy in my commercial projects.
    Anyways.. the published code in this post is now working.

  11. [...] My friend sur wrote and shares his code for Ajax based drag drop and sortable tree for rails. He is also trying to pluginize this, and soon it will be publicly available. Find more detail here. [...]

  12. schmii says:

    Hi Sur thank you for sending me the zip files for ‘testapp’. I followed your instructions and it works great.
    I’m new to ruby and rails and I’m attempting to develop my first application. Your sample code has given me a working example from which I can apply to my application.
    thanks
    schmii

  13. [...] I have provided the source code of the ajax based drag drop tree in rubyonrails in one of my previous posts. I found some of the people are getting problems to incorporate the code into their running applications so i am providing a sample rails application in which all the code for tree is already been placed well. [...]

  14. Gaurav says:

    Cool,
    Nice code.
    Now all I need is an web application to make use of this code.

  15. [...] My friend sur wrote and shares his code for Ajax based drag drop sortable tree for rails. He is also trying to pluginize this, and soon it will be publicly available. Find more detail here. [...]

  16. andy says:

    hello!
    your code looks very interesting! i would greatly appreciate the sample poject to play with. thank you for sharing your hard work with all of us!

    much appreciated,
    cheers,
    andy

  17. TimN says:

    Sur,

    Great project you are working on. I’ve implemented your code in the way you describe, but am still running into an RJS error. When I click on a parent group on the ‘show’ page, my browser shows a javascript error:

    RJS Error:
    TypeError: Effect.toggle is not a function

    This happens to me on the Mac (Safari & Firefox) as well as on a PC (IE).

    I am fairly certain I implemented your code correctly (I did it twice, just to be sure and named all files and DB table the same as your example). Any ideas what I may be doing wrong?

    Thanks,
    Tim

  18. Sur Max says:

    Hi TimN !!

    Well, before i figure out if there is any problem, could you try the Sample Application in which you need not to code a single line but just need to follow 4 steps described Here.
    I will look forward if the problem still persists, let me know in any case whether or not the application is running fine.

    Thanks.

  19. TimN says:

    Sur,

    I created my own “items” table with the fields you had in your schema, but other than that, I did follow the steps you described… I think ;-)

    Do you know of a publicly available URL where your example app is running so that I could check it out?

    Best,
    Tim

  20. Kunjan says:

    How do I display the tree upto 2 or more Levels? Currently it is being displayed till only 1 level..

  21. Sur Max says:

    Hi Kunjan !!
    Drag any element and drop it onto an element of second level, and the dropped element will become child and become a third level element.
    How you need not to explicitly specify any level for nodes, but just add any element having parent_id as the id of second level… third level… and so on.

  22. StevenG says:

    Thanks for submitting this code. I have been looking at Javascript versions, but yours is much simpler and RoR native! It seemed the toggleBackground function was never called, so the selected item would never highlight. There may be a better way, but it can be fixed by adding

    ; toggleBackground($('#{n.id}_tree_item'));

    to the loading or complete portion of

    {...},
    :loading=>"Element.show('tree_indicator')",
    :complete=>"Element.hide('tree_indicator'); "
    )}
    %>

    in _ajax_tree.rhtml. In other words, you want

    :loading=>"Element.show('tree_indicator');
    toggleBackground($('#{n.id}_tree_item'));",

  23. Sur Max says:

    Thanks Steven,
    I guess i have missed that while extracting it from my application.
    I will add it now.

    Thanks.


Sponsors