Rails / Trees
From WhyNotWiki
Rails / Trees edit (Category edit)
[edit] [Model-level (category)]
[edit] Comparison: Features and best-suitedness
| Support for ordered trees? | Efficient to query for all descendants of a node? | Comments / Overall impression | |
|---|---|---|---|
| ActsAsTree | No! | No. You can pass :order => :position to read in a certain order, but doesn't help with changing/setting order/position. |
|
| ActsAsTree + ActsAsList | Somewhat. | ||
| ActsAsOrderedTree | ? | ||
| ActsAsNestedSet | Yes! | Yes! | |
| BetterNestedSet | Yes! | Yes! |
[edit] Tests/Demonstrations!
See the tests I've started to write, which not only test for expected behavior and compatibility between different configurations/plugins, but also demonstrates somewhat how to use these plugins:
http://svn.tylerrick.com/public/rails/examples/trees/ordered_trees/test/acts_as_list_test.rb
I admit, it could be more extensive, ... but it's a start.
[edit] Comparison: Columns required/used'
| ActsAsTree | ActsAsList | BetterNestedSet | |
|---|---|---|---|
parent_id |
✓ | - | ✓ |
position |
- | ✓ | - |
lft/rgt |
- | - | ✓ |
[edit] Comparison: How to...
| ActsAsTree | ActsAsTree + ActsAsList | BetterNestedSet | ||
|---|---|---|---|---|
| Move node to different parent: | n1.parent = n2; n1.save |
n1.parent = n2; n1.save? |
n1.move_to_child_of n2 |
|
| Create a new node n2 a child of n1: |
n1.children.create |
same |
n2 = Node.create n2.move_to_child_of n1 |
|
[edit]
ActsAsTree
(acts_as_tree)
| Categories/Tags: | [Trees (category)] [acts_as plugins (category)] |
|---|---|
| Documentation: | API Wiki |
| Source code: | (Core Rails)
|
[edit] Examples
http://api.rubyonrails.org/classes/ActiveRecord/Acts/Tree/ClassMethods.html
root = Category.create("name" => "root")
child1 = root.children.create("name" => "child1")
subchild1 = child1.children.create("name" => "subchild1")
root.parent # => nil
child1.parent # => root
root.children # => [child1]
root.children.first.children.first # => subchild1
http://wiki.rubyonrails.org/rails/pages/ActsAsTree
top = TreeItem.find_by_parent_id(nil) top.children.each do |child| puts child.parent.name + " is my parent. I am " + child.name end
[edit] Recursive find / Descendants / Children and children's children
http://wiki.rubyonrails.com/rails/pages/QuestionOfTheDay
[Paraphrase (category)]
Let's say you have a category model that acts_as_tree so that you can have a nice tree of categories. Let's also say that our category has_many products. So a category can have many categories and/or products.Given these two relationships, is there an easy way to get the products from a category AND the products from all its children and their children?
So far this is what I am doing, just wondering if there is a better way:
products = [] products += @category.products.find_all @category.children.each {|child| products += child.products.find_all}The problem with this is that it only searches one level of children, not children of children recursively.
Proposed better way:
category_ids = @category.children.collect{|x| x.id} << @category.id products = Product.find :all, :conditions => ['category_id in (?)', category_ids]The problem with this is that it only searches one level of children, not children of children recursively.
Also check out acts_as_threaded plugin...
acts_as_ordered_tree also tries to solve this, using its descendants method.
http://www.rubyinside.com/19-rails-tricks-most-rails-coders-dont-know-131.html
acts_as_nested_set - Almost everyone is familiar with [acts_as_tree, but acts_as_nested_set snuck into Rails quietly. It's much like acts_as_tree, but with the added benefit that you can select all of the children (and their own descendants) of a node with a single query. A list of the instance methods is available.
[edit] Test coverage
./test/fixtures/mixin.rb
class TreeMixin < Mixin
acts_as_tree :foreign_key => "parent_id", :order => "id"
end
class TreeMixinWithoutOrder < Mixin
acts_as_tree :foreign_key => "parent_id"
end
class RecursivelyCascadedTreeMixin < Mixin
acts_as_tree :foreign_key => "parent_id"
has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id
end
test/fixtures/mixins.yml
tree_1: id: 1001 type: TreeMixin parent_id: tree_2: id: 1002 type: TreeMixin parent_id: 1001 tree_3: id: 1003 type: TreeMixin parent_id: 1002 tree_4: id: 1004 type: TreeMixin parent_id: 1001 tree2_1: id: 1005 type: TreeMixin parent_id: tree3_1: id: 1006 type: TreeMixin parent_id: tree_without_order_1: id: 1101 type: TreeMixinWithoutOrder parent_id: tree_without_order_2: id: 1100 type: TreeMixinWithoutOrder parent_id: recursively_cascaded_tree_1: id: 5005 type: RecursivelyCascadedTreeMixin parent_id: recursively_cascaded_tree_2: id: 5006 type: RecursivelyCascadedTreeMixin parent_id: 5005 recursively_cascaded_tree_3: id: 5007 type: RecursivelyCascadedTreeMixin parent_id: 5006 recursively_cascaded_tree_4: id: 5008 type: RecursivelyCascadedTreeMixin parent_id: 5007
./test/associations/cascaded_eager_loading_test.rb
def test_eager_association_loading_with_acts_as_tree
roots = TreeMixin.find(:all, :include=>"children", :conditions=>"mixins.parent_id IS NULL", :order=>"mixins.id")
assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], roots
assert_no_queries do
assert_equal 2, roots[0].children.size
assert_equal 0, roots[1].children.size
assert_equal 0, roots[2].children.size
end
end
def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
root_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:children=>{:children=>:children}}, :order => 'mixins.id')
assert_equal mixins(:recursively_cascaded_tree_4), assert_no_queries { root_node.children.first.children.first.children.first }
end
def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
root_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:first_child=>{:first_child=>:first_child}}, :order => 'mixins.id')
assert_equal mixins(:recursively_cascaded_tree_4), assert_no_queries { root_node.first_child.first_child.first_child }
end
def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:parent=>{:parent=>:parent}}, :order => 'mixins.id DESC')
assert_equal mixins(:recursively_cascaded_tree_1), assert_no_queries { leaf_node.parent.parent.parent }
end
[edit]
ActsAsTree + ActsAsList
acts_as_tree :order => :position acts_as_list :scope => :parent_id
[edit] ↓ ActsAsOrderedTree
(acts_as_ordered_tree)
| Categories/Tags: | [Trees (category)] [acts_as plugins (category)] |
|---|---|
| Homepage: | http://ordered-tree.rubyforge.org/ |
| Documentation: | http://ordered-tree.rubyforge.org/ There is a drag-n-drop demo (download): svn://rubyforge.org/var/svn/ordered-tree/demo |
| Source code: | svn://rubyforge.org/var/svn/ordered-tree/acts_as_ordered_tree
|
| As listed in other directories: | http://agilewebdevelopment.com/plugins/acts_as_ordered_tree |
| Description: | Adds list capability to the standard acts_as_tree, as well as various movements within the tree.
|
http://ordered-tree.rubyforge.org/
+----+-----------+----------+---------+
node_1 | id | parent_id | position | node |
\_ node_2 +----+-----------+----------+---------+
\_ node_3 | 1 | 0 | 1 | Node_1 |
| \_ node_4 | 2 | 1 | 1 | Node_2 |
| \_ node_5 | 3 | 1 | 2 | Node_3 |
| | \_ node_8 | 4 | 3 | 1 | Node_4 |
| | \_ node_9 | 5 | 3 | 2 | Node_5 |
| \_ node_10 | 6 | 1 | 3 | Node_6 |
| \_ node_11 | 7 | 1 | 4 | Node_7 |
\_ node_6 | 8 | 5 | 1 | Node_8 |
\_ node_7 | 9 | 5 | 2 | Node_9 |
...
Actions Tree Methods List Method
--------------------------------------------------------------------------------------------------
Create
To create a child object at a specific position,
use one of the following:
Person.create(:parent_id => parent.id, :position => 2)
parent.children << Person.new(:position => 3)
parent.children.create(:position => 5)
To create a new 'root', use:
Person.create(:position => 2)
:position will default to the bottom of the parent's list
:parent_id defaults to 0 (Class.roots)
Read
roots (class method) self_and_siblings
root siblings
parent position_in_list
ancestors
children
descendants
Update
shift_to(parent = nil, position = nil) move_above(sibling = nil)
orphan move_higher
orphan_children move_lower
parent_adopts_children move_to_top
[edit] acts_as_tree / acts_as_ordered_tree comparison
| ActsAsTree | ActsAsOrderedTree | |
|---|---|---|
| Location of source: | /usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/acts/tree.rb (89 lines) |
./vendor/plugins/acts_as_ordered_tree/lib/acts_as_ordered_tree.rb (439 lines) |
| Test coverage: | There aren't many tests for ActsAsTree. Those that there are are mixed in among other ActiveRecord tests. | The unique methods are pretty well tested, but the basic stuff, the stuff that is common with ActsAsTree, is not well tested at all. |
This test:
def test_root
me = Person.create(:name => "Me")
assert_equal nil, me.parent_id # Fails for acts_as_ordered_tree (is 0) because their schema makes it a non-null, default 0 column
end
|
passes | fails |
This test:
def test_children_without_saving
Person.delete_all
dad = Person.new(:name => "Dad")
dad.children << me = Person.new(:name => "Me")
assert_equal 1, dad.children.size # Fails when using acts_as_ordered_tree! Because it tries to do a reload from the database, but in this case, the model only exists in memory, so the reload actually causes dad.children to be reset to [].
end
|
passes | fails |
./vendor/plugins/acts_as_ordered_tree/lib/acts_as_ordered_tree.rb:
def children(reload=false)
reload = true if !@children
reload ? child_nodes(true) : @children
end
|
||
Conclusion: I do not recommend this plugin.
[edit] ActsAsNestedSet
(acts_as_nested_set)
(Actually use BetterNestedSet (see below), but they're mostly compatible so what you learn about one should be transferable to the other.)
I had set up an ActiveRecord for a hierarchical list of product categories. Since I wanted to be able to search arbitrarily within a category (i.e., “in this category OR any of its descendants), a nested set was the obvious choice [...].
[edit] Modeling a Threaded Forum with acts_as_nested_set
O'Reilly - Safari Books Online - 9780596527310 - Rails Cookbook (http://safari.oreilly.com/9780596527310/model_a_threaded_forum_with_acts_as_nested_set).

You want to create a simple threaded discussion forum that stores all its posts in a single table. All posts should be visible in a single view, organized by topic thread....
[edit] ActsAsNestedSet + ActsAsList
Using ActsAsList and ActsAsNestedSet (http://wiki.rubyonrails.org/rails/pages/Using+ActsAsList+and+ActsAsNestedSet).
In certain situations, you might want to create a table that has the ordering properties of a nested set, without the overhead involved in keeping a nested set properly ordered. After all, you think, surely it doesn’t matter if the records are ordered correctly in the database, as long as the hierarchy is sound? I can use another column for the position attribute, and save my DBMS some work!Well, almost. This got me into a lot of trouble recently. I had set up an ActiveRecord for a hierarchical list of product categories. Since I wanted to be able to search arbitrarily within a category (i.e., “in this category OR any of its descendents), a nested set was the obvious choice, but I also wanted to give the user control of the order of sub-categories. No problem! I’d just declare the record as both acts_as_list and acts_as_nested_set. Since I wanted the positioning of the sub-categories to be local to their parent, I set the :scope parameter in the acts_as_list declaration to "parent_id".
So that all worked, and I was testing out my system, when I noticed that for certain cases (notably adding sub-sub categories), the left and right limits weren’t getting updated properly in the database. What was going on?
Eventually, I worked out that the index updating was stopping at the parent of the new category. But why? It turns out that acts_as_list and acts_as_nested_set both access a method called scope_condition. Since this was set by the acts_as_list declaration, it was getting picked up by acts_as_nested_set as well, even though that scope condition made no sense as far as the nested set updates were concerned (and turned out to be quite destructive). The Solution
In the end, I did things the hard way: I got rid of the acts_as_list declaration and rewrote some of the instance methods to cope with a nested set implementation:
...
[edit] BetterNestedSet
| Documentation: | . |
|---|---|
| Source code: | http://opensource.symetrie.com/svn/better_nested_set/trunk |
| Project/Development: | http://opensource.symetrie.com/trac/better_nested_set/ |
| As listed in other directories: | http://www.agilewebdevelopment.com/plugins/betternestedset
|
| Based on: | ActsAsNestedSet |
| License: | MIT
|
| Authors: | Jean-Christophe Michel
|
http://opensource.symetrie.com/svn/better_nested_set/trunk/README
This plugin provides an enhanced acts_as_nested_set mixin for ActiveRecord, the object-db mapping layer of the framework RubyOnRails. The original nested set feature seems to be quite old and missed some necessary functionalities.
Provides Nested Set functionality. Nested Set is a smart way to implement an ordered tree, with the added feature that you can select the children and all of their descendants with a single query.
Nested sets are appropriate each time you want either an ordered tree (menus, commercial categories) or an efficient way of querying big trees (threaded posts).
== Small nested set theory reminder
Instead of picturing a leaf node structure with children pointing back to their parent, the best way to imagine how this works is to think of the parent entity surrounding all of its children, and its parent surrounding it, etc. Assuming that they are lined up horizontally, we store the left and right boundries in the database.
Imagine:
root |_ Child 1 |_ Child 1.1 |_ Child 1.2 |_ Child 2 |_ Child 2.1 |_ Child 2.2If my cirlces in circles description didn't make sense, check out this sweet ASCII art:
___________________________________________________________________ | Root | | ____________________________ ____________________________ | | | Child 1 | | Child 2 | | | | __________ _________ | | __________ _________ | | | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | | 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14 | |___________________________| |___________________________| | |___________________________________________________________________|The numbers represent the left and right boundries. The table then might look like this:
id | parent_is | left | right | data 1 | | 1 | 14 | root 2 | 1 | 2 | 7 | Child 1 3 | 2 | 3 | 4 | Child 1.1 4 | 2 | 5 | 6 | Child 1.2 5 | 1 | 8 | 13 | Child 2 6 | 5 | 9 | 10 | Child 2.1 7 | 5 | 11 | 12 | Child 2.2So, to get all children of an entry 'parent', you
SELECT * WHERE left IS BETWEEN parent.left AND parent.rightTo get the count, it's (right - left - 1)/2, etc. To get the direct parent, it falls back to using the parent_id field. There are instance methods for all of these.
[edit] API
Methods names are aligned on Tree's ones as much as possible, to make replacement from one by another easier, except for the creation:
in acts_as_tree:
item.children.create(:name => "child1")in acts_as_nested_set:
# adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1 child = MyClass.new(:name => "child1") child.save # now move the item to its right place child.move_to_child_of my_itemYou can use:
- move_to_child_of
- move_to_right_of
- move_to_left_of
and pass them an id or an object.
Other methods added by this mixin are:
- root - root item of the tree (the one that has a nil parent; should have left_column = 1 too)
- roots - root items, in case of multiple roots (the ones that have a nil parent)
- level - number indicating the level, a root being level 0
- ancestors - array of all parents, with root as first item
- self_and_ancestors - array of all parents and self
- siblings - array of all siblings, that are the items sharing the same parent and level
- self_and_siblings - array of itself and all siblings
- children_count - count of all immediate children
- children - array of all immediate childrens
- all_children - array of all children and nested children
- full_set - array of itself and all children and nested children
These should not be useful, except if you want to write direct SQL:
- left_col_name - name of the left column passed on the declaration line
- right_col_name - name of the right column passed on the declaration line
- parent_col_name - name of the parent column passed on the declaration line
[edit] Dissection: move_to_child_of()
vendor/plugins/better_nested_set/lib/better_nested_set.rb
def move_to_child_of(node)
self.move_to node, :child
end
def move_to(target, position)
...
# compute new left/right for self
if position == :child
if target_left < cur_left
new_left = target_left + 1
new_right = target_left + extent
else
new_left = target_left - extent + 1
new_right = target_left
end
target is the node that you want to become the new parent of self.
Note that self will become the left-most node of target (new_left = target_left + 1).
I would have preferred that it become the right-most node of target (new_right = target_right - 1), or at least to have the option as to which side it should be inserted on, but currently I don't yet have the confidence to make that change.
[edit] Questions
[edit] Has there been any effort to get this included in core Rails?
If it's better than the core ActsAsNestedSet, then this should replace it and become the new ActsAsNestedSet?
[edit] Has anyone documented the exact differences between ActsAsNestedSet and BetterNestedSet?
And if so, have they tracked any changes...? (Or do I have to do that here...?)
For instance, perhaps Core has added all the features that BetterNestedSet has, rendering BetterNestedSet no longer useful...?
[edit] [ActiveRecord / Single table inheritance (category)] and trees
[edit] Bugs
http://dev.rubyonrails.org/ticket/7775
- 7775 (Derived class types aren't kept when using STI for acts_as_tree) - Rails Trac - Trac
When you create a base class with the acts_as_tree association in it, the class type isn't kept with the children.class Shape < ActiveRecord::Base acts_as_tree end class Circle < Shape end circles = Circle.create! circles.children.create! puts circles.class circles.children.each {|c| puts c.class}The first will print 'Circle' and the children will print 'Shape'. In my opinion, when a child is created it should look to it's parent to see what it's type should be. Therefore, all nodes in the tree are of the same type.
[edit] How to migrate a table from ActsAsTree to ActsAsNestedSet
How to migrate a table from ActsAsTree to ActsAsNestedSet edit
[edit] Introduction
Let's assume that we have an acts_as_tree model called MenuItem stored in table menu_items and we want to convert it into acts_as_nested_set (I'm actually using BetterNestedSet in this example).
I don't think this migration is possible (easily) without having two models (and thus two tables) -- one with acts_as_tree that we read from and one with acts_as_nested_set that we insert into. I can't imagine trying to transform the data into a nested set by simply doing updates on a single table. It might be possible, but it would require rewriting a lot of the logic for dealing with these two different data structures -- better just to use the code we already have for reading and manipulating them!
Once we've migrated the data to the new table, we can drop the old table and rename the new table to whatever name the old table had.
[edit] Details
class MenuItem < ActiveRecord::Base
acts_as_list :scope => :parent_id
acts_as_tree :order => :position
def preorder_traverse(prev_return_value = nil, *args, &visitor)
cur_return_value = yield self, prev_return_value, *args
children.each do |item|
item.preorder_traverse(cur_return_value, *args, &visitor)
end
end
def render_as_text
preorder_traverse &render_as_text_proc
end
def render_as_text_proc
lambda { |node, *args|
puts ' '*node.level + "(##{node.id}; #{node.position})" + '. ' +
"#{node.display_name.to_s.gsub(/./,'*')}" +
"#{node.name.to_s}"
}
end
def level(i = 0)
parent ? parent.level(i+1) : i
end
end
class NestedSetMenuItem < ActiveRecord::Base
acts_as_nested_set
def preorder_traverse(prev_return_value = nil, *args, &visitor)
cur_return_value = yield self, prev_return_value, *args
children.each do |item|
item.preorder_traverse(cur_return_value, *args, &visitor)
end
end
def render_as_text
preorder_traverse &render_as_text_proc
end
def render_as_text_proc
lambda { |node, *args|
puts ' '*node.level + "(##{node.id}; #{node.lft}-#{node.rgt})" + '. ' +
"#{node.display_name.to_s.gsub(/./,'*')}" +
"#{node.name.to_s}"
}
end
end
class MigrateTreeToNestedSet < ActiveRecord::Migration
def self.up
# Apparently we can't do anything with the table (like add indexes to a column or insert into the table) until we commit it
# (at least in sqlite3) -- that's why this is in >=2 transactions instead of 1.
MenuItem.transaction do
# Create the new table for BetterNestedSet
create_table "nested_set_menu_items", :force => true do |t|
t.column :type, :text # For single-table inheritance
t.column :name, :text # To uniquely identify a menu item or tree
t.column :display_name, :text
t.column :parent_id, :integer # For BetterNestedSet
t.column :lft, :integer # For BetterNestedSet
t.column :rgt, :integer # For BetterNestedSet
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
MenuItem.transaction do
add_index(:nested_set_menu_items, :parent_id)
add_index(:nested_set_menu_items, :lft)
add_index(:nested_set_menu_items, :rgt)
add_index(:nested_set_menu_items, :name)
end
MenuItem.transaction do
# Migrate the data over to the new table
tree_root = MenuItem.find_by_name('Root')
tree_root.preorder_traverse do |tree_item, nested_set_root|
# Create a new node in the new nested set (neste_set_item) identical to the old node from the tree (tree_item) and
# make it a child of nested_set_root (unless this is the root node).
nested_set_item = NestedSetMenuItem.create!(
:name => tree_item.name,
:display_name => tree_item.display_name
)
if nested_set_root
nested_set_root.add_child(nested_set_item)
puts "Added #{nested_set_item.display_name} to #{nested_set_root.display_name}"
else
puts "Added #{nested_set_item.display_name} as root"
end
nested_set_item
end
# Proof that the trees look the same before and after the migration.
tree_root.render_as_text
puts '------------------'
nested_set_root = NestedSetMenuItem.find_by_name('Root')
nested_set_root.render_as_text
drop_table 'menu_items'
rename_table 'nested_set_menu_items', 'menu_items'
end
end
def self.down
raise IrreversibleMigration
end
end
== MigrateTreeToNestedSet: migrating ==========================================
-- add_index(:nested_set_menu_items, :parent_id)
-> 0.0031s
-- add_index(:nested_set_menu_items, :lft)
-> 0.0014s
-- add_index(:nested_set_menu_items, :rgt)
-> 0.0014s
-- add_index(:nested_set_menu_items, :name)
-> 0.0014s
(#1; 1). **************
(#2; 2). **********************
(#41; 4). *******************
(#20; 4). **************************
(#32; 4). **************
(#37; 4). ***************************
(#42; 4). ****************
(#44; 4). ***********
(#45; 4). ********
(#38; 4). ********************
(#43; 4). ******************
(#33; 4). *********************
(#39; 4). **********
(#40; 4). **********
(#34; 4). ******************
(#24; 4). *****
(#35; 4). ******
(#36; 4). ******
(#3; 2). *****
(#9; 4). ********
(#18; 4). *************
(#19; 4). *********
(#30; 4). ************
(#46; 4). *******
(#47; 4). *********
(#48; 4). ****************
(#31; 4). *************
(#49; 4). *********
(#50; 4). *********
(#51; 4). ************
(#52; 4). **********
(#53; 4). ***********
------------------
(#1; 1-64). **************
(#2; 2-35). **********************
(#3; 3-34). *******************
(#4; 4-27). **************************
(#5; 5-18). **************
(#6; 6-13). ***************************
(#7; 7-8). ****************
(#8; 9-10). ***********
(#9; 11-12). ********
(#10; 14-15). ********************
(#11; 16-17). ******************
(#12; 19-24). *********************
(#13; 20-21). **********
(#14; 22-23). **********
(#15; 25-26). ******************
(#16; 28-33). *****
(#17; 29-30). ******
(#18; 31-32). ******
(#19; 36-41). *****
(#20; 37-40). ********
(#21; 38-39). *************
(#22; 42-63). *********
(#23; 43-50). ************
(#24; 44-45). *******
(#25; 46-47). *********
(#26; 48-49). ****************
(#27; 51-62). *************
(#28; 52-53). *********
(#29; 54-55). *********
(#30; 56-57). ************
(#31; 58-59). **********
(#32; 60-61). ***********
-- drop_table("menu_items")
-> 0.0020s
-- rename_table("nested_set_menu_items", "menu_items")
-> 0.0020s
== MigrateTreeToNestedSet: migrated (2.0795s) =================================
After you run the migration, all you have to do is get rid of the old model and move the new, ActsAsTree-powered model into its place...
svn rm app/models/menu_item.rb svn mv app/models/nested_set_menu_item.rb app/models/menu_item.rb
[edit] Rails / Sortable trees
[edit]
Most of this is a specialization of Rails / Sortable lists.
[edit] Scriptaculous's Sortable's tree:true option
Demo: http://script.aculo.us/playground/test/functional/sortable_tree_test.html
It's really neat! But... I found it a bit hard to use, actually. Too easy to accidentally move a node up or down a level to a different subtree. Having whole (large) subtrees bumping down dynamically to make room for the element I'm dragging is kind of "neat", but quite distracting and confusing, especially since it does this for every single element as you drag over it, which means it's doing it for a lot of elements...
Conclusion: Don't use the tree:true option. I think generally I would prefer to only allow "easy"/"quick" re-ordering (using the Scriptaculous "Sortable") for nodes at the same level. The user interface to move a node to a different subtree can be accomplished with "plain old" Draggables -- only updating the view after the Ajax request (to assign a new parent) has been processed and the server responds with some RJS to update the view.
A bit laggy perhaps, but it's better than having the [skitterish] feeling of having everything constantly moving/jumping around as you're dragging a node. Ideally, it would be updated on the client-side immediately and then maybe highlighted when the Ajax request completes, but I don't (currently) how easy that would be...
[edit] Sur's AjaxTree
Source Code For Ajax Based Drag Drop Navigation Tree in Ruby on Rails (http://ajaxonrails.wordpress.com/2006/08/18/sorce-code-for-ajax-based-drag-drop-navigation-tree-for-rails/) (2006-11-26).
...
http://ajaxonrails.wordpress.com/2006/11/26/ajaxonrailsdragdroptree/ (the source code for the above)
http://svn.tylerrick.com/public/rails/examples/ajaxtree
Comments:
- You can drag and drop a node to a different parent but not to a different position (well, of course, because it doesn't even keep track of position -- it uses acts_as_tree, which is an unordered tree)
- It has +/- icons for each node that let you collapse/expand that subtree
- When you drag and drop, it reloads the entire tree, expanding the subtree that you dropped onto and collapsing all (non-ancestor) trees.
- There is no indication when you are over a droppable element.
- Bug: If you drag onto a collapsed node (say 'item4'), it won't just add it as a child of item4, but will actually add it as a child of the last child of item4, which is item4.3 in the demo.
- By the author's own admission [2], this code has "lagged behind the current trends followed in Rails development" and he was new to Rails when he started it, so it may not use the best conventions...
[edit] Sortable tree addition / discussion on Rails-spinoffs list
http://lists.rubyonrails.org/pipermail/rails-spinoffs/2006-March/002890.html [Rails-spinoffs] sortable tree?
http://wrath.rubyonrails.org/pipermail/rails-spinoffs/2006-February/subject.html#2632 The Rails-spinoffs February 2006 Archive by subject
[edit] sortable tree fix and enhancement for acts_as_nested_set+better_nested_set plugin

http://dev.rubyonrails.org/ticket/7807
- 7807 ([PATCH] Sortable tree fixes for #4691 (adding sub-items to empty branches)) - Rails Trac - Trac
1. #4691 fix: you may add any branch under any node and, thus, create new branch. 2. acts_as_nested_set & better_nested_set plugin integration. onChange provides 2 more arguments: movement type and reference object (drop-on-element). Three movements are supported: "left_to", "right_to" and "to_child_of".http://dev.rubyonrails.org/attachment/ticket/7807/sortable_tree_fix_and_enhancement.2.diff
[edit] [View-level (category)]
[edit] LiveTree
| Homepage: | http://www.epiphyte.ca/code/live_tree.html |
|---|---|
| Documentation: | RDoc
|
| Description: | LiveTree implements a JavaScript/DHTML tree (hierarchical list) widget that can load data asynchronously as-needed (using AJAX). This makes it ideal for cases where the dataset is too large to load to the browser all at once.
|
| License: | MIT
|
| Authors: | Emanuel Borsboom
|
class FamilyController < ApplicationController
live_tree :family_tree, :model => :person
def show_tree
@root = Person.find(params[:id]);
end
end
<div style="width:300px;height:415px">
<%= live_tree(:family_tree, {
:initial_data_root => @root,
:on_click_item => "alert('You clicked on ' + item.name)",
}) %>
</div>
[edit] Features
- Can load data asynchronously as-needed (so only the parts of the tree the user needs are sent to the client).
- Data can be provided in the HTML page as well, in which case no asynchronous loading is needed, making this widget suitable for standalone client-side use.
- Ideal for large/deep data sets (such as navigating a file system, or complete table of contents of a book).
- Intelligently pre-loads parts of the tree that it anticipates the user will want to look at, so delays for the user are minimized.
- Customizable using CSS, snippets of HTML, event handlers, and many options.
- Controllable from JavaScript.
- Supports deep linking - can jump to a part of the tree that has not been loaded yet, and it will search for that item and load its parents.
- Supports an "active" (highlighted) item.
- Automatically scrolls the tree so that what the user is interested in is on the screen.
- Can Use acts_as_tree or acts_as_nested_set model for data from Rails.
- Easy to integrate into your application.
- Can be used standalone (client-side only without server support).
[edit] Miscellaneous
http://wiki.rubyonrails.org/rails/pages/CategoryTreeUsingActsAsTree
def display_categories(categories)
ret = "<ul>"
for category in categories
ret << display_category(category)
end
ret << "</ul>"
end
def display_category(category)
ret = "<li>"
ret << link_to h(category.name), :id => category
ret << display_categories(category.children) if category.children.any?
ret << "</li>"
end
| Licensed under | MIT + |
| Description | [Oops! No type defined for attribute] |
