Complex Rails Forms with Nested Attributes

Rails provides a powerful mechanism for easily creating rich forms called “nested attributes.” It allows you to combine more than one model in your forms while keeping the same basic code pattern that you use with simple single model forms.

In this article I’ll be showing a number of different ways to use this technique. I’ll assume that you’re familiar with basic Rails forms, of the kind that are generated by the scaffolding commands. We’ll be building up a complex form step by step that allows a user to edit their preferences. Our domain is a not-for-profit management system, where volunteers (users) have areas of expertise and tasks that have been assigned to them.

The Base Form

Let’s start with a basic form that can edit a user. I assume you are familiar with this pattern, so I won’t explain it. I only present it here because the rest of the article will be building on top of this code.

First up is a simple user model with just one attribute:

# app/models/user.rb
class User < ActiveRecord::Base
  validates_presence_of :email
end

We will be using the same controller for the entire of this article. This is the beauty of nested attributes—we don’t have to change our controller code!

# app/controllers/users_controller.rb
class UsersController
  def new
    @user = User.new
  end

  def edit
    @user = User.find(params[:id])
  end

  def create
    @user = User.new(params[:user])

    if @user.save
      redirect_to @user
    else
      render :action => 'new'
    end
  end

  def update
    @user = User.find(params[:id])

    if @user.update(params[:user])
      redirect_to @user
    else
      render :action => 'edit'
    end
  end
end

Our base form is exactly what is generated by the Rails scaffolding:

# app/views/users/_form.html.erb
<%= form_for(@user) do |f| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= pluralize(@user.errors.count, "error") %> prohibited
        this user from being saved:
      </h2>

      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div>
    <%= f.label :email %>
    <%= f.text_field :email %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

With that out of the way, let’s dive in!

Adding an Address

We are storing a user’s address record in a separate model, but we want to be able to edit the address on the same form as other attributes of the user.

# app/models/user.rb
class User < ActiveRecord::Base
  # ... code from above omitted
  has_one :address
  accepts_nested_attributes_for :address
end

# app/models/address.rb
class Address < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :city
end

Note the addition of accepts_nested_attributes_for to the User model. This method allows you to modify instances of Address using the same mass-assignment facility on User that makes simple forms so trivial. accepts_nested_attributes_for adds a writer method _attributes to the model that allows you to write code like this:

user = User.find(1)

# Normal mass-assignment
user.update(:email => 'new@example.com')

# Creates or edits the address
user.update(:address_attributes => {:city => 'Hobart'})

You can see how we won’t have to modify our controller code if we set up our form correctly, since to edit the address attributes you use the same #update method as you do to edit the user’s email.

To create the form, we will use the fields_for builder method. This is a complicated method that can do many things. Rather than explain it all upfront, I will introduce some of its many behaviours through the upcoming examples.

First of all you can pass fields_for a symbol of a relationship name and it will intuit from that relationship how it should render the fields. I know that sounds complicated, but the following code snippet should make it clearer:

# app/views/users/_form.html.erb
# ... Form code from above omitted
<%= f.fields_for :address do |ff| %>
  <div>
    <%= ff.label :city %>
    <%= ff.text_field :city %>
  </div>
<% end %>

Please note the changed variable name for the fields_for block—ff rather than f. In this case for a has_one relationship, the logic is “if an address exists, show a field to edit the city attribute. Otherwise if there is no address, don’t show any fields.” Here we hit our first stumbling block: if the fields are hidden when there is no address, how do we create an address record in the first place? Since this is a view problem (do we display fields or not?), we want to solve this problem in the view layer. We do this by setting up default values for the form object in a helper:

# app/helpers/form_helper.rb
module FormHelper
  def setup_user(user)
    user.address ||= Address.new
    user
  end
end

# app/views/users/_form.html.erb
<%= form_for(setup_user(user)) do |f| %>
  ...

Now if the user doesn’t have an address we create a new unsaved one that will be persisted when the form is submitted. Of course, if they do have address no action is needed (||= means “assign this value unless it already has a value”).

Try this out and you’ll see that rails even correctly accumulates and displays errors for the child object. It is pretty neat.

Adding Tasks

A user can have many tasks assigned to them. For this example, a task simply has a name.

# app/models/task.rb
class Task < ActiveRecord::Base
  belongs_to :user

  validates_presence_of :name
end

# app/models/user.rb
class User < ActiveRecord::Base
  # ... code from above omitted
  has_many :tasks

  accepts_nested_attributes_for :tasks,
    :allow_destroy => true,
    :reject_if     => :all_blank
end

There are two new options here: allow_destroy and reject_if. I’ll explain them a bit later on when they become relevant.

As with the address, we want tasks to be assigned on the same form as editing the user. We have just set up accepts_nested_attributes_for, and there are two steps remaining: adding correct fields_for inputs, and setting up default values.

# app/views/users/_form.html.erb
<h2>Tasks</h2>
<%= f.fields_for :tasks do |ff| %>
  <div>
    <%= ff.label :name %>
    <%= ff.text_field :name %>

    <% if ff.object.persisted? %>
      <%= ff.check_box :_destroy %>
      <%= ff.label :_destroy, "Destroy" %>
    <% end %>
  </div>
<% end %>

When fields_for is given the name of a has many relationship, it iterates over every object in that collection and outputs the given fields once for each record. So for a user that has two tasks, the above code will create two text fields, one for each task.

In addition, for each task that is persisted in the database, a check box is created that maps to the _destroy attribute. This is a special attribute that is added by the allow_destroy option. When it is set to true, the record will be deleted rather than edited. This behaviour is disabled by default, so remember to explicitly enable it if you need it.

Note that the id of any persisted records is automatically added in a hidden field by fields_for, you don’t have to do this yourself (though if you do have to for whatever reason, fields_for is smart enough to not add it again.) View the source on the generated HTML to see for yourself.

The form we have created will allow us to edit and delete existing tasks for the user, but there is currently no way to add new tasks since for a new user with no tasks, fields_for will see an empty relationship and render no fields. As above, we fix this by adding new default tasks to the user in the view.

There are a number of different UI behaviours you could apply, such as using javascript to dynamically add new records as they are needed. For this example though we are going to choose a simple behaviour of adding three blank records at the end of the list that can optionally be filled in.

# app/helpers/form_helper.rb
module FormHelper
  def setup_user(user)
    # ... code from above omitted

    3.times { user.tasks.build }
    user
  end
end

fields_for will iterate over these three records and create inputs for them. Now no matter how few or many tasks a user has, there will always be three blank text fields for new tasks to be added. There is a problem here though—if a blank task is submitted, is it a new record that is invalid (blank name) and should cause the save to fail, or was it never filled in? By default Rails assumes the former, but that is often not what is desired. This behaviour can be customized by specifying the reject_if option to accepts_nested_attributes_for. You can pass a lambda that is evaluated for each attributes hash, returning true if it should be rejected, or you can use the :all_blank shortcut like we have above, which is equivalent to:

accepts_nested_attributes_for :tasks,
  :reject_if => proc {|attributes|
    attributes.all? {|k,v| v.blank?}
  }

More complicated relationships

For this application, we want users to specify which areas in our not-for-profit they are interested in helping out with, such as admin or door knocking. This is modelled with a many-to-many relationship between users and interests.

# app/models/interest.rb
class Interest < ActiveRecord::Base
  has_many :interest_users

  validates_presence_of :name
end

# app/models/interest_user.rb
class InterestUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :interest
end

# app/models/user.rb
class User < ActiveRecord::Base
  # ... code from above omitted
  has_many :interest_users
  has_many :interests, :through => :interest_users

  accepts_nested_attributes_for :interest_users,
    :allow_destroy => true
end

The only extra concept added here is the allow_destroy option, which we used in the previous example. As the name implies, this allows us to destroy child records in addition to creating and editing them. Recall that by default, this behaviour is disabled, so we need to explicitly enable it.

As before, after adding accepts_nested_attributes_for there are two more steps to adding interest check boxes to our form: setting up appropriate default values, and using fields_for to create the necessary form fields. Let’s start with the first one:

# app/views/users/_form.html.erb
<%= f.fields_for :interest_users do |ff| %>
  <div>
    <%= ff.check_box :_destroy,
          {:checked => ff.object.persisted?},
          '0', '1'
    %>
    <%= ff.label :_destroy, ff.object.interest.name %>
    <%= ff.hidden_field :interest_id %>
  </div>
<% end %>

Once again, when fields_for is given the name of a has many relationship, it iterates over every object in that collection and outputs the given fields once for each record. So for a user that has two interests, the above code will create two check boxes, one for each interest.

We know that the allow_destroy option above allows us to send a special _destroy attribute that if true will flag the object to be deleted. The problem is that this is the inverse of the default check box behaviour: when the check box is unchecked we want _destroy to be true, and when it is checked we want to keep the record around. That is what the last two parameters to check_box do (‘0’ and ‘1’): set the checked and unchecked values respectively, flipping them from their defaults.

While we are in that area, we also need to override the default logic that decides whether the check box is checked initially. This is what :checked => ff.object.persisted? does—if the record exists in the database, then the user has indicated they are interested in that area, so the box should be checked. Note here the use of ff.object to access the current record in the loop. You can use this method inside any form_for or fields_for to get the current object.

I have been talking about checking whether the current record is persisted or not. When you load a user out of the database, of course all the interest records will be persisted. The problem is only those interests already selected will be shown and checked, whereas we actually need to show all interests whether or not they have been selected in the past. This is where we use our setup_user method from earlier to provide “default” new records for interests that are not persisted.

# app/helpers/form_helper
module FormHelper
  def setup_user(user)
    user.address ||= Address.new
    (Interest.all - user.interests).each do |interest|
      user.interest_users.build(:interest => interest)
    end
    user.interest_users.sort_by! {|x| x.interest.name }
    user/tmp/clean-controllers.md.html
  end
end

First this code creates a new join record for all interests that the user does not currently have selected (Interest.all - user.interests), and then uses an in-place sort (sort_by!) to ensure that the check boxes are always shown in a consistent order. Without this, all the new unchecked records would be grouped at the bottom of the list.

Parting Words

Nested attributes is a powerful technique to quickly develop complex forms while keeping your code nice and neat. fields_for gives you a lot of flexibility and options for conforming to the nested attributes pattern—see the documentation—and you should always try to structure your forms to take advantage of the behaviour that accepts_nested_attributes_for gives you. Going beyond this article, just a touch of javascript magic supporting dynamic creating of new nested records can make your forms really stand out.

You can download the complete code for this article over on github to have a play around with it. Let us know how you go in the comments.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Joseph

    Thank-you kindly for taking the time to document this technique as I am relatively new to rails and have noticed the tutorials in the rails guides on how to implement nested relationships (as in the blog->comments example). However, I found the absence of how to edit/remove the nested comments a bit challenging to accomplish (as easily as using SQL for myself on other platforms). May you prosper in all you set out to do!

  • http://www.thepursuitofquality.com Gavin Miller

    Just wanted to say that this was a great tutorial Xavier, and it really helped me out this weekend. Look forward to future tutorials.

  • Dave

    Thanks for the guide. When I embed a class in another, even with accepts_nested_attributes_for, I get an error in the main class that the subclass is invalid if it does not pass validation, not the errors from the sub-class.

    For example:

    Class User String
    has_one: :name
    accepts_nested_attributes_for :name

    validates_presence_of :email
    end

    Class Name String
    field :first_name, :type => String

    belongs_to: :user
    validates_presence_of :first_name, :last_name
    end

    If I do something like this on the console:

    @user = User.new
    @user.name = Name.new
    @user.name.valid?
    @user.name.errors

    I get my errors for name:

    #["can't be blank"], :first_name=>["can't be blank"]}>

    Then, when I check @user:
    @user.valid?
    @user.errors
    => #["can't be blank"], :name=>["is invalid"], :email=>["can't be blank"]}>

    It’s invalid rather than the specific errors. How can I get the errors to pass through to @user.errors?

    Thanks!

    • http://xaviershay.com/ Xavier Shay

      With the errors on the child object, they will show up correctly against the form field. It would be confusing on the parent object. If you want an aggregated list, perhaps you could add them together? @user.errors + @user.name.errors (untested)

  • Michael

    maybe you forget the instance variable @user

    # app/views/users/_form.html.erb

    instead of

  • http://www.davidstephens.co.uk David Stephens

    Thanks for the article – helped me to fix my fields_for issue where it wasn’t showing when the record didn’t yet exist!

  • Shane

    Hey Xavier

    I was just doing a similar thing with nested attributes as you did with the interest_users association and hacked the _destroy to be the opposite.

    The thing I have hit (and I see you have the same problem), is if I select some interests but have not entered email (ie a validation error on User), I lose my selected records.

    In the case of editing I lose both newly selected and unselected.

    Any thoughts on how that could be resolved?

  • Pingback: Building a Complex Form using Join Models

  • Tom

    Thanks for this tutorial … I will be following turning to it for guidance as I design my forms.

    I see you also have great skill and knowledge with regards to databases. I am new to programming in general, accounting is my background, but I am working on an app and I have ran across an article that suggested that ones security should be handled through the database rather than through the application. I think the reasoning was that a database is typically the most valuable asset of an organization and that it the single object that organizations want to protect.

    I was wondering if you have any thoughts on this issue and whether you have any tutorials that deal with it or if you could point me to some resources that discuss how one could design the security mechanism through triggers, rules etc in the database. The database I am using is postgresql 9.1.

  • Arif Usman

    Nice Tutorial. But can you help me with some AJAX stuff. I want to add a new set of fields on click of Add Another link and also want to save the previous data present in my dynamically generated text fields. Just doing this thing for entering the new data which should always be greater than the previous data which I entered and for doing that I need to store my previous data and to compare with that only.

  • Mohamad

    Xavier, great tutorial. But what about when we need to set additional attributes on the join model? So, if your example, say interest_user had an additional field. Although the example is stupid, say a string field “reason_for_interest”. How would you set that up?

    My use case is slightly. My joiner table is “roles”, and my field is “role_type”. Roles links User to Account (user has many accounts through roles and vice-versa). I don’t want to expose the Role object to the view, but I do want to set the role_type attribute in the controller. My method does not work though:

    def new
    @user = User.new
    @role = @user.roles.build(role_type: “admin”)
    @account = @user.accounts.build
    end

  • Joël

    In app/helpers/form_helper.rb, is the line with “user/tmp/clean-controllers.md.html” a copy/paste error or could you explain what is that line?

  • Dennis

    Is it possible on my html web page hit the html submit form, the data of form should email at the desired email address through ruby on rails code instead of using php

  • Jan

    Does this still work? I get a mass assignment error in the FormHelper from :alley

    “MassAssignmentSecurity::Error: Can’t mass-assign protected attributes…”

  • http://pehrlich.com Peter Ehrlich

    I’ve made a quick module which allows you to easily see full error messages for nested resources:

    #1.9.3-p362 :008 > s.all_full_error_messages
    # => ["Purchaser can't be blank", "Consumer email can't be blank", "Consumer email is invalid", "Consumer full name can't be blank"]

    see https://gist.github.com/4710856

  • http://zaidisoft.com Zaidi

    Just what I was looking for to start working on my project. My question is, how can I build on this to create a multiple choice quiz?

    Thanks.