Moving Between Lists With actsaslist
How do you move an item managed by
actsaslist 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.
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
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
actsaslist adds to your model update records in the database directly, triggering callbacks you don’t necessarily want triggered.
So instead of using
aftersave 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
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
actsaslist 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
By its nature,
actsaslist 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.