Scaffolding for complex forms using nested attributes

While the new nested attributes feature in Rails 2.3 greatly simplifies writing forms that operate on two or more models at the same time, writing a complex form is still a confusing and daunting task even for experienced Rails developers. To make this easier, I just added nested attribute support to my View Mapper gem. This means you can generate complex form scaffolding for two or more models in a has_many/belongs_to, has_and_belongs_to_many or has_many, through relationship.

Example:

If I have a group model that has many people and accepts nested attributes for them like this:

class Group < ActiveRecord::Base
  has_many :people
  accepts_nested_attributes_for :people, :allow_destroy => true
end

… and a person model that belongs to a group:

class Person < ActiveRecord::Base
  belongs_to :group
end

… then View Mapper will allow you to generate scaffolding that displays groups of people all at once, like this:

$ ./script/generate view_for group --view has_many:people
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/groups
…etc…
      create  app/views/groups/_form.html.erb
      create  app/views/groups/_person.html.erb
      create  public/javascripts/nested_attributes.js

Now if I open my Rails app and create a new group, I will see:

This looks just like the standard Rails scaffolding, but with one additional “Add a Person” link. If you click on it, you’ll see the attributes of the person model appear along with a “remove” link, indented to the right:

If I enter some values and submit, ActiveRecord will insert a new record into both the groups table and the people table, and set the group_id value in the new person record correctly:

View Mapper has:

  • inspected your group and person models to find their attributes (columns).
  • validated that they are in a has_many / belongs_to relationship, or in a has_and_belongs_to_many or a has_many, through relationship.
  • checked that you have a foreign key column (“group_id” by default for this example) in the people table if necessary. (The foreign key isn’t in the people table for has_and_belongs_to_many or has_many, through.)
  • generated scaffolding using your attribute and model names, and that uses Javascript to support the “Add a person” and “remove” links.

To get the add/remove links to work, I used a simplified version of the “complex-form-examples” sample application from Ryan Bates and Eloy Duran. Ryan has a few screen casts on this topic as well. In my next post I’ll explain how that works in detail, since understanding the details about how scaffolding works is the first step towards using it successfully in your app.

But for now, you can try this on your machine using the precise commands below…

Creating a new complex form from scratch

Let’s get started by creating a new Rails application; you will need to have Rails 2.3 or later in order to make this work:

$ rails complex-form
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
… etc …
      create  log/production.log
      create  log/development.log
      create  log/test.log

Using the same group has many people example from above, let’s generate a new person model:

$ cd complex-form
$ ./script/generate model person first_name:string last_name:string
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/person.rb
      create  test/unit/person_test.rb
      create  test/fixtures/people.yml
      create  db/migrate
      create  db/migrate/20091109204744_create_people.rb

And let’s run that migration to create the people table:

$ rake db:migrate
(in /Users/pat/rails-apps/complex-form)
==  CreatePeople: migrating ===================================================
-- create_table(:people)
   -> 0.0013s
==  CreatePeople: migrated (0.0014s) ==========================================

Now we’re ready to run View Mapper. View Mapper contains two generators; one is for creating scaffolding for an existing model, called “view_for,” which is what I used above. There’s also another generator called “scaffold_for_view” which will create a new model along with the scaffolding, using the same syntax as the standard Rails scaffold generator. Let’s use that here, since we have a new app and haven’t created the group model yet:

$ ./script/generate scaffold_for_view group name:string --view has_many:people
     warning  Model Person does not contain a belongs_to association for Group.

Here View Mapper is reminding me that I didn’t specify “belongs_to” in the person model. This saves me the trouble later of figuring out what’s wrong when my complex form doesn’t work. Let’s add that line to app/models/person.rb and try again:

class Person < ActiveRecord::Base
  belongs_to :group
end

$ ./script/generate scaffold_for_view group name:string --view has_many:people warning Model Person does not contain a foreign key for Group.

Duh… I also forgot to include the “group_id” column when I generated the person model. I could have done that by including “group_id:integer” on the script/generate model command line above. Since I already have the person model now, let’s just continue by creating a new migration for the missing column:

$ ./script/generate migration add_group_id_column_to_people
      exists  db/migrate
      create  db/migrate/20091109205711_add_group_id_column_to_people.rb

Editing the migration file:

class AddGroupIdColumnToPeople < ActiveRecord::Migration
  def self.up
    add_column :people, :group_id, :integer
  end
etc…

And running the migration:

$ rake db:migrate
(in /Users/pat/rails-apps/complex-form)
==  AddGroupIdColumnToPeople: migrating =======================================
-- add_column(:people, :group_id, :integer)
   -> 0.0010s
==  AddGroupIdColumnToPeople: migrated (0.0012s) ==============================

Now let’s run View Mapper once more to see whether we have any other problems, or whether we’re ready to generate the complex form scaffolding:

$ ./script/generate scaffold_for_view group name:string --view has_many:people
      exists  app/models/
      exists  app/controllers/
…etc…
      create  app/models/group.rb
      create  test/unit/group_test.rb
      create  test/fixtures/groups.yml
      exists  db/migrate
      create  db/migrate/20091109210312_create_groups.rb
      create  app/views/groups/show.html.erb
      create  app/views/groups/_form.html.erb
      create  app/views/groups/_person.html.erb
      create  public/javascripts/nested_attributes.js

It worked! Just looking at the list of files that View Mapper created, you can get a sense of how it has customized the standard Rails scaffolding to implement the complex form: _form.html.erb, _person.html.erb, nested_attributes.js. More on these details in my next article.

One detail I will point out now is that in order to get you started in the right direction and to allow the complex form to work immediately, the scaffold_for_view generator included the has_many and accepts_nested_attributes_for calls in the new model:

class Group < ActiveRecord::Base
  has_many :people
  accepts_nested_attributes_for :people,
                                :allow_destroy => true,
                                :reject_if => proc { |attrs|
                                  attrs['first_name'].blank? &&
                                  attrs['last_name'].blank?
                                }
end

You don’t need to type in all of this code yourself and know the precise syntax of the accepts_nested_attributes_for method… it’s all generated for you. Later when you start to customize the scaffolding to work for your specific requirements, you’ll have a working example to look at right inside your app.

Finally, we’re need to run the migrations once more since the scaffold_for_view generator created a new group model and corresponding migration for the groups table:

$ rake db:migrate
(in /Users/pat/rails-apps/complex-form)
==  CreateGroups: migrating ===================================================
-- create_table(:groups)
   -> 0.0013s
==  CreateGroups: migrated (0.0014s) ==========================================

Now if you start up Rails and hit http://localhost:3000/groups/new, you’ll see the complex form!