Rejecting nested objects when doubly-nested object attributes are blank

In cases where you have nested models in a rails application, there is no built-in method to ignore (not save) nested objects, when their related nested object’s attributes are blank.

Consider first this case, in which I am working on a recipe application with two related models:

1
2
3
4
5
6
7
8
9
class Recipe < ActiveRecord::Base
  has_many :recipe_items, dependent: :destroy
  accepts_nested_attributes_for :recipe_items, 
    allow_destroy: true, 
    reject_if: :all_blank

## Additional code omitted for brevity

end
1
2
3
class RecipeItem < ActiveRecord::Base
  belongs_to :recipe
end

I’ve previously covered doubly-nested forms with an Parent Object has_many Child Objects has_many Child Objects arrangement, so I’m leaving the code needed for that out of the above models, to focus on what’s important for enabling proper handling of the situation in which blank nested objects are submitted while editing or creating a new Parent Object (a Recipe in this case).

A nested Recipe form with the models set up as above, and the safe parameters nested properly in the Recipe controller, would result in recipe items with all blank attributes being ignored (not saved) if they were added on the front-end via ajax say… No matter how many were added and left blank. There’s not much more to do in this case. Still, you may not be satisfied with modeling your data in this way, since you’ll end up with a table full of the same ingredients repeated over and over as you add recipes that contain them. So, you may want to nest another level here, so a Recipe can have many Recipe Items (which will have a quantity and units attribute, perhaps), and each Recipe Item will then have one Ingredient.

This relationship is more complex, but it allows you to reuse common ingredients. Once you get past the doubly-nested form. However, there is a problem with this arrangement because when you submit your Recipe form, Rails will check to see if the Recipe Item model is blank, so it can ignore them (and avoid adding blank ones to your Recipe. If you’ve added a bunch of Recipe Items (with their nested Ingredients) to the Recipe, however, even if you don’t select or enter an ingredient, the test of whether these Recipe Items is blank will return false - and your blank Recipe_items and ingredient will be stored and related to your Recipe (You’ll see if you go back to edit the Recipe. False is returned because even though the nested “attributes_ingredient” hash in params is also “blank”, it is still a hash inside of the attributes_recipe_items has, which is therefore not blank.

You could validate presence of the Quantity and Unit attributes on your Recipe Item model, but let’s say you want to be able to save ingredients, then come back later and fill in the quantity and measurement unit (not at all unreasonable, given how creative cooks work). In that case, you’ll need to make some additional accommodations in your models, including a proc or method in your Recipe Model to return true of false for “reject_if:” on the accepts_nested_attributes_for statement. OK, so that was a mouthful. Let me just show what I mean:

1
2
3
4
5
6
7
8
9
class Recipe < ActiveRecord::Base
  has_many :recipe_items, dependent: :destroy
  accepts_nested_attributes_for :recipe_items, 
    allow_destroy: true, 
    reject_if: :all_blank

## Additional code omitted for brevity

end
1
2
3
4
5
class RecipeItem < ActiveRecord::Base
  belongs_to :recipe
  has_one :ingredient, dependent: :destroy 
  accepts_nested_attributes_for :ingredient
end
1
2
3
class Ingredient < ActiveRecord::Base
  belongs_to :recipe_item
end

So, let’s walk though this. Say I open a new Recipe form. I enter my details on the Recipe itself (say, title and description), then go down to my ingredients area and start to add ingredients. I add an extra ingredient item, only to realize I’ve already entered them all, and I hit the save button. Rails sees reject_if: all_blank in the Recipe model, and iterates through all of the submitted Recipe Item hashes in params. Since the blank one I submitted returns false, because it’s got another hash nested inside it, it still gets written to the recipe items table. As mentioned earlier, you could require an amount, but that would make your app more of a pain to use for those who want to save ingredients, then come back later and add the quantity and units.

How can you accommodate them without risking a bunch of useless recipe_items ending up in the database? It’s as simple as adding a few lines of code to the Recipe Model:

Here’s one good method:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Recipe < ActiveRecord::Base
  has_many :recipe_items, dependent: :destroy
  accepts_nested_attributes_for :recipe_items, 
    allow_destroy: true, 
    reject_if: :reject_recipe_item

  def reject_recipe_item(attributed)
    attributed[:fermentable_attributes]['name'].blank?
  end

## Additional code omitted for brevity

end

Fortunately, the reject_if: wants a boolean, and doesn’t care how it gets it. You could easily use a proc, but I find a simple method like this to be more readable. Basically, what I’m doing here is saying that if I haven’t picked an ingredient (in my scheme you pick them by name in your recipe form), the method will return false to reject_if in my accepts_nested_attributes_for statement, and I avoid adding a recipe items field altogether, even if I’ve forgotten to delete empty ingredients from my form before submitting.

Created 7/11/2015 4:33PM (MDT) | Last Updated 7/11/2015 5:31PM (MDT)

Comments

Log in to add comments.