Moving Between Lists With acts_as_list
How do you move an item managed by acts_as_list from one parent to another? Moving items within a list is well documented, but there’s scant information on moving between lists.
I wrote about this previously in a pre-Rails 2 world. This is an update for Rails 2 and — happy days — it’s simpler.
Intra-List Movement
Here’s the basic set-up. This is all you need for intra-list movement.
class Category
has_many :products, :order => :position
end
class Product
belongs_to :category
acts_as_list :scope => :category
end
Inter-List Movement
When we move a product between categories, we need first to remove it from its current category’s list; and afterwards to insert it at the correct position in its new category’s list.
This is slightly tricky because the instance methods acts_as_list adds to your model update records in the database directly, triggering callbacks you don’t necessarily want triggered.
So instead of using before_save and after_save callbacks to tinker with the old and new categories’ lists, you need to get involved at the point where you change the product’s category.
class Product
belongs_to :category
acts_as_list :scope => :category
def category_id=(category_id)
p = position
remove_from_list if (p && valid?)
super
insert_at position_in_bounds(p) if (p && valid?)
end
private
def position_in_bounds(pos)
length = category.products.length
length += 1 unless category.products.include? self
if pos < 1
1
elsif pos > length
length
else
pos
end
end
end
The Controller
Assuming your GUI for editing a product has:
- a dropdown list of categories to which the product can belong;
- a dropdown list of positions within its category’s list;
- AJAX that refreshes the positions when a different category is chosen;
— then your controller would look like this:
class ProductsController
def update
@product = Product.find params[:id]
new_position = params[:product].delete(:position).to_i
if @product.update_attributes params[:product]
@product.move_to_position new_position
redirect_to product_url(@product)
else
@product.position = new_position
render :action => 'edit'
end
end
end
The only difference from a normal update is the special handling of position. Why do we treat position differently? Because acts_as_list executes SQL updates directly on the database, independently of our model’s updates. We only want this under-the-covers SQL update to take place if the model’s own update goes through successfully.
For this to work, we need to define one further method on our model.
class Product
def move_to_position(position)
insert_at position_in_bounds(position)
end
end
Conclusion
By its nature, acts_as_list needs to update multiple records when you change a model’s position. This update saves your model, which may come at an inconvenient time for you. It’s easier, therefore, to avoid the usual lifecycle callbacks and instead operate at the point where the parent is changed.
Posted in Rails

0 Comments
Jump to comment form