Validations For Multiple Nested Model Forms

Here’s a little trick I found when handling multiple nested models in a form that require validations for each model.

Spec time. For example, we have a model, Author, which hasmany Books and hasmany Magazines. Book and Magazine has validations. On a single page, we create a three forms for Book, Magazine, and Author. There is also an option select that a user can choose if the Author has a Book or a Magazine.

Pretty straight forward. Let’s do some code.

#models/author.rb
class Author < ActiveRecord::Base
  has_many :books
  has_many :magazines

  accepts_nested_attributes_for :books
  accepts_nested_attributes_for :magazines

  validates_presence_of :name
end
#models/book.rb
class Book < ActiveRecord::Base
  belongs_to :author
  validates_presence_of :title,
                        :genre
end
#models/magazine.rb
class Magazine < ActiveRecord::Base
  belongs_to :author
  validates_presence_of :title,
                        :genre
end

Basic model setup with validations and acceptsnestedattributes_for. More info on that can be found in the Ruby on Rails API - Nested Attributes.

Moving on to the form setup..

#views/authors/new.html.erb
<% form_for @author do |f| %>
<%= f.error_messages %>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.radio_button("media_type", "book" %><%= f.label :media_type_book, 'Book' %> [?]
    <%= f.radio_button("media_type", "magazine" %><%= f.label :media_type_magazine, 'Magazine' %> [?]
  </p>
  <% f.fields_for :books do |book| %>
    <p>
      <%= book.label :title %><br />
      <%= book.text_field :title %>
    </p>
    <p>
      <%= book.label :genre %><br />
      <%= book.text_field :genre %>
    </p>
  <% end %>

  <% f.fields_for :magazines do |magazine| %>
    <p>
      <%= magazine.label :title %><br />
      <%= magazine.text_field :title %>
    </p>
    <p>
      <%= magazine.label :genre %><br />
      <%= magazine.text_field :genre %>
    </p>
  <% end %>
  <%= f.submit 'Submit' %>
<% end %>

That sets up the form with the nested model, now for the controller code.

#controllers/authors_controller.rb
class AuthorsController < ApplicationController
  def new
    @author = Author.new
    books = @author.books.build #this builds the nested form in the view
    magazines = @author.magazines.build #this builds the nested form in the view
  end

  def create
    param_hash = params[:author]
    if params[:author][:media_type] == "book"
      param_hash.delete("books_attributes")
    elsif params[:author][:media_type] == "magazine"
      param_hash.delete("magazines_attributes")
    end
    @author = Author.new(param_hash)
    @author.save
  end
end

Instead of passing the params straight to Author.new, it’s put into a hash variable. Then the params get checked for a title and genre, if empty delete the key from the hash and pass to Author.new. Activerecord wont see the book param and will skip the validations for it.