Creating associations to existing data part 2: belongs_to with auto_complete

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) =======================================