Deleting children with accepts_nested_attributes_for in Rails

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 do not 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.

2 Responses to Deleting children with accepts_nested_attributes_for in Rails

  1. Anatortoise House says:

    Thanks!
    It was hard to find any posts on accepts_nested_attributes_for describe this problem of “not wanting to burden the user with a _destroy checkbox and responsibility in the view” and wanting to handle it in the model. Seems like similar to :reject_if, there could be a :destroy_if flag for accepts_nested_attributes_for.

    So the time you took to post this is appreciated.

  2. Alex says:

    I think passing “_destroy” works too.

Leave a Reply

Your email address will not be published. Required fields are marked *


6 − = four

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>