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