Using 'acts_as_list' in Ruby on Rails
** UPDATE - The two sections at the end about :position and move_higher and move_lower have been updated after further research **
** UPDATE - Checked this with Rails 1.2 and the bugs still exist **
One very nice feature of rails is the easy way it can handle parent/child relationships where the children need to be treated as an ordered list, which it does through the acts_as_list interface. This is explained a bit in the otherwise excellent rails book but not enough to actually use it.
So just to recap, if you have a table of parent objects and a table of children objects and you want to represent the children as an ordered list, you add an ‘acts_as_list’ statement to you child model, specifying the parent scope:
class Parent < ActiveRecord::Base has_many :children end class Child < ActiveRecord::Base belongs_to :parent acts_as_list :scope => :parent # could have used :parent_id end
Adding records
So the first question that comes to mind is how do I add a record to the list. A quick look at the documentation reveals a method called insert_at but no instructions on how to use it. Well it turns out to be pretty simple. All you do is create a new child record and save it, which automatically adds it to the end of the list. Then call insert_at on it, specifying the new position. For example (though I’m sure this can be simplified further):
@child = Child.new(params[:child]) @child.parent_id = @parent.id @child.save @child.insert_at(params[:new_position])
Retrieving the ordered list
Having done this I soon discovered that I was not getting the list in the order that I was setting the children, but rather the order in which I had added them. After much testing I realised the obvious missing code. The declaration of the parent model given in the book is incomplete. It should actually be:
class Parent < ActiveRecord::Base has_many :children, :order => :position end
Which then ensures that the generated SQL contains an ‘order by’ clause to return the list in the order of the position, which is the proper order for the list.
Starting index
When the records are added to the database by ActiveRecord the :position column starts at 1 and goes up from there. The book claims that the starting index for the list is 0, but there are a number of bugs with this. If you create the parent object then access it via the array interface child.parent[0] then it does work as the book says. However ‘move_higher’ and ‘move_lower’ don’t work correctly, as explained below.
The other confusing bit is when you use ‘insert_at’, where the starting index is actually the position you add the first record at. So if you add a record and then move it to position 1 using the ‘insert_at’ then ActiveRecord does not complain and from that point on your list has a starting index of 1 not 0. Though in practice you don’t need to do that as the default for ‘insert_at’ is position = 1. Interestingly you can even insert a record at position -1 and it all works as expected.
move_lower and move_higher
The method move_lower is used to move a child to a lower position in the list, in other words from position 4 to position 3. The counterpart is move_higher which is used to move a child the other way.
To get move_lower to work you need to substract two from the position. So if you want to move the child at position 4 to 3 then do
child[2].move_lower
and if you want to move the child at position 2 to 1 then do
child[0].move_lower
To get move_higher to work you need to add one to the position. (Don’t forget the array index is one less than the position). So if you want to move the child at position 3 to 4 then do
child[3].move_higher
and if you want to move the child at position 1 to 2 then do
child[1].move_higher


(10 votes, average: 3.8 out of 5)
February 27th, 2007 at 12:12 am
Thankyou!
I have been trying to work out why my lists were not being displayed in the rights order. I ended up coding search code to return them in the right order, despite having the list right there.
The pickaxe is great, but it glosses over too many things, and unfortunatly the ruby/rails online docs are not always that easy to find how to do things in. They are good references, but sometimes you need a recipe.
March 15th, 2008 at 5:39 pm
A little over a year later I am trying to use list, using a transaction so I can update the parent and child at the same time. Which works, but it seems to edit a child you have to destroy the old child and append
September 18th, 2008 at 9:57 pm
I’m leary of acts_as_list in a multi user app on postgres. I implemented a list like this recently where you make room for the item you are moving by incrementing higher order items ar the target and decrementing items to remove gaps. There were deadlocks when someone moved an item above an item being moved below the item in the list. This can be worked around by first selecting for update all of the rows you are incrementing or decrementing, but I don’t see any sign of a ’select for update…’ in the acts_as_list code or in the AR functions it uses.
I’m curious if anyone has seen a deadlock with acts_as_list.
September 23rd, 2008 at 2:07 am
Note that the following commands are equivalent:
* child[2].move_lower #from position 3 to 4
* child[3].move_higher #from position 4 to 3
Higher is toward the top of the list, as in index 0.
So if you want to move the child at position 4 to 3, then do:
* child[3].move_higher # index 3 is object 4, move higher reduces the position
I was scratching my head for a bit as to why it would be so odd to have to do funny math to make it work!
Cheers!