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:

1class Book < ActiveRecord::Base
2  has_many :chapters
3
4  accepts_nested_attributes_for :chapters
5end

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:

1class Book < ActiveRecord::Base
2  has_many :chapters
3
4  accepts_nested_attributes_for :chapters,
5    :allow_destroy => true,
6end

At this point, you may want to detect and reject empty associated models like this:

1class 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? }
7end

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:

 1class 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
17end

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:

 1class 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
17end

Now the validation will fail as expected; you might check the associated models for destruction seperately to generate a more appropriate message.

blog comments powered by Disqus