I just updated View Mapper to support scaffolding for models in a has_many, :through relationship. It generates a complex form that is a combination of the “belongs_to” scaffolding from part 1 of this series and the nested attributes scaffolding I wrote about in November:

Based on the programmer/assignment/project example from the ActiveRecord documentation page, this form will create a new programmer record and allow the user to add one or more assignments, each of which also has a name text field. For each new assignment the user can also select an existing project record. Here’s the Programmer model with the has_many :through association:
class Programmer < ActiveRecord::Base
has_many :projects, :through => :assignments
has_many :assignments
accepts_nested_attributes_for :assignments,
:allow_destroy => true,
:reject_if => proc { |attrs|
attrs['name'].blank? &&
attrs['project_id'].blank?
}
end
This implements a many-many relationship between programmers and projects; the assignments model is used to map the projects with the programmers. I’ve also specified that the programmer model accepts_nested_attributes_for assignments… more on that below.
You can now use the “view_for” generator from View Mapper to generate the form above for your models using a new view called “has_many_existing:”
$ sudo gem install view_mapper
$ script/generate view_for programmer --view has_many_existing:projects
Assumptions and requirements
Nested Attributes: the form above works by using ActiveRecord’s nested attributes feature to save multiple assignments for a single programmer. Therefore, you need to be sure you call accepts_nested_attributes_for in your target model; if you forget to do this, you’ll get an error from View Mapper:
$ script/generate view_for programmer --view has_many_existing:projects
warning Model Programmer does not accept nested attributes
for model Assignment.
To fix this problem you can use the code I showed above:
class Programmer < ActiveRecord::Base
has_many :projects, :through => :assignments
has_many :assignments
accepts_nested_attributes_for :assignments,
:allow_destroy => true,
:reject_if => proc { |attrs|
attrs['name'].blank? &&
attrs['project_id'].blank?
}
end
The options I’ve specified here tell ActiveRecord it is allowed to delete assignment records (when the user clicks “remove” in the form) and to avoid creating empty assignment records if all of their attributes are blank (if the user clicked “Add Assignment” an extra time).
Or if you prefer you can generate the entire target model including the nested attributes call using the scaffold_for_view generator like this – specify the new model’s columns using the same syntax as the standard Rails scaffold generator:
$ script/generate scaffold_for_view programmer
first_name:string last_name:string
--view has_many_existing:projects
It’s easy to overlook one very elegant detail here about ActiveRecord’s nested attribtues feature: note that “project_id” is one of the nested attributes, generated by each of the project select list boxes. (They are implemented with collection_select; see part 1 of this series). Now when the new programmer form is submitted all of the associations for each assignment – and for the new programmer – are setup. In other words, after you save the new programmer record this way you can immediately access the associated projects through assignments: “programmer.projects” – very cool! And it's all seamless: I don't have to write any code in my controller to associate the projects or assignments with the new programmer.
Correct associations among your models: if you forget to put the proper associations in your three models the has_many :through behavior will not work. You need to have six associations setup among your three models like this:
class Programmer < ActiveRecord::Base
has_many :projects, :through => :assignments
has_many :assignments
end
class Project < ActiveRecord::Base
has_many :programmers, :through => :assignments
has_many :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :project
belongs_to :programmer
end
View Mapper will help you out by displaying an error message if you’re missing one of these:
$ script/generate scaffold_for_view programmer name:string
--view has_many_existing:projects
warning Model Project does not contain a has_many association for Assignment.
…or if you’re missing one of the corresponding foreign key columns in the “through” model:
$ script/generate scaffold_for_view programmer name:string
--view has_many_existing:projects
warning Model Assignment does not contain a foreign key for Programmer.
Has many existing model identified by name attribute: In the form above, the Project records were identified in the select list boxes using their “name” attribute. Therefore, you need to insure that your existing model has a name column or method; if it does not View Mapper will display an error message like this:
$ script/generate scaffold_for_view programmer name:string
--view has_many_existing:projects
warning Model Project does not have a name attribute.
To fix this problem, add a “name” method to your existing model, or else you can specify that View Mapper use a different attribute (e.g. “code”) instead with this syntax:
$ script/generate scaffold_for_view programmer name:string
--view has_many_existing:projects[code]
Associated model name method in through model: The last requirement is that the through model, Assignment in this example, have a method (“project_name”) to display the name of its associated existing model. View Mapper requires this to avoid putting this code into the view:
class Assignment < ActiveRecord::Base
belongs_to :project
belongs_to :programmer
def project_name
project.name if project
end
end
If you forget this method, View Mapper will remind you with this error message:
$ script/generate scaffold_for_view programmer name:string
--view has_many_existing:projects[code]
warning Model Assignment does not have a method project_code.
Detailed Example
Here’s a step by step example of how to create a Rails application from scratch that contains the has_many :through scaffolding:
Here’s our model to hold the existing data:
$ cd hmt_example
$ script/generate model project code:string
And the through model to associate projects with programmers; note I’ve included integer attributes as the foreign keys for both the existing model and the new model:
$ script/generate model assignment name:string
project_id:integer programmer_id:integer
$ rake db:migrate
Next edit the new models and enter the required associations along with the project_code method:
class Project < ActiveRecord::Base
has_many :programmers, :through => :assignments
has_many :assignments
end
class Assignment < ActiveRecord::Base
belongs_to :programmer
belongs_to :project
def project_code
project.code if project
end
end
Now we’re ready to create the programmer has_many :through scaffolding; note I’ve specified “code” as the attribute to use to identify each project:
$ sudo gem install view_mapper
$ script/generate scaffold_for_view programmer
first_name:string last_name:string
--view has_many_existing:projects[code]
$ rake db:migrate
Note this won’t work yet for a has_and_belongs_to_many association; dealing with that is next on my View Mapper to do list.
Tags:view_mapper
In my last post I started a series on how to write Rails forms that associate a new record with existing data. This sort of requirement comes up for me over and over again at my day job, and so I decided to support scaffolding for these forms in View Mapper.
Today I’ll continue by showing how to use the auto_complete plugin to select an existing record – exactly what Ryan Bates discussed in his screen cast Auto-Complete Association. Using the same Category/Product example, this form would allow the user to create a new product record, and associate it with an existing category tag:
To create scaffolding like this in your app with View Mapper, just run this command:
script/generate scaffold_for_view product name:string bar_code:integer
--view belongs_to_auto_complete:category
View Mapper will validate you have a line “has_many :products” in your category model, that you have a category model to begin with, and also that the auto_complete plugin is installed before proceeding to generate this form. Also, View Mapper assumes the parent model, “Category” in this example, has a “name” column and will use that value to identify each category in the auto complete list. You can indicate to use a different parent model field instead with this syntax:
script/generate scaffold_for_view product name:string bar_code:integer
--view belongs_to_auto_complete:category[display_name]
For more details on View Mapper, see the example below where I create a sample app from scratch.
Code review: model
Since Ryan explains auto complete association so well in his screen cast, I won’t repeat all of that information here. Instead, let’s take a look at the code View Mapper generates and compare it to what Ryan showed. First, in the product model Ryan has a “category_name” virtual attribute:
def category_name
category.name if category
end
def category_name=(name)
self.category = Category.find_or_create_by_name(name) unless name.blank?
end
This allows the view to display the category for each product easily, and also supports creating new categories on the fly when you submit a new product. The View Mapper scaffolding is a bit simpler and uses “find_by_name” instead of “find_or_create_by_name” since it assumes the category records already exist. Also, Ryan’s code uses “unless name.blank?” to avoid creating empty categories, while the View Mapper scaffolding assumes a blank category name indicates a product without a category, and allows the user to clear the category when editing an existing product. Either approach can make sense depending on the business requirements of your application. Here’s the View Mapper model code:
class Product < ActiveRecord::Base
belongs_to :category
def category_name
category.name if category
end
def category_name=(name)
self.category = Category.find_by_name(name)
end
end
Code review: controller
In the controller code, the View Mapper scaffolding differs from Ryan’s solution more dramatically. To return the list of matching categories to the auto_complete plugin, Ryan adds this code to the categories controller to query the category records that have a name field that match a given search parameter:
def index
@categories =
Category.find(:all, :conditions => ['name LIKE ?', "%#{params[:search]}%"])
end
This makes a lot of sense: the categories controller should be used to generate a list of categories. However, for View Mapper I chose to use the products controller instead since the scaffolding generator already generates that code file, and to avoid the need for creating or modifying the categories controller also. View Mapper just adds this one line to the products controller to return the list of category names:
auto_complete_for :category, :name
This simple call actually achieves exactly the same thing as the Category.find call above. In my next post, I’ll take a look at the Ruby metaprogramming used by auto_complete_for and show how it automatically generates a method that executes the same SQL query.
Code review: view
Finally in the view we see a call to “text_field_with_auto_complete” to use the Prototype javascript library’s auto_complete feature. Here’s Ryan’s view code from the screen cast:
<%= text_field_with_auto_complete :product,
:category_name,
{ :size => 15 },
{ :url => formatted_categories_path(:js),
:method => :get,
:param_name => 'search' }
%>
“:url => formatted_categories_path(:js)” calls the categories controller code above when the user starts to type in the text field, and the “:param_name =>‘search’” option passes the user’s text in as the search parameter. Ryan’s solution also uses a view file called “index.js.erb” to return the list of completion options in the proper format – this is called by the index action when the categories controller receives the “/categories.js” request:
<%= auto_complete_result @categories, :name %>
By contrast, View Mapper’s call to text_field_with_auto_complete looks like this:
<%= text_field_with_auto_complete :product,
:category_name,
{},
{ :method => :get,
:url => '/auto_complete_for_category_name',
:param_name => 'category[name]' }
%>
This is very similar, but uses “category[name]” as the search parameter and sets the AJAX url to “auto_complete_for_office_code”, since this is what “auto_complete_for” expects.
Ryan’s approach is more elegant since it follows the REST model for the Ajax URL and controller code: the categories controller is used to handle category related requests, and its index action is used to return the list of category values. The scaffolding code View Mapper generates uses the auto_complete plugin the way it was originally intended with the “auto_complete_for” function, but is a bit ugly in that the products controller returns the category values, and uses a custom action method name instead of the normal “index” action. There’s no need for the index.js.erb file since auto_complete_for renders the response inline.
The advantage of the View Mapper approach is that there’s no need for the categories controller at all, and also you don’t need to code the “index” action or the index.js.erb view file yourself. If you plan to have a categories controller in your application anyway, you might want to change the text_field_with_auto_complete call to use that controller instead.
Detailed example
To make sure you can get a working example on your computer, let’s run through a step by step example:
$ rails sample_app
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
etc...
First, let’s create a model to represent our existing data called “Office” that will have two attributes: “display_name” and “code:”
$ cd sample_app/
$ ./script/generate model office code:string display_name:string
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/office.rb
create test/unit/office_test.rb
create test/fixtures/offices.yml
create db/migrate
create db/migrate/20100212193446_create_offices.rb
$ rake db:migrate
(in /Users/pat/rails-apps/sample_app)
== CreateOffices: migrating ==================================================
-- create_table(:offices)
-> 0.0031s
== CreateOffices: migrated (0.0034s) =========================================
And now let’s create a few sample office records:
$ ./script/console
Loading development environment (Rails 2.3.5)
>> Office.create :display_name => 'Boston', :code => 'BOS'
>> Office.create :display_name => 'Boise', :code => 'BOI'
>> Office.create :display_name => 'Barcelona', :code => 'BAR'
>> exit
Now you can install View Mapper – you’ll need version 0.3.3 for the “belongs_to_auto_complete” view:
$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources
$ sudo gem install view_mapper
Successfully installed view_mapper-0.3.3
1 gem installed
Installing ri documentation for view_mapper-0.3.3...
Installing RDoc documentation for view_mapper-0.3.3...
And now we can just run View Mapper’s “scaffold_for_view” generator to create the scaffolding code. Let’s try creating a new model called “Employee” that will belong to one of the existing offices:
$ ./script/generate scaffold_for_view employee name:string
--view belongs_to_auto_complete:office
error The auto_complete plugin does not appear to be installed.
$ ./script/plugin install git://github.com/rails/auto_complete.git
Initialized empty Git repository in /Users/pat/rails-apps/sample_app/vendor/plugins/auto_complete/.git/
remote: Counting objects: 13, done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 13 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (13/13), done.
From git://github.com/rails/auto_complete
* branch HEAD -> FETCH_HEAD
Trying again:
$ ./script/generate scaffold_for_view employee name:string
--view belongs_to_auto_complete:office
warning Model Office does not contain a has_many association for Employee.
Editing app/models/office.rb:
1 class Office < ActiveRecord::Base
2 has_many :employees
3 end
Trying a third time:
$ ./script/generate scaffold_for_view employee name:string
--view belongs_to_auto_complete:office
warning Model Office does not have a name attribute.
This time we get a warning that our existing model doesn’t have a “name” attribute (we chose “display_name” instead). To make this a bit more interesting, let’s use the “code” attribute for the auto_complete options. You can specify this to View Mapper with this syntax:
$ ./script/generate scaffold_for_view employee name:string
--view belongs_to_auto_complete:office[code]
exists app/models/
exists app/controllers/
exists app/helpers/
create app/views/employees
exists app/views/layouts/
exists test/functional
etc...
Now we just run rake db:migrate again, start our server and we’re done!
$ rake db:migrate
(in /Users/pat/rails-apps/sample_app)
== CreateEmployees: migrating ================================================
-- create_table(:employees)
-> 0.0034s
== CreateEmployees: migrated (0.0036s) =======================================
Tags:view_mapper
I decided it would be fun to look into various different types of Rails forms that allow you to create a new object that is associated with existing data. In my next few posts, I’ll explore different ways to select existing records, and also how to work with has_and_belongs_to_many and has_many, through relationships in a Rails form. It seems to me that these use cases are more common than the nested models form in the complex-form-examples sample app that creates new records but doesn't associate with existing ones.
To start with today, here’s the simplest such form I could think of – I call this a “belongs_to” form:
Here we can see a form for a new “shirt” record; along with the color and size the user can also select the person who owns the shirt. In this example, the shirt and person models have a has_many/belongs_to relationship. This form uses simple HTML <select> and <option> tags to display the list of people, and is generated by Rails ERB code that uses “collection_select” like this:
1 <p>
2 Person:<br />
3 <%= f.collection_select(:person_id,
4 Person.all,
5 :id,
6 :name,
7 { :prompt => true })
8 %>
9 </p>
The parameters passed to collection_select here indicate that we want to display all of the people that exist, and set the value and label for each <option> tag to the id and name of each person respectively. When the form is submitted, the “person_id” field for this shirt is set to the “id” value of the selected person.
Belongs_to scaffolding with View Mapper
If you need a form like this in your app, you can use my View Mapper gem to generate it for your models as follows… first install it from gemcutter; you’ll need version 0.3.2 at least for this form:
$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources
$ sudo gem install view_mapper
Successfully installed view_mapper-0.3.2
Then you can generate the belongs_to view scaffolding you see above like this:
$ ./script/generate view_for shirt --view belongs_to
View Mapper will open the specified model (“Shirt”), detect the associated model(s) that Shirt belongs to, and then generate the form using collection_select along with all of the other standard scaffolding files. You can also have View Mapper generate the new Shirt model and the view scaffolding code at the same time like this:
./script/generate scaffold_for_view shirt color:string size:integer
--view belongs_to:person
Here you’ve specified the desired attributes for the new shirt model, along with the fact that you want it to belong to a person.
To make the generated view code simple and concise, View Mapper makes a couple of assumptions about your models:
- It assumes the parent model (“Person” in this example) has an attribute or method called “name.” This is used to display the list of people.
- It also assumes the child model (“Shirt”) has a method to display the name of the parent model it belongs to (“person_name” in this example).
I decided not to make the View Mapper command line more complex than it already is by providing a way to pass the “name” attribute as another parameter. Instead you can just edit the scaffolding code it produces as desired.
Detailed example and code review
Let’s create a sample app now together using View Mapper, and then review how this “belongs_to view” works. First, create a new Rails app:
$ rails belongs_to
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
create config/environments
etc...
And now let’s create the Person model with first and last name attributes; I’ll also use the console to create a few Person records so we have some existing data to work with:
$ cd belongs_to
$ ./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/20100124120247_create_people.rb
$ rake db:migrate
(in /Users/pat/rails-apps/belongs_to)
== CreatePeople: migrating ===================================================
-- create_table(:people)
-> 0.0030s
== CreatePeople: migrated (0.0032s) ==========================================
$ ./script/console
Loading development environment (Rails 2.3.5)
>> Person.create :first_name => 'Barack', :last_name => 'Obama'
>> Person.create :first_name => 'George', :last_name => 'Bush'
>> Person.create :first_name => 'Bill', :last_name => 'Clinton'
Now let’s go ahead and run View Mapper to create the form…
$ ./script/generate scaffold_for_view shirt color:string size:integer
--view belongs_to:person
warning Model Person does not contain a has_many association for Shirt.
Here View Mapper is warning us that we haven’t called “has_many :shirts” in the Person model yet; let’s edit person.rb and enter that code:
1 class Person < ActiveRecord::Base
2 has_many :shirts
3 end
Now we can try again:
$ ./script/generate scaffold_for_view shirt color:string size:integer
--view belongs_to:person
warning Model Person does not have a name attribute.
Above I mentioned that View Mapper assumes the parent model has a “name” attribute or method. This is required to know how to display each person record in the <select> drop down box on the form. Remember in the call to collection_select we passed in the symbol “:name” – this tells Rails to call the name method for the label of each <option> tag. So let’s create a name method in the Person model that displays the first and last name attributes together:
1 class Person < ActiveRecord::Base
2 has_many :shirts
3 def name
4 "#{first_name} #{last_name}"
5 end
6 end
The highlighted code returns the first and last names concatenated together as a single name. Now View Mapper won’t complain and we can go ahead and create our new form:
$ ./script/generate scaffold_for_view shirt color:string size:integer
--view belongs_to:person
exists app/models/
exists app/controllers/
… etc …
create app/views/shirts/_form.html.erb
… etc …
exists test/fixtures/
create app/models/shirt.rb
create db/migrate/20100124121032_create_shirts.rb
Note the output here looks just like what you get from the standard Rails scaffold generator, except for the one additional line I highlighted above. If you run your app now, you’ll be able to see scaffolding for the Shirts model at http://localhost:3000/shirts, and you’ll see the person select box on the new and edit forms.
Let me take a few more minutes to point out a couple of interesting details about the belongs_to scaffolding… first I’ve moved the form fields into a partial shared among the new and edit views; this is the new _form.html.erb file highlighted above in the generator output. If you look at new.html.erb, you’ll see a call to render :partial:
1 <h1>New shirt</h1>
2
3 <% form_for(@shirt) do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <p>
6 <%= f.submit 'Create' %>
7 </p>
8 <% end %>
9
10 <%= link_to 'Back', shirts_path %>
The edit.html.erb file looks similar. The actual call to collection_select is in _form.html.erb; that way if you need to display the list of people differently, for example to use a method other than “name” for each person or possibly to use a filtered list of people instead of Person.all, then you just need to make your changes in one place.
And one more interesting detail: if you open up the new Shirts model that View Mapper generated you’ll see this:
1 class Shirt < ActiveRecord::Base
2 belongs_to :person
3 def person_name
4 person.name if person
5 end
6 end
The person_name method I highlighted above returns the person each shirt belongs to; it also checks if person is nil for that shirt. This simplifies the code in index.html.erb and show.html.erb; take a look at show.html.erb for example:
1 <p>
2 <b>Color:</b>
3 <%=h @shirt.color %>
4 </p>
5
6 <p>
7 <b>Size:</b>
8 <%=h @shirt.size %>
9 </p>
10
11 <p>
12 <b>Person:</b>
13 <%=h @shirt.person_name %>
14 </p>
15
16 <%= link_to 'Edit', edit_shirt_path(@shirt) %> |
17 <%= link_to 'Back', shirts_path %>
Here the Person field looks just like any other field from the Shirt model. There’s no need to repeat the check for person == nil in the view, and if you ever need to use a Person attribute other than name or if you needed to find the associated person in some more complex way, you’ll only need to make the change to the model and not in each view file.
Again, if you run View Mapper on two has_many/belongs_to models that you’ve already written in your app, you will first need to provide:
- A “name” method in the parent model, if it’s not already an attribute, and
- A “[parent_model]_name” method in the child model
If these two methods don’t exist, View Mapper will display warning messages and not proceed; this avoids the confusion you would run into when the scaffolding view code didn’t work.
Next time, I’ll repeat this example but use type ahead/auto_complete to select the person instead...
Tags:view_mapper
The complex-form-examples sample application written by Ryan Bates and then updated by Eloy Duran and many others is the standard example of how to implement a complex form in Rails. It shows how you can create and update more than one model using the same form. Last month, I wrote about how to create scaffolding for a complex form; using my View Mapper gem you can create a simplified version of the sample app right inside your application for your models.
Today I’d like to take some time to explain how my simplified version of the complex form actually works – the key to using scaffolding in your Rails application is understanding how it works so you can eventually modify and adapt it for you needs, and discard the code you don’t need. However, since this sample application is fairly complex I decided it would be more interesting and fun to follow a single code path through the app in a series of small steps, seeing how each small piece works in detail. To do this, I wrote a series of short blog pages or slides; to see it, click the “Follow Code Path” link… once you’re on the first page you’ll see links to move forward and backward through the slides.
Follow Code Path
Let me know here if you have any feedback on either the content or the style of the code path pages since they are just hard coded HTML for now and not part of my blog. Thanks!
Tags:view_mapper
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.
Tags:view_mapper
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:view_mapper
I just updated the View Mapper gem to support Paperclip. You can use it to generate scaffolding code that supports uploading and downloading Paperclip file attachments.
Creating a view for an existing model
If you have a model like this:
class Song < ActiveRecord::Base
has_attached_file :mp3
end
… you can generate a “Paperclip view” for this model like this:
script/generate view_for song --view paperclip
This will generate a controller, view and other code files that support uploading and downloading files. If you run your app you’ll see the typical scaffolding user interface but with a file field for the “mp3” Paperclip attachment:

View Mapper has:
- inspected your model (“Song” in this example) and found its Paperclip attachments and other standard ActiveRecord attributes
- called the Rails scaffold generator and passed in the ActiveRecord columns it found
- added additional view code to support Paperclip (e.g. set the form to use multipart/form-data encoding)
- created a file field in the form for each Paperclip attachment (“mp3” in this example), as well as a link to each attachment in the show view code file.
If you’re not very familiar with Paperclip and how to use it or if you just want to get a Rails upload form working very quickly, then View Mapper can help you.
Creating an entirely new Paperclip model and view
View Mapper also provides a generator called “scaffold_for_view” that is identical to the standard Rails scaffold generator, except it will create the specified view. As an example, let’s create a new Rails app from scratch that uses Paperclip; you should be able to type in these precise commands on your machine and get this example to work.
First, let’s install View Mapper and create a new Rails app to display my MP3 library online (ignoring copyright issues for now):
$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources
$ sudo gem install view_mapper
Successfully installed view_mapper-0.2.0
1 gem installed
Installing ri documentation for view_mapper-0.2.0...
Installing RDoc documentation for view_mapper-0.2.0...
$ rails music
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
etc…
And now we can generate a new “Song” model that has a Paperclip attachment called “MP3” using View Mapper like this:
$ cd music
$ ./script/generate scaffold_for_view song name:string artist:string
album:string play_count:integer --view paperclip:mp3
error The Paperclip plugin does not appear to be installed.
Wait… I forgot to install Paperclip; let’s do that and then try again:
$ ./script/plugin install git://github.com/thoughtbot/paperclip.git
Initialized empty Git repository in /Users/pat/rails-apps/music/vendor/plugins/paperclip/.git/
remote: Counting objects: 71, done.
remote: Compressing objects: 100% (59/59), done.
remote: Total 71 (delta 7), reused 29 (delta 3)
Unpacking objects: 100% (71/71), done.
From git://github.com/thoughtbot/paperclip
* branch HEAD -> FETCH_HEAD
$ ./script/generate scaffold_for_view song name:string artist:string
album:string play_count:integer --view paperclip:mp3
exists app/models/
exists app/controllers/
exists app/helpers/
create app/views/songs
exists app/views/layouts/
exists test/functional/
etc…
Finally, we just need to run db:migrate – one minor detail here is that the scaffold_for_view generator included the Paperclip columns (“mp3_file_name,” “mp3_content_type,” etc…) in the migration file to create the songs table:
$ rake db:migrate
(in /Users/pat/rails-apps/music)
== CreateSongs: migrating ====================================================
-- create_table(:songs)
-> 0.0022s
== CreateSongs: migrated (0.0024s) ===========================================
Now you can run your app and see the scaffolding UI I showed above, and will be able to upload and download MP3 files using Paperclip.
Let’s take a quick look at exactly what is different about the scaffolding code View Mapper generated vs. the standard Rails scaffolding code:
<h1>New song</h1>
<% form_for(@song, :html => { :multipart => true }) do |f| %>
<%= f.error_messages %>
<p>
<%= f.label :name %><br />
<%= f.text_field :name %>
</p>
… etc …
<p>
<%= f.label :mp3 %><br />
<%= f.file_field :mp3 %>
</p>
<p>
<%= f.submit 'Create' %>
</p>
<% end %>
<%= link_to 'Back', songs_path %>
The code in bold was generated by View Mapper specifically to support Paperclip since we used the “--view paperclip” command line option. You can see that “:html => { :multipart => :true }” was added to form_for to allow for file uploads, and also a file_field was added for the mp3 Paperclip attachment.
If you take a look at the show view, you’ll see:
<p>
<b>Name:</b>
<%=h @song.name %>
</p>
… etc …
<p>
<b>Mp3:</b>
<%= link_to @song.mp3_file_name, @song.mp3.url %><br>
</p>
<%= link_to 'Edit', edit_song_path(@song) %> |
<%= link_to 'Back', songs_path %>
Here a link to the file attachment was added, using Paperclip to provide the name and URL of the attachment.
Next I’ll be adding support for nested attributes and complex forms to View Mapper.
Tags:view_mapper
View Mapper will generate scaffolding illustrating how to write view code using a specified plugin or feature with your existing models. It can also generate new models.
A couple simple examples:
script/generate view_for office --view auto_complete:address
… will generate Rails scaffolding code that displays a form for an existing “office” model, with auto complete on the “address” field.
script/generate scaffold_for_view office address:string code:string
--view auto_complete:address
… will generate the same form, but also create the “office” model class file, a migration file containing the “address” and “code” columns, and other standard scaffolding files as well.
The idea behind View Mapper is that it’s easy to write simple, concise model classes representing your domain objects using ActiveRecord, but very hard to implement the corresponding views using a combination of HTML, Javascript Rails helper functions, routes, controllers, etc. If you’re not very familiar with a certain plugin you want to use in your app, View Mapper can help you get started in the right direction by generating a working example with scaffolding code.
If you’re developing a Rails plugin or gem it’s easy to write your own View Mapper module for your plugin’s users to call with View Mapper.
Code: http://github.com/patshaughnessy/view_mapper
Install:
sudo gem install view_mapper
Usage:
Two generators are provided, called view_for and scaffold_for_view:
script/generate view_for model [ --view view_name:param ]
This will generate the specified view code for an existing model. The view_for generator will look for your model, inspect all of its columns and then generate standard Rails scaffolding containing a form field for each existing column.
If you also specify a view, then a custom view will be created using the specified Rails feature or plugin, using the specified parameter.
script/generate scaffold_for_view model attributes [ --view view_name:param ]
If you don’t specify a view, then this command is identical to the standard Rails scaffold generator.
If you do specify a view, then the entire working set of a model, views and controller will be generated to implement the specified Rails feature or plugin, using the specified parameter.
Views:
Right now, I’ve implemented eight views:
- auto_complete: Uses the standard Rails auto_complete plugin to implement type ahead behavior for the specified field.
- paperclip: Uses the Paperclip plugin to upload and download file attachments.
- has_many: Displays a complex form to edit two or more associated models.
- has_many_auto_complete: This is the same as has_many but also uses the auto_complete plugin to implement type ahead behavior for each text field. This view requires you to install my fork of the Rails auto_complete plugin.
- belongs_to: Generates scaffolding that allows you to select an existing, associated model.
- belongs_to_auto_complete: Generates scaffolding that allows you to select an existing, associated model using auto_complete.
- has_many_existing: Generates scaffolding for a complex form to edit two models that have a has_many, :through association with a third model. Use this if you have a many-many relationship with existing data.
- (Default) If no view is specified, then standard Rails scaffold code will be generated.
I’ll be implementing more views in the coming weeks and months. There is also an API for implementing your own View Mapper module, for example to generate code illustrating how to use a plugin or gem you are working on. In the future I’ll document this as well.
Tags:view_mapper
I’ve written a lot here about the Rails auto_complete plugin; I’ve also refactored the auto_complete plugin to support repeated fields and named scopes. Today I’d like to show how you can automatically generate Rails view and controller code with auto_complete behavior for one of your models using a new gem I’ve written called View Mapper. If you’ve never used the auto_complete plugin before this is a great way to learn quickly how to use it in your app; even if you are familiar with the plugin using scaffolding like this can help to get a working auto_complete form up and running quickly and let you concentrate on more important parts of your app.
Let’s say you have an existing model in your app called “Person:”
Class Person < ActiveRecord::Base
end
And suppose the Person model has two string attributes for the person’s name and the name of the office they work in:
class CreatePeople < ActiveRecord::Migration
def self.up
create_table :people do |t|
t.string :name
t.string :office
etc…
Now let’s install View Mapper so we can generate an auto_complete view for our person model. Since I’ve only deployed view_mapper on gemcutter.org for now, you’ll also need to add gemcutter as a gem source if you haven’t already.
$ gem sources -a http://gemcutter.org
http://gemcutter.org added to sources
$ sudo gem install view_mapper
Successfully installed view_mapper-0.1.0
1 gem installed
Installing ri documentation for view_mapper-0.1.0...
Installing RDoc documentation for view_mapper-0.1.0...
Now with view_mapper you can run a single command to generate scaffolding that displays the your existing person model’s fields in a form with auto_complete type ahead behavior for the office field:
$ ./script/generate view_for person --view auto_complete:office
exists app/controllers/
exists app/helpers/
create app/views/people
exists app/views/layouts/
exists test/functional/
exists test/unit/
create test/unit/helpers/
exists public/stylesheets/
create app/views/people/index.html.erb
create app/views/people/show.html.erb
create app/views/people/new.html.erb
create app/views/people/edit.html.erb
create app/views/layouts/people.html.erb
create public/stylesheets/scaffold.css
create app/controllers/people_controller.rb
create test/functional/people_controller_test.rb
create app/helpers/people_helper.rb
create test/unit/helpers/people_helper_test.rb
route map.resources :people
route map.connect 'auto_complete_for_person_office',
:controller => 'people',
:action => 'auto_complete_for_person_office'
This works just like the Rails scaffold generator, except that the view_for generator has also:
- inspected your person model and found the name and office columns.
- added a route for “auto_complete_for_person_office” to routes.rb.
- added a call to “auto_complete_for :person, :office” to PersonController.
- used text_field_for_auto_complete on the office field in your new and edit forms.
- inserted “javascript_include_tag :defaults” into views/layouts/people.html.erb to load the prototype javascript library.
If you start up your application and create a few person records with names and addresses, then you will see the auto_complete plugin working!

With this working example right inside your application, you can easily review exactly how the view, route and controller files use auto_complete. After that you can adapt the view to fit into your application’s design and delete the scaffolding you don’t really need or want.
As another example, let’s create an entirely new Rails application completely from scratch, and use View Mapper to setup auto_complete inside it:
$ rails auto_complete_example
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
etc…
$ cd auto_complete_example
First, let’s install the auto_complete plugin. (In a future post I’ll show how to use View Mapper with my fork of auto_complete in a complex form.)
$ ./script/plugin install git://github.com/rails/auto_complete.git
Initialized empty Git repository in /Users/pat/rails-apps/auto_complete_example/vendor/plugins/auto_complete/.git/
remote: Counting objects: 13, done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 13 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (13/13), done.
From git://github.com/rails/auto_complete
* branch HEAD -> FETCH_HEAD
Now that we have the plugin installed, let’s create our scaffolding. Along with the
“view_for” generator I used above, View Mapper also provides a generator called “scaffold_for_view.” This works the same way, except it just creates a new model the same way the Rails scaffold generator does, instead of inspecting an existing model.
Let’s create the same person model we used above, and an auto_complete view:
$ ./script/generate scaffold_for_view person name:string office:string
--view auto_complete:office
exists app/models/
exists app/controllers/
exists app/helpers/
create app/views/people
exists app/views/layouts/
exists test/functional/
exists test/unit/
create test/unit/helpers/
exists public/stylesheets/
create app/views/people/index.html.erb
create app/views/people/show.html.erb
create app/views/people/new.html.erb
create app/views/people/edit.html.erb
create app/views/layouts/people.html.erb
create public/stylesheets/scaffold.css
create app/controllers/people_controller.rb
create test/functional/people_controller_test.rb
create app/helpers/people_helper.rb
create test/unit/helpers/people_helper_test.rb
route map.resources :people
dependency model
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/20091001161349_create_people.rb
route map.connect 'auto_complete_for_person_office',
:controller => 'people',
:action =>'auto_complete_for_person_office'
Note the syntax is the same as the standard Rails scaffold generator, except I’ve added the “view” parameter to specify we want the auto_complete plugin to be used in the view.
Now we just need to migrate the database schema and run our app:
$ rake db:migrate
(in /Users/pat/rails-apps/auto_complete_example)
== CreatePeople: migrating ===================================================
-- create_table(:people)
-> 0.0012s
== CreatePeople: migrated (0.0015s) ==========================================
And if you add a few records, you’ll see auto_complete working!
I’ll be adding more views to View Mapper soon, and in future posts here I’ll write about how to generate scaffolding for Paperclip file attachments, my version of auto_complete used on a complex form, and also how to write your own views for View Mapper.
Tags:view_mapper
(Updated October 2009)
I just rewrote and cleaned up the code I describe below here and published it as a new gem called View Mapper on gemcutter.org. See my usage article for more details. I also rethought and redesigned the commands I describe below… the part about nested attributes will not work for now. I’m planning to reimplement that soon.
I’ve been thinking for a while that a generator that creates view scaffolding for an existing model or models would be really useful. For example, I want to be able to write this by hand:
class Group < ActiveRecord::Base
has_many :people
end
class Person < ActiveRecord::Base
belongs_to :group
end
… and then run a generator and get a working view to edit and display groups of people. Recently I started developing a generator called “ViewMapper” that does this; see: http://github.com/patshaughnessy/view_mapper.
The idea is that it creates a view that maps to your existing models. Writing model classes is often very easy; using ActiveRecord allows you to write concise, short code files. However, writing a view that displays a form for these models is often very hard, requiring knowledge of a long list of Rails functions, macros, classes, as well as the usual HTML and Javascript. Right now ViewMapper it just a plugin; so you can install it into your app like this:
$ ./script/plugin install git://github.com/patshaughnessy/view_mapper.git
Probably it should be packaged as a gem instead… this is still work-in-progress. At the moment it supports:
- Creating simple form based on the columns of an existing model
- Creating a complex form based on the columns of two associated models in a many-one relationship, using nested attributes
If you’re interested and have time, try the examples below or better yet try the generator on some of your own models and let me know what you think.
Example 1: Create a view based on the columns of an existing model
Let’s say I have an existing model class that manages a series of person records:
class Person < ActiveRecord::Base
end
Now to create the corresponding view with ViewMapper, just run this command:
$ ./script/generate view_for person
exists app/controllers/
exists app/helpers/
create app/views/people
exists app/views/layouts/
exists test/functional/
create test/unit/helpers/
exists public/stylesheets/
create app/views/people/index.html.erb
create app/views/people/show.html.erb
create app/views/people/new.html.erb
create app/views/people/edit.html.erb
create app/views/layouts/people.html.erb
create public/stylesheets/scaffold.css
create app/controllers/people_controller.rb
create test/functional/people_controller_test.rb
create app/helpers/people_helper.rb
create test/unit/helpers/people_helper_test.rb
route map.resources :people
If you run your Rails app you will see the usual scaffolding UI:
This looks just like the scaffolding page you would have gotten from the Rails scaffolding generator. In fact, I’ve based the “view_for” generator class (ViewForGenerator) on the existing Rails ScaffoldGenerator class, so it generates the same code.
The difference here is that the scaffolding code was based on the properties of the existing Person model. Here’s what the view_for generator did:
- Find the specified model class
- Inspect it and get a list of column names and data types
- Create the scaffolding code as usual
If you also want to create your model class at the same time, then you don’t need ViewMapper; you would just run the standard Rails scaffold generator like this:
$ ./script/generate scaffold person name:string age:integer
ViewMapper provides you with the flexibility to create the view scaffolding after the fact for a model you or someone else has already created in your app.
Example 2: Create a view using a complex form based on two associated models in a many-one relationship, using Rails 2.3 nested attributes
Let’s suppose you have two related models that describe groups of people:
class Group < ActiveRecord::Base
has_many :people
end
class Person < ActiveRecord::Base
belongs_to :group
end
Again, writing model classes is really easy. With just a few lines of code I have told ActiveRecord to manage two separate tables and their relationship.
Now you can use ViewMapper to create a complex form for creating and editing groups and people at the same time like this:
$ ./script/generate nested_attributes_view_for group containing:people
error Model 'Group' does not accept nested attributes
for child models 'people'
This error indicates that my Group model is missing the “accepts_nested_attributes_for” directive. Since the generator creates view code that relies on the nested attributes feature, it won’t let you create the view for a model that is missing this line. This sort of helpful error message is possible since ViewMapper is inspecting an existing model class, and not just creating new scaffold code.
Now if we add the missing line (suddenly my model is less concise... nested attributes aren't that easy to use yet!):
class Group < ActiveRecord::Base
has_many :people
accepts_nested_attributes_for :people,
:allow_destroy => true,
:reject_if => proc { |attrs| attrs['name'].blank? && attrs['age'].blank? }
end
…we can re-run the generator:
$ ./script/generate nested_attributes_view_for group containing:people
exists app/controllers/
exists app/helpers/
create app/views/groups
exists app/views/layouts/
exists test/functional/
create test/unit/helpers/
exists public/stylesheets/
create app/views/groups/index.html.erb
create app/views/groups/show.html.erb
create app/views/groups/new.html.erb
create app/views/groups/edit.html.erb
create app/views/layouts/groups.html.erb
create public/stylesheets/scaffold.css
create app/controllers/groups_controller.rb
create test/functional/groups_controller_test.rb
create app/helpers/groups_helper.rb
create test/unit/helpers/groups_helper_test.rb
route map.resources :groups
And now if we run our app, we get view scaffolding illustrating how to create and edit people and groups all at the same time:
If I save the group…
And re-editing the same group:
More to come… as I said this is work-in-progress. As a next step I’m planning to improve the scaffolding code for nested attributes by adding javascript to dynamically add/delete the child objects. But after that I'm thinking about:
- Adding a “paperclip_view_for” generator that creates a file upload form for a model that contains a “has_attached_file” directive.
- Adding a “auto_complete_view_for” generator that adds “auto_complete_for” to the controller and uses “text_field_for_auto_complete” in the form.
- Adding the ability for you to extend this with your own scaffolding code.
Tags:view_mapper