Pat Shaughnessy

Ribadesella, Spain

Scaffolding for complex forms using nested attributes

November 09, 2009 · 10 comments

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!

Tags:·

10 responses so far ↓

  • 1 Martin Evans // Nov 11, 2009 at 02:10 PM

    Using view mapper is it possible to combine the nested attributes, with auto complete on a have_many through model thanks
  • 2 pat // Nov 11, 2009 at 02:41 PM

    Hi Martin – no, not today; View Mapper only supports one view at a time, and won’t be able to combine has_many with auto_complete.

    But if you can wait I was actually planning to do exactly what you’re asking for next by adding a ViewMapper module to my repeated auto complete plugin, enabling auto complete on a text field in a repeated child model’s form. (The standard Rails auto_complete plugin won’t work if it’s used for text fields repeated on the same form.)

    If you can’t wait, then take a look at my article Auto complete for complex forms using nested attributes in Rails 2.3, and specifically at my last comment there about using the plugin with the :child_index parameter in Eloy Duran's complex-forms-examples sample app.

  • 3 Martin Evans // Nov 12, 2009 at 10:46 AM

    Sounds good thanks for info, whats your timescale for adding viewmapper module to auto complete?
  • 4 pat // Nov 12, 2009 at 03:53 PM

    Sadly, View Mapper isn't my day job :( It's going to take me about 2 weeks before I can have that coded, tested and on gemcutter/github.
  • 5 Nancy Martin // Mar 16, 2010 at 08:12 PM

    I created a scaffold for paperclip and scaffold for people with has_many paperclip attachments. I can't seems to link the paperclip attachments with my people scaffold, I always received a missing.png attachments. How will I link them together?
  • 6 pat // Mar 16, 2010 at 11:50 PM

    Hi Nancy, Sorry, I’m not quite sure what you’re trying to do exactly… do you have a Person model which will have many attachments? And then you want to display a form for it? If so, the “has_many view” I describe above can help you do this, but you’ll have to do a bit of extra work to add the paperclip fields to the form. Let me know if this is what you’re looking for and I can post some detailed instructions on how to do that.

    Or maybe I’ve misunderstood you, and you’re thinking of something else?

  • 7 Nancy // Mar 17, 2010 at 02:27 AM

    Hi Pat!, yes that is what I'm trying to do, I have a person model that I have created using "./script/generate scaffold_for_view person name:string --view has_many:attachment", and my attachment model is created using "./script/generate scaffold_for_view attachment name:string --view paperclip:doc" I have edited the views to display the attached file, and added html multipart in the block that render the form partials for the document that includes the name field and file field, but the file doesn't get uploaded and instead I have a "/files/missing.png" links in the record. I'm really interested about your instructions, I'm sorry for my english.
  • 8 pat // Mar 17, 2010 at 05:27 PM

    Hi Nancy, Your English is perfect; don’t worry about that! Here’s how I did it… very similar to what you did but I also added a foreign key column “person_id” and used “asset” instead of “attachment” since I got an error from Paperclip with the model name “attachment.”

    A few other people have asked me about this so maybe I’ll add a “has_many_paperclip” view to the gem which will handle all of this automatically… for now you will have to use these manual steps.

    Create a new Rails project and install Paperclip:

    rails multiple_attachments
    cd multiple_attachments
    ./script/plugin install git://github.com/thoughtbot/paperclip.git


    Create a model to hold each attachment – note the foreign key “person_id” here:

    ./script/generate scaffold_for_view asset name:string
                      person_id:integer --view paperclip:doc
    rake db:migrate
    


    Edit app/models/asset.rb and add the “belongs_to” line:

    class Asset < ActiveRecord::Base
      has_attached_file :doc
      belongs_to :person
    end


    Create the primary model that will contain multiple attachments:

    ./script/generate scaffold_for_view person name:string
                      --view has_many:assets
    rake db:migrate


    Select “yes” when asked about overwriting the scaffold.css file. Now you need to edit both app/views/people/new.html.erb and app/views/people/edit.html.erb and add the multipart option:

    <% form_for(@person, :html => { :multipart => true }) do |f| %>


    Now copy and paste the paperclip input field from app/views/asset/edit.html.erb to app/views/people/_asset.html.erb:

    <p>
      <%= f.label :doc %><br />
      <%= f.file_field :doc %>
    </p>
    


    And finally copy and paste the show link from app/views/asset/show.html.erb to app/views/people/show.html.erb – also edit the code to use asset and not @asset:

    <% @person.assets.each do |asset| %>
    
      <div class="child">
        <p>
          <b>Asset Name:</b>
          <%=h asset.name %>
        </p>
        <p>
          <b>Doc:</b>
          <%= link_to asset.doc_file_name, asset.doc.url %><br>
        </p>
      </div>
    <% end %>
  • 9 Nancy // Mar 17, 2010 at 09:59 PM

    Thank you so much for this wonderful plugin. Here's what I suggest that I wish this plugin can do more is to have multiple --view parameters. Example, I want to attach a picture of the person, that has also has_many assets. ./script/generate scaffold_for_view person name:string --view has_many:assets --view paperclip:photo This will then create a view for a person that has_many assets and also create a paperclip field for the person model and views to upload a single photo. Again, merci beaucoup!
  • 10 pat // Mar 18, 2010 at 07:28 AM

    Yea that would be very cool… the only problem is how to implement it :) Right now each view is really just a series of ERB generator templates that override the standard Rails scaffold generator ERBs. But having multiple views would require something very different… more like a repeated search/replace model. Anyway, I’ll see what I can do; thanks for trying it and for the positive feedback…

Leave a Comment