A nifty little problem with a simple, lightly-documented solution came up recently while I was working on a super-duper-top-secret-and-totally-awesome app for a client.
accepts_nested_attributes_for has been a boon in Rails 2.3 and a great replacement for the venerable attribute_fu. Under normal circumstances, you would use it like this to allow you to save an associated model thorough the parent model:
1 class Book < ActiveRecord::Base 2 has_many :chapters 3 4 accepts_nested_attributes_for :chapters 5 end
In your forms, you can use fields_for to specify associated Chapter models. With a little JavaScript-fu, you can craft a form to create a Book with multiple Chapters all at once, without a lot of controller code. With a little more JavaScript and an option to accepts_nested_attributes_for you can enable deletions:
1 class Book < ActiveRecord::Base 2 has_many :chapters 3 4 accepts_nested_attributes_for :chapters, 5 :allow_destroy => true, 6 end
At this point, you may want to detect and reject empty associated models like this:
1 class Book < ActiveRecord::Base 2 has_many :chapters 3 4 accepts_nested_attributes_for :chapters, 5 :allow_destroy => true, 6 :reject_if => proc {|attrs| attrs['title'].blank? } 7 end
The wrinkle comes in if you also want to validate that you always have at least one associated model. You might think of doing the following, which is close, but does not work in all situations:
1 class Book < ActiveRecord::Base 2 has_many :chapters 3 4 accepts_nested_attributes_for :chapters, 5 :allow_destroy => true, 6 :reject_if => proc {|attrs| attrs['title'].blank? } 7 8 validate :must_have_one_chapter 9 10 def must_have_one_chapter 11 errors.add(:chapters, 'must have one chapter') if chapters_empty? 12 end 13 14 def chapters_empty? 15 chapters.empty? 16 end 17 end
This works except when you are destroying an associated model; the destroy occurs after the validations have been run, making chapters_empty? true. The fix is to check the associated models to see if they are marked_for_destruction during the save, like so:
1 class Book < ActiveRecord::Base 2 has_many :chapters 3 4 accepts_nested_attributes_for :chapters, 5 :allow_destroy => true, 6 :reject_if => proc {|attrs| attrs['title'].blank? } 7 8 validate :must_have_one_chapter 9 10 def must_have_one_chapter 11 errors.add(:chapters, 'must have one chapter') if chapters_empty? 12 end 13 14 def chapters_empty? 15 chapters.empty? or chapters.all? {|chapter| chapter.marked_for_destruction? } 16 end 17 end
Now the validation will fail as expected; you might check the associated models for destruction seperately to generate a more appropriate message.