Pat Shaughnessy

Ribadesella, Spain

Scaffolding for auto complete on a complex, nested form

November 25, 2009 · 24 comments

I just updated View Mapper to work with my fork of the Rails auto_complete plugin that allows for repeated text fields on the same complex form. This means that View Mapper can now generate scaffolding code that uses both nested attributes and the auto_complete plugin at the same time, to display a form like this:

To generate this sort of complex form for two of your models you’ll first need to install my “repeated_auto_complete” gem from gemcutter.org:

$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources
$ sudo gem install repeated_auto_complete
Successfully installed repeated_auto_complete-0.1.0
1 gem installed
Installing ri documentation for repeated_auto_complete-0.1.0...
Installing RDoc documentation for repeated_auto_complete-0.1.0...

To learn more about repeated_auto_complete and what it does, see: http://patshaughnessy.net/repeated_auto_complete. Now you can generate a complex form like the one shown above for two of your models in a has_many/belongs_to, has_and_belongs_to_many or has_many, :through association by installing View Mapper (version 0.3.1 or later):

$ sudo gem install view_mapper
Successfully installed view_mapper-0.3.1
1 gem installed
Installing ri documentation for view_mapper-0.3.1...
Installing RDoc documentation for view_mapper-0.3.1...

… and then running the “view_for” generator with a view option called “has_many_auto_complete,” like this:

./script/generate view_for group --view has_many_auto_complete:people

 

Detailed Example

To see how easy it is to create a complex form using View Mapper, let’s create one from scratch in a brand new Rails app. You should be able to follow along using the commands below on your machine. First, let’s create a new Rails application:

$ rails complex_auto_complete
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  config/initializers
      create  config/locales
    … etc..
      create  log/server.log
      create  log/production.log
      create  log/development.log
      create  log/test.log

The first thing I’ll do is install the auto_complete plugin. However, since I’m planning to use auto_complete on a complex form, I’ll need to get my fork of auto_complete which I’ve deployed as a gem on gemcutter.org:

$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources
$ sudo gem install repeated_auto_complete
Successfully installed repeated_auto_complete-0.1.0
1 gem installed
Installing ri documentation for repeated_auto_complete-0.1.0...
Installing RDoc documentation for repeated_auto_complete-0.1.0...

And let’s update my new app to use the repeated_auto_complete gem by editing the config/environment.rb file:

Rails::Initializer.run do |config|
…etc…
  config.gem "repeated_auto_complete"
…etc…

If you prefer, you can also install this the old fashioned way, using “script/plugin install git://github.com/patshaughnessy/auto_complete.git”. Next, let’s generate a new model called “person” with a couple of fields for name and age, like the ones shown above in the screen shot:

$ cd complex_auto_complete/
$ ./script/generate model person name:string age:integer group_id:integer
      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/20091125195040_create_people.rb

Note that I’ve also included an integer field for the group id, since in a minute I’ll be adding a belongs_to association for people to groups.

Now I’m ready to use View Mapper… if you haven’t installed that yet, get it from gemcutter.org like this:

$ sudo gem install view_mapper
Successfully installed view_mapper-0.3.1
1 gem installed
Installing ri documentation for view_mapper-0.3.1...
Installing RDoc documentation for view_mapper-0.3.1...

You’ll need at least version 0.3.1 to use auto_complete on a complex form. Now I can use View Mapper to create scaffolding for a new “group” model that has many people with auto_complete like this:

$ ./script/generate scaffold_for_view group name:string
                    --view has_many_auto_complete:people
       error  Table for model 'person' does not exist
              - run rake db:migrate first.

Yes… I forgot to create the people table in my database; if we do that:

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

… and then re-run View Mapper:

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

… we get a second error message! This time View Mapper is reminding me that I still need to add “belongs_to :group” to the person model in order to get the complex form to work. Let’s do that now:

class Person < ActiveRecord::Base
  belongs_to :group
end

And now I can run View Mapper once more:

$ ./script/generate scaffold_for_view group name:string
                    --view has_many_auto_complete:people
      exists  app/models/
…etc…
      create  app/models/group.rb
      create  test/unit/group_test.rb
      create  test/fixtures/groups.yml
      exists  db/migrate
      create  db/migrate/20091125195715_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
       route  map.connect 'auto_complete_for_group_name',
                          :controller => 'groups',
                          :action => 'auto_complete_for_group_name'
       route  map.connect 'auto_complete_for_person_name',
                          :controller => 'groups',
                          :action => 'auto_complete_for_person_name'
       route  map.connect 'auto_complete_for_person_age',
                          :controller => 'groups',
                          :action => 'auto_complete_for_person_age'

Now you can see the new scaffolding files View Mapper created, including some new scaffolding files peculiar to complex forms, like “nested_attributes.js,” “_form.html.erb,” and “_person.html.erb.” You may also have noticed View Mapper added three new routes related to the auto_complete plugin; these will handle the AJAX requests used to return the auto_complete options to the form.

Now to get it all to work, I just need to create the group table:

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


Now running my server and creating a new group I see:

If you click “Add a Person” you’ll see nested fields for new Person records appear. This all works exactly the same way as the standard nested attributes scaffolding that I described in my last post. The only difference is that in this form, each of the text fields present in both the parent (“Group”) and child (“Person”) models are displayed using the “text_field_with_auto_complete” method.

I’ll try to write up a detailed walk through of how this scaffolding actually works as soon as I can… there are a lot of interesting details in the code that will be fun to look at. In the meantime, hopefully this scaffolding will make it easier for you to learn how to use auto_complete and nested attributes together in your app.

24 comments Tags:··

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!

10 comments Tags:·