On the previous post I wrote a few lines on the basic usage of the accepts_nested_attributes_for method in rails models. I strongly recommend reading that post before this one if you haven’t.
Although there is a standard way for deleting items, there is few information on how to deal with them on the model or the controllers without messing with the unprocessed parameters. I’ll try to go over some of the different ways for deleting (or rejecting) children.
Do not create at all
Before actually deleting, it is important to consider the case in which you don't want to create the child in the first place. Doing this is easy and handled completely on the model: there is a very useful reject_if option that allows you to specify conditions under which you do not want to process a specific child.
accepts_nested_attributes_for :children :reject_if => proc { |att| att['name'].blank? }
The procedure runs for every child, if it evaluates to true, that set of parameters is ignored. Note that this is not the same as a validation: the child will be ignored simply rejected, and will not raise a validation error (children validation and parental control will be left for another post).
Remember that here you must predicate on the raw attributes. The class is not constructed (unless you manually do it in the procedure) so you do not have access to any model fields or instance methods.
This method is useful when you display a text field for the user to complete, which represents the child. If the user does not want to fill that field (name in the example), the child will not be created.
Suppose you have 5 empty text fields for the user to fill his/her hobbies. Should a user have only 3 hobbies, then you want to create only 3 instances of the Hobby class that belong_to that user.
Check _delete
The recommended option for deleting an item is setting a _delete parameter to true in the attributes. This is, if you have a set of parameters like the following:
children_attributes => 1 => { id => 16, :name => Jack, :_delete => true } 2 => { id => 18, :name => Mary }
Then Jack will be removed from the association to his parent. If the association has :dependent => :destroy set, Jack will be completely destroyed.
It is critical not to forget adding the allow_destroy option to the nested attributes method:
accepts_nested_attributes_for :children, :reject_if => proc { |att| att['name'].blank? }, :allow_destroy => true
The rationale behind this feature is that if the user wants to delete an item, he/she must simply check a box named _delete and the controller will forward the parameter to the model, which will remove the child.
If you want to use a different method for deleting (in the hobbies example, delete when the user clears the textbox) you can use javascript to toggle a hidden checkbox whenever a textbox changes, for example. But this is clearly delegating model logic to the view, so a different approach is needed.
Let the controller do it
Since we are messing with the raw params representation of the object, the controller could simply iterate over each of the children_attributes and add a _delete parameter whenever needed, if a certain condition occurs.
However, if we had rejected the previous javascript-based solution for deleting a child since it implies keeping model logic in the view, why should we be happy by moving it to the controller? We have to move another step further.
Mark for destruction
No, we will not hack the attributes= method to manually walk the children_attributes params and add the deletion flag. Since we are in the model, we will use the objects themselves.
The AutosaveAssociation class has a handy method for marking objects for removal which allows you to flag certain objects that should be destroyed when the parent is saved.
Therefore, you can add a callback before the parent is saved that walks through the children and marks for removal those who match a certain condition. And this time, you have the actual children, not some raw-params representation:
before_save :mark_children_for_removal def mark_children_for_removal children.each do |child| child.mark_for_destruction if child.name.blank? end end
This allows you to keep all your model logic in your model, where it belongs, and avoid messing with nested parameters in the view or the controller.