Generating view scaffolding code for existing models

(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.