sur says ajax :on => rails

Thursday, August 17, 2006

Source Code For Ajax Based Drag Drop Navigation Tree For Rails.

Hello Everyone!
for some CSS issues i was not able to publish the source code properly on my wordpress's blog so i am publishing all the code here only. Check this out...

CASE STUDY

I m providing a very generalized use case where the tree fits in a good position. Here it is...
Consider a model MyItem, a controller MyController and a myaction.rhtml as a view. MyItem model is using acts_as_tree and we are going to put a seed for MyItem to grow it in an ajax tree ;-) ... So, lets start the code now...

==========================================

From the command prompt run this command to generate the migration file to create the table in database



and a file name db/migrate/00x_create_my_item.rb will be created where 00x represents the version of your schema info incremented by 1.

Add the following code to this file 00X_create_my_item.rb


class CreateMyItem < force =""> true do |t|
t.column "name", :string
t.column "created_at", :datetime
t.column "parent_id", :integer, :default => 0, :null => false
end
end
def self.down
drop_table :my_items
end
end



As we are finished with the migration file, run this migration to create the
table in databse by this command.


Now create the model for this table by executing the follwing command


and this will create a file app/models/my_item.rb file will be created.

Add the following code to this file my_item.rb


class MyItem < order =""> "created_by"
HIERARCHY_LEVEL = 3
validates_presence_of :name
attr_accessor :style

def self.roots
self.find_all_by_parent_id(0)
end

def level
self.ancestors.size
end
end



Add the following code to the view file app/views/my_controller/myaction.rhtml



<% @myitem = @myitems[0] %>
<!-- here @myitems[0] reflects the first node item that will be selected by default -->
<div id="navtree">
<%= render :partial=>'my_controller/mytree', :object=>[@myitem,@myitems] %>
</div>

<div id="selected_item">
<%= render :partial=>'my_controller/myitem', :object=>@myitem %>
</div>



Add the following code to app/controllers/my_controller.rb




def myaction
@myitems = MyItem.find(:all)
end

def show_selected_item
if request.xhr?
@myitem = MyItem.find(params[:id])
if @myitem
# 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=>"my_controller/myitem", :object=>@myitem
page.visual_effect 'toggle_appear', "selected_item"
end
end
end
end

def sort_my_tree
if request.xhr?
if @myitem = MyItem.find(param[:id].split("_")[0])
parent_myitem = MyItem.find(params[:parent_id])
render :update do |page|
@myitem.parent_id = parent_myitem.id
@myitem.save
@myitems=MyItem.find(:all)
@sort = 'inline'
page.replace_html "navtree", :partial=>"my_controller/mytree", :object=>[@myitem,@myitems,@sort]
end
end
end
end



Add the following code in the file app/views/my_controller/_myitem.rhtml



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



I have include all the style and the javascript methods here in this file only.
You can make CSS and JS files accordingly. JS also includes collection proxies (fundo it is ).
Add the following code in the file app/views/my_controller/_mytree.rhtml



<% @sort ||= 'none' %>
<% reorder_style = (@sort=='none')? 'inline' : 'none' %>
<% done_style = (@sort=='none')? 'none' : 'inline' %>

<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>

<p>
<a href="#" id="reorder" style="display:<%=reorder_style%>" onclick="javascript: return showDrag();">Reorder</a>
<a href="#" id="done" style="display:<%=done_style%>" onclick="javascript: return hideDrag();">Done Reordering</a>
<br/><br/>
</p>

<a id="drop_at_collection" href="/collection" onclick="javascript: return toggleDiv(); ">
<img src="/images/expanded.gif" id='expanded' />
<img src="/images/collapsed.gif" style="display:none" id='collapsed'/>
<b><%= "Displayed Root" %></b>
</a>
<%= drop_receiving_element("drop_at_collection",:accept=>'inner_tree_element',
:url=>{:controller=>'my_controller',:action=>'sort_my_tree',:id=>nil},
:loading=>"Element.show('sort_tree_indicator')",
:complete=>"Element.hide('sort_tree_indicator');"
)%>

<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:2px 0 0 8px;
}

.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
{
/* this class is mandatory for the collection proxies...
u can avoid this class if u need ur tree not to work well :-) */
border:1px;
}
</style>

<div id="mytree" class="mytree">
<% @ancestors = @myitem.ancestors.collect{|parent| parent.id} unless !@myitem.has_parent? %>
<% @myitems = MyItem.find(:all) %>
<%= get_tree_data(@myitems,0){|n|
link_to_remote n.name,
:url=>{:controller=>'my_controller',:action=>'show_selected_item',:id=>n.id}
:loading=>"Element.show('tree_indicator')",
:complete=>"Element.hide('tree_indicator')",
}
<% @myitems.each do |node| %>
<% if node.has_parent? %>
<%= draggable_element node.id.to_s+'_tree_div',:revert=>true, :snap=>false, :handle=>"'#{node.id.to_s}_drag_image'" %>
<% end %>
<% if node.level < MyItem::HIERARCHY_LEVEL %>
<%= drop_receiving_element(node.id.to_s+'_tree_div',:accept=>'inner_tree_element',
:url=>{:controller=>'collection',:action=>'sort_my_tree',:parent_id=>node.id,:id=>nil},
:loading=>"Element.show('sort_tree_indicator')",
:complete=>"Element.hide('sort_tree_indicator');"
)%>
<% end %>
<% end %>

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

</div>


<script type="text/javascript">

var selected_el = document.getElementById('<%=@myitem.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>




Add the following code in app/helper/application_helper.rb



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='display:#{@sort}; 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(tree, node.id){|n| yield n}
ret += "</span>"
ret += "</div>"
end
end
ret += "</div>"
return ret
end


16 Comments:

  • Looks great, will try it soon! thanks!
    What do you think about my idea of implmenting acts_as_tree in ActiveLDAP to have a ruby ldap admin?

    By Anonymous Anonymous, at 1:05 AM  

  • This comment has been removed by a blog administrator.

    By Anonymous Anonymous, at 8:44 AM  

  • Hi Anver,
    I guess ruby's ActiveLDAP library will just work fine for u. I have goy this link for the authorization which might help u in some ways....


    Authorization


    And as far as ur LDAP is concerned i think SilverStripe Tree will work fine, because in that sort of projects i will not suggest you to go for ajaxified application. Try to use ajax as appropriate.

    By Blogger Sur Max, at 8:51 AM  

  • Hi,

    This is a great idea. However there are a number of problems with the sample code - in particular the line which reads:

    ret += sort_tree_data(tree, node.id){|n| yield n}

    should in fact be:

    ret += sort_tree_data(tree, node.id){|n| yield n}

    Are you interested in feedback at this stage?

    By Anonymous Anonymous, at 5:05 AM  

  • Hi ANONYMOUS,
    First of all i must say ... you should leave a name ... ;-)
    yup,
    Any feedback/comment is entertained.
    and i think you have done the same mistake
    its in the code as
    ret += sort_tree_data(tree,node.id){|n| yield n}
    and u have again given as it is ,....
    whether it would probably be
    ret += get_tree_data(tree,node.id){|n| yield n}

    By Blogger Sur Max, at 5:19 AM  

  • Previously was Anonymous...

    Yes, you're right - I posted the correct fix, however this didn't go through. I have it working locally now, and it works very well.

    My suggestions: given some time, you should:

    a. Remove 'my_' as a variable prefix - this is inconsistent with the Ruby idiom - just a style issue.

    b. Supply a working demo - this makes it clear what the behavior should be.

    c. Currently 'mytree' and 'myitem' (not 'my_tree' and 'my_item') are in the controller code.

    d. It would be useful to have some generic expanded/contracted/drag/transparent images as starters for new users.

    e. Finally the whole thing would work very well as its own generate action (like scaffold). Personally I prefer the embedded approach you use over a plug-in, since it allows the tree to be customised extensively. But it can be tricky working out how to cut and paste it into an existing model.

    But these are just minor points, it works very well otherwise. Thank you for your work on this.

    By Anonymous Anonymous, at 7:20 AM  

  • Hi Liam,
    Thanx for this closer review to my code. I will to rectify it asap.
    I am also working to release a plugin for the same with more functionalities and an appropriate doc and i guess major of these points will get covered there. Plugin will be easier to use.

    By Blogger Sur Max, at 12:20 PM  

  • Is there a working demo anywhere?

    By Anonymous Anonymous, at 4:00 PM  

  • Hi johnson !
    I will provide a demo soon on a personalized blog...

    By Blogger Sur Max, at 8:33 PM  

  • Thank you!

    By Anonymous Anonymous, at 4:27 AM  

  • I've gotten this implemented in my own application - THANK YOU.

    Is there any possibility you could make the graphic icons you are using available as well?

    Thank you.

    By Anonymous Anonymous, at 9:50 AM  

  • Hi Scotia King !!
    Get a variety of icons from the FamFamFam Icons

    By Blogger Sur Max, at 9:40 AM  

  • It doesn't work on any of IE 6 or IE 7.

    Reporducing the problem
    1. Drag and drop once in the tree, works fine.
    2. Second time the nodes are not dragable.

    By Blogger Raj, at 5:29 PM  

  • Hi sur,
    Thanks for sharing the tree code.
    I tried it and am experiencing two problems
    1. the window does not scroll while dragging
    2. the tree does not reload after drag and drop. It attempts to load but the tree does not come up.

    Any idea where i am going wrong.
    Thank you.

    By Anonymous Anonymous, at 1:01 PM  

  • This comment has been removed by the author.

    By Blogger AJ, at 5:48 AM  

  • Is working model ready ??
    I am very much interested in tree view, and if this one doesn't works, I would write my own code.
    I just don't want to implement it before seeing how it will look like finally.
    Please provide a working model if you have one.

    By Blogger AJ, at 5:52 AM  

Post a Comment

<< Home