Pat Shaughnessy

Ribadesella, Spain

Getting started with Ruby metaprogramming

February 20, 2010 · 2 comments

The Rails auto_complete plugin was my first exposure to Ruby metaprogramming. It’s code was simple enough for a Rails beginner like me to understand, but also just complex enough for me to learn something new. Specifically, I ran into metaprogramming when I took a close look at the “auto_complete_for” method and tried to figure out how it worked. I won’t spend any time here explaining what the auto_complete plugin does or what it’s used for, beyond to say that if you add this line to one of your controllers:

class CategoriesController < ApplicationController

  auto_complete_for :category, :name

  etc…


… a method called “auto_complete_for_category_name” will be automatically generated in that controller that will return a list of category records that have a name matching a given search query. This is very cool, and is a typical example of Ruby on Rails magic: you add one line to a class in your application and suddenly an entire feature or behavior is added, customized to the data and objects in your app!

This sort of thing is really what makes Ruby on Rails so amazing… but how does it work? Let’s take a look at the implementation of auto_complete_for method:

def auto_complete_for(object, method, options = {})
  define_method("auto_complete_for_#{object}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[object][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = object.to_s.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

You can find this code in the lib/auto_complete.rb file inside the rails/auto_complete repository on github. So what the heck does all of this mean? Let’s take a step-by-step look at this code, and see if we can figure it out.

To get started, let’s use the category/name example I used in my last article, and also that Ryan Bates used in his Auto-Complete Association screencast on the auto_complete plugin:

auto_complete_for :category, :name


Here we are passing “:category” into auto_complete_for as the value for “object,” and “:name” as the value for “method.” Now let’s repeat the auto_complete_for code, but substitute object with :category:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = :category.to_s.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

In the code snippet above I’ve highlighted the places where the symbol :category appears. You can see that it’s used in a few different places, but the most important line is near the bottom: @items = :category.to_s.camelize.constantize… etc. Let’s evaluate each of the method calls on this one line, one at a time. The first call is “:category.to_s”. The “to_s” method name means “to string” and will convert the target object (the object you call to_s on) to a string. This means that the :category symbol will be converted to a string. So now let’s display the string ‘category’ in place and see what we are left with:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = 'category'.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

You can see ‘category’ highlighted above where I’ve evaluated the to_s method. Now the next method call is “camelize” – what does this mean? The camelize method is one of a series of functions that Rails provides in the “ActiveSupport” gem, one of the components of the Rails framework. It converts the given string to camel case, for example “office_code” to “OfficeCode.” Since Ruby class names are written using camel case, this is often very useful for obtaining a class name from a string. In our example, the string “category” is converted into “Category” with an upper case “C” …

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = 'Category'.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

Now let’s take a look at the next method call on that same line of code: “constantize.” This converts a string into an actual Ruby constant, and returns an error if that constant doesn’t exist. In our example, the string “Category” is converted into the Ruby class Category:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

Now we can see that the complex line above evaluates to a simple ActiveRecord find call. The power of Ruby metaprogramming has enabled us to write a single helper function “auto_complete_for” that takes any symbol or string as an argument (e.g. :category), and performs a SQL query on the corresponding ActiveRecord class. The amazing part of this for me is how flexible and easy to use Ruby is: helper methods like camelize and constantize make it very easy to extract common bits of code you might write over and over again in your app, and generalize them into a single method that will apply to any target class. This would be possible in any programming language but it’s amazing just how easy it is to do with Ruby.

Let’s continue to simplify the auto_complete_for code by substituting a value for the “method” parameter – in our example method will become the symbol “:name” :

def auto_complete_for(:category, :name, options = {})
  define_method("auto_complete_for_#{:category}_#{:name}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{:name}) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "#{:name} ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{:name}' %>"
  end
end

Again you can see that the :name symbol is used in a few different places. Most commonly here the symbol is inserted in a string using this syntax: “#{:name}”. This is the standard Ruby #{} string interpolation operator, which is also implicitly converting the symbol into a string before inserting it into the surrounding string value, just as if we called to_s as above. So let’s replace “#{:name}” and “#{:category}” with the strings “name” and “category,” and then also insert them into the surrounding string values:

def auto_complete_for(:category, :name, options = {})
  define_method("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

And now let’s replace the last parameter to auto_complete_for “options” with it’s default value since we aren’t providing that in our auto_complete_for :category, :name call:

def auto_complete_for(:category, :name, {})
  define_me thod("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }.merge!({})

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

You can see that options was used in only one place: …merge!(options). Using merge with hashes is another extremely common technique in Ruby coding; it adds the key/value pairs from the hash you provide as a parameter to the hash you call merge on. Merge! is a variation on this which directly modifies the target object, as opposed to only returning the merged hash. Merge is very useful for metaprogramming because, as in this example, it makes it easy to allow the user/client code of a method to customize a hash of options that is passed into some other function. Here merge was used to allow the caller of auto_complete_for to pass in additional find options to the Category.find call. In our case since we didn’t pass in a value for options, it is set to {} and then has no effect on the find_options hash. So let’s just remove the call to merge!({}), which is a NOP anyway:

def auto_complete_for(:category, :name, {})
  define_method("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

Now the code is looking simpler and simpler. Let’s finish this example by evaluating the “define_method” call at the top. As you might guess, define_method is how you can dynamically create a new method in a class in Ruby. In auto_complete_for it’s used to add a new method to the controller in which you included the call to auto_complete_for, called “auto_complete_for_category_name.” In other words, adding the auto_complete_for :category, :name line to the CategoriesController was equivalent to adding this method definition to the class:

def auto_complete_for_category_name
  find_options = { 
    :conditions => [
                     "LOWER(name) LIKE ?",
                     '%' + params[:category][:name].downcase + '%'
                   ],
    :order => "name ASC",
    :limit => 10 }

  @items = Category.find(:all, find_options)

  render :inline => "<%= auto_complete_result @items, 'name' %>"
end

Now we’ve seen the real value of metaprogramming: the author of the auto_complete plugin was able to provide a simple helper method, auto_complete_for, which dynamically added this fairly complex method to your controller. The beauty and power of Ruby and Rails is just how easy this was to do: the generated function is tailored to use the Category model just as if you had written it yourself. When I first came across this code I was impressed not only by the power that this sort of metaprogramming provided, but also by how easy it was to do this using Ruby on Rails. Helper modules and methods, such as constantize in ActiveSupport, make it very, very easy to convert from symbols to strings to constants and back again… which is exactly what you need to do to write a general method that can be dynamically generated in this way.

Last week I asserted that calling auto_complete_for in your controller like this was equivalent to this code from Ryan Bates’ Auto-Complete Association screencast:

class CategoriesController < ApplicationController

  def index
    @categories =
      Category.find(:all, :conditions => ['name LIKE ?', "%#{params[:search]}%"])
  end

etc...


Now you can see how this is true: the code Ryan wrote was a simple call to Category.find, with conditions that search for a name like the name provided in the “search” parameter. Looking at the simplified auto_complete_for_category_name method above, you can see that it calls Category.find in the same way. However, the auto_complete_for version is a bit different in that it:

  • Looks for a parameter called “category[name]” instead of “search,” and
  • Converts it to lower case, and
  • Passes a couple of other SQL options into Category.find: order by name ascending, and limit 10, and
  • Makes a call to render :inline to return the result to the browser without the need for a normal view code file.

Auto_complete_for does essentially the same thing that Ryan explained in his screen cast, but the elegance of Ruby metaprogramming allows the users of the auto_complete plugin to implement this search feature without writing any code at all.

2 comments Tags:

Creating associations to existing data part 2: belongs_to with auto_complete

February 13, 2010 · 3 comments

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

3 comments Tags:

How does the complex-form-examples sample app work?

December 30, 2009 · 2 comments

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!

2 comments Tags:

Scaffolding for auto complete on a complex, nested form

November 25, 2009 · 24 comments

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

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

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

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

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

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

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

 

Detailed Example

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

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

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

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

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

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

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

$ cd complex_auto_complete/
$ ./script/generate model person name:string age:integer group_id:integer
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/person.rb
      create  test/unit/person_test.rb
      create  test/fixtures/people.yml
      create  db/migrate
      create  db/migrate/20091125195040_create_people.rb

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

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

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

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

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

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

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

… and then re-run View Mapper:

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

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

class Person < ActiveRecord::Base
  belongs_to :group
end

And now I can run View Mapper once more:

$ ./script/generate scaffold_for_view group name:string
                    --view has_many_auto_complete:people
      exists  app/models/
…etc…
      create  app/models/group.rb
      create  test/unit/group_test.rb
      create  test/fixtures/groups.yml
      exists  db/migrate
      create  db/migrate/20091125195715_create_groups.rb
      create  app/views/groups/show.html.erb
      create  app/views/groups/_form.html.erb
      create  app/views/groups/_person.html.erb
      create  public/javascripts/nested_attributes.js
       route  map.connect 'auto_complete_for_group_name',
                          :controller => 'groups',
                          :action => 'auto_complete_for_group_name'
       route  map.connect 'auto_complete_for_person_name',
                          :controller => 'groups',
                          :action => 'auto_complete_for_person_name'
       route  map.connect 'auto_complete_for_person_age',
                          :controller => 'groups',
                          :action => 'auto_complete_for_person_age'

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

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

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


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

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

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

24 comments Tags:

Auto_complete scaffolding

October 01, 2009 · 0 comments

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.

0 comments Tags:

Filtering auto_complete pick lists – part 2: using named scopes

April 02, 2009 · 1 comment

I just updated my customized version of the auto_complete plugin to allow you to provide a named scope to auto_complete_for, in order to filter the auto_complete pick list options differently than the plugin does by default. The updated code is now on github:

http://github.com/patshaughnessy/auto_complete

This is based on the ideas from my last post, Andrew Ng’s original post and my friend Alex’s suggestion to use named scopes instead of manually modifying the find options. Here’s an example of how to use it taken from the auto_complete sample app I posted in January:

  1. Add a named scope to your target model: For example suppose tasks belong to projects and have a named scope “by_project” which joins on the projects table and returns the tasks belonging to the project with the given name:
    class Task < ActiveRecord::Base
      belongs_to :project
      named_scope :by_project,
        lambda { |project_name| {
          :include => :project,
          :conditions => [ "projects.name = ?", project_name ]
        } }
    end
  2. In the controller, pass a block to auto_complete_for to specify that a named scope should be used to generate the competion options. Here the “by_project” named scope will be used to handle the task auto complete requests, using the “project” HTTP parameter:
    auto_complete_for :task, :name do | items, params |
      items.by_project(params['project'])
    end
  3. In the view, optionally specify additional parameters you might want to pass into your named scope: in my sample app I have a field called “project_name” elsewhere on my form:
    <% fields_for_task task do |f| -%>
      …
      <%= f.text_field_with_auto_complete :task,
            :name,
            {},
            {
              :method => :get,
              :with =>"value + '&project=' + $F('project_name')"
            } %>
      …
      <% end -%>

So how does this work? Let’s take a look at my new implementation of auto_complete_for:

def auto_complete_for(object, method, options = {})
  define_method("auto_complete_for_#{object}_#{method}") do
    model = object.to_s.camelize.constantize
    find_options = { 
      :conditions => [ "LOWER(#{model.quoted_table_name}.#{method}) LIKE ?",
        '%' + params[object][method].downcase + '%' ], 
      :order => "#{model.quoted_table_name}.#{method} ASC",
      :limit => 10 }.merge!(options)
    @items = model.scoped(find_options)
    @items = yield(@items, params) if block_given?
    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

One minor change I made here was to call “quoted_table_name” on the given model to specify the table name in the SQL generated to retrieve the auto complete results later. This was needed in case, like in my sample application, the controller specifies a named scope that joins with another table containing columns with the same name as the target model. If this isn’t the case, adding the table name to the SQL is harmless.

However, the most important 2 lines here are in bold: first we call a function called “scoped” to create an anonymous named scope based on the default auto_complete options “find_options:”

@items = model.scoped(find_options)

The exciting thing about this line, which Alex explained in his blog post, is that the use of named scopes delays the corresponding SQL statement from being executed until later when we actually access the query results in auto_complete_result. What happens instead is that an ActiveRecord:: NamedScope::Scope object is created, containing a temporary cache of the find options.

A good way to understand how this works is to try it in the Rails console:

complex-form-examples pat$ ./script/console 
Loading development environment (Rails 2.1.0)
>> find_options = { 
?>   :conditions => [ "LOWER(`tasks`.name) LIKE ?", '%t%' ], 
?>   :order => "`tasks`.name ASC",
?>   :limit => 10 }
=> {:order=>"name ASC", :conditions=>["LOWER(name) LIKE ?", "%t%"], :limit=>10}
>> Task.scoped(find_options)
=> [#<Task id: 4, project_id: 2, name: "Task 2a", due_at: nil, created_at: "2009-04-02 16:21:54",
updated_at: "2009-04-02 16:21:54">, #<Task id: 5, project_id: 2, name: "Task 2b", due_at: nil,
created_at: "2009-04-02 16:21:54", updated_at: "2009-04-02 16:21:54">, #<Task id: 6, project_id: 2,
name: "Task 2c", due_at: nil, created_at: "2009-04-02 16:21:54", updated_at: "2009-04-02
16:21:54">, #<Task id: 1, project_id: 1, name: "Task One", due_at: nil, created_at: "2009-04-02
16:21:30", updated_at: "2009-04-02 16:21:30">, #<Task id: 3, project_id: 1, name: "Task
Three", due_at: nil, created_at: "2009-04-02 16:21:30", updated_at: "2009-04-02 16:21:30">,
#<Task id: 2, project_id: 1, name: "Task Two", due_at: nil, created_at: "2009-04-02 16:21:30",
updated_at: "2009-04-02 16:21:30">]

Wait a minute! I thought the actual SQL execution was delayed by named scopes until I needed to access the results? Here the console has already displayed the query results, so the SQL statement must have been executed already. How and why did this happen? In this case, when you enter an expression into the Rails console and press ENTER, the expression is evaluated and then the “inspect” method is called on it. The problem is that the named scopes implementation has delegated the “inspect” method to another method, which executes the SQL statement and loads the query results.

We can use a trick in the console to open the ActiveRecord::NamedScope::Scope class and override the inspect method so the SQL is not executed, and prove to ourselves that “scoped()” actually does return a named scope object without executing the SQL statement:

>> module ActiveRecord
>> module NamedScope
>> class Scope
>> def inspect
>>   super # Avoids calling ActiveRecord::Base.find and calls Object.inspect
>> end
>> end
>> end
>> end
=> nil
>> Task.scoped(find_options)
=> #<ActiveRecord::NamedScope::Scope:0x21e65d4
      @proxy_options={:conditions=>["LOWER(`tasks`.name) LIKE ?", "%t%"],
          :order=>"`tasks`.name ASC", :limit=>10},
      @proxy_scope=Task(id: integer, project_id: integer, name: string,
          due_at: datetime, created_at: datetime, updated_at: datetime)>

So here we can see that “scoped” returns an ActiveRecord::NamedScope::Scope object, and that it has two interesting instance variables: proxy_scope and proxy_options. The first of these, proxy_options, contains the find options that were passed into the scoped() function, or into the “named_scope” declaration in your model. The second value, proxy_scope, indicates the parent scope or context in which this named scope object’s SQL statement should be run. In this example, that is the Task model itself. The named scope object is essentially a cache of the query options that will be user later when the query is executed.

Let’s see how this works in the auto_complete plugin. Back again to the new implementation of auto_complete_for, we have:

@items = model.scoped(find_options)
@items = yield(@items, params) if block_given?

The first line generates a ActiveRecord::NamedScope::Scope object, which is then passed into the block provided by the controller code, if any. Let’s take a look at my sample app’s implementation of the controller:

auto_complete_for :task, :name do | items, params |
  items.by_project(params['project'])
end

This is a good example of the second very cool feature of named scopes: that they are composable… in other words, that two or more named scopes can be combined together to form a single SQL statement that is executed only once! Let’s return to the same Rails console session with our redefined “inspect” method and see if we can understand a bit more about this:

>> Task.scoped(find_options).by_project 'Project One'
=> #<ActiveRecord::NamedScope::Scope:0x21d1864
  @proxy_options={
    :conditions=>["projects.name = ?", "Project One"],
    :include=>:project},
  @proxy_scope=
    #<ActiveRecord::NamedScope::Scope:0x21d1a30
      @proxy_options={
        :conditions=>["LOWER(`tasks`.name) LIKE ?", "%t%"],
        :order=>"`tasks`.name ASC",
        :limit=>10},
      @proxy_scope=Task(id: integer, project_id: integer, name: string, due_at: datetime, created_at: datetime, updated_at: datetime)
    >
  >

Now we can see that calling scoped(find_options).by_project just returns a chain of two named scopes: the first scope object with @proxy_scope set to the second one, and the second one with @proxy_scope set to the base model class. Later when this SQL query is executed, the code in NamedScope and ActiveRecord::Base will simply walk this chain of objects, accumulate the options into a single hash, convert the hash to SQL and execute it.

In auto_complete_for after the controller’s block returns, the “@items” value in auto_complete_for above is set to the parent/child named scope chain of objects, and then passed into auto_complete_result:

render :inline => "<%= auto_complete_result @items, '#{method}' %>"

Inside of auto_complete_result the @items value is used as if it were an array… like this:

def auto_complete_result(entries, field, phrase = nil)
  return unless entries
  items = entries.map { |entry|
    content_tag("li",
                phrase ? highlight(entry[field], phrase) : h(entry[field]))
  }
  content_tag("ul", items.uniq)
end

… to generate the HTML needed for the Prototype library’s implementation of the auto complete drop down box. The interesting thing here is that the SQL statement is executed as soon as the call to “map” is executed, accessing the elements of the “@items” array. This works because the ActiveRecord::NamedScope::Scope class redirects or delegates the [] and other array methods to ActiveRecord::Base.find. Here's the single SQL that is executed with the combined, accumulated query options:

SELECT `tasks`.`id` AS t0_r0, `tasks`.`project_id` AS t0_r1,
  `tasks`.`name` AS t0_r2, `tasks`.`due_at` AS t0_r3,
  `tasks`.`created_at` AS t0_r4, `tasks`.`updated_at` AS t0_r5,
  `projects`.`id` AS t1_r0, `projects`.`name` AS t1_r1,
  `projects`.`created_at` AS t1_r2, `projects`.`updated_at` AS t1_r3
FROM `tasks`
LEFT OUTER JOIN `projects` ON `projects`.id = `tasks`.project_id
WHERE ((projects.name = 'Project One') AND
  (LOWER(`tasks`.name) LIKE '%t%'))
ORDER BY `tasks`.name ASC LIMIT 10

In fact, in the original version of the auto_complete plugin before my changes to it for named scopes, the value passed into auto_complete_result was a simple array. The fact that named scopes are used now is entirely hidden from this code!

One last note here about named scope: as described in a comment in named_scope.rb from the Rails source code, the “proxy_options” method provides a convenient way to test the behavior of named scopes without actually checking the results of an actual SQL query. Here’s one of the tests I wrote for my new version of auto_complete_for:

def test_default_auto_complete_for
  get :auto_complete_for_some_model_some_field,
      :some_model => { :some_field => "some_value" }
  default_auto_complete_find_options = @controller.items.proxy_options
  assert_equal "`some_models`.some_field ASC", 
               default_auto_complete_find_options[:order]
  assert_equal 10, default_auto_complete_find_options[:limit]
  assert_equal ["LOWER(`some_models`.some_field) LIKE ?", "%some_value%"],
               default_auto_complete_find_options[:conditions]
end

Since I didn’t want to go to the trouble of setting up an actual in-memory database using SQLite, or to introduce mocha or some other mocking framework to the auto_complete tests, all I had to do was just call @controller.items.proxy_options and check that the find options are as expected. (I also had to expose “items” in the mock controller using attr_reader.) I have another test that checks that the controller’s block is called and it’s named scope options are present as expected… this test uses the proxy_scope method to walk up the chain to the parent named scope and get it’s proxy_options. See auto_complete_test.rb for details.

1 comment Tags:

Filtering auto_complete pick lists

March 13, 2009 · 3 comments

I’ve written a lot here during the past few months about the auto_complete plugin and how to get it to work with repeated text fields on a complex form. Back in January I modified Ryan Bates’s complex forms sample app to illustrate how to use my version of the auto complete plugin to handle repeated text fields. Here’s what the form looks like in that sample app:

Here as the user types into a “Task” text field, a list of all of the existing task names in the system that match the characters typed in by the user are displayed in a drop down list. But what if I didn’t want to display all of the matching task names? What if I wanted to display only the tasks for a given project? Or if I wanted to filter the task names in some other way based on other field values?

In this simple example, what if I only wanted to display Tasks 2a, 2b and 2c, since they belonged to Project Two?

Today I took a look at this problem and expected to see a number of simple solutions, but instead I was surprised to find that it is fairly difficult to do this. I got started by reading this nice solution from Andrew Ng (nice work Andrew!). Andrew explains how to get the Prototype javascript code to pass an additional HTTP parameter to the server when the user types into an autocomplete field. This additional parameter can then be used to filter the list of auto complete options differently. I’ll let you read the details, but basically Andrew found that you can use a Javascript callback function like this to load a value from another field on your form, and pass it to the server in the Ajax request as an additional query string parameter:

<script type="text/javascript">
  new Ajax.Autocompleter(
    'task_name', 
    'task_name_auto_complete', 
    '/projects/auto_complete_for_task_name', 
    { callback: function(e, qs) {
        return qs + '&project=' + $F('project_name');
      }
    }
  );
</script>

(I’ve renamed the variables to use my project/tasks example.) What I didn’t like about this was the need to manually code all of this javascript; there must be a way to get the auto_complete plugin to do this instead… and there is! If you look at the definition of text_field_with_auto_complete in auto_complete_macros_helper.rb, you’ll see that it takes both tag_options and completion_options as parameters, and eventually calls auto_complete_field with the completion_options. Here’s what auto_complete_field looks like in the auto_complete plugin:

def auto_complete_field(field_id, options = {})
  function =  "var #{field_id}_auto_completer = new Ajax.Autocompleter("
  function << "'#{field_id}', "

etc...

  js_options = {}
  js_options[:tokens] = etc...
  js_options[:callback]   =
    "function(element, value) { return #{options[:with]} }" if options[:with]
  js_options[:indicator]  = etc...
  js_options[:select]     = etc...
  js_options[:paramName]  = etc...

etc...

  function << (', ' + options_for_javascript(js_options) + ')')
  javascript_tag(function)
end

If you look closely at the line I bolded above, you’ll see that we can actually generate Andrew’s Javascript callback function automatically by simply passing in a value for the “with” completion option when we call text_field_with_auto_complete in our view, like this:

text_field_with_auto_complete :task, :name, {},
  {
    :method => :get,
    :with =>"value + '&project=' + $F('project_name')"
  }

Again, this line of Javascript code is called when the user types into the task name field, and appends “&project=XYZ” to the query string for the Ajax request. “XYZ” is the name of the project typed in by the user on the same form, loaded with prototype’s “$F” (Form element get value) function. The “:method => :get” completion option is used to avoid problems with CSRF protection; see http://www.ruby-forum.com/topic/128970. If you look at your server’s log file, you’ll see HTTP requests that look something like this now:

127.0.0.1 - - [13/Mar/2009:16:17:03 EDT] "
GET /projects/auto_complete_for_task_name?task%5Bname%5D=T&project=Project%20Two
HTTP/1.1" 200 57

Here we can see the “auto_complete_for_task_name” route is called and given two request parameters: “task[name]” and “project”. The task name is the standard parameter generated by the autocompleter javascript, and “project” is the additional parameter created by the callback function generated by the :with option.

Now… how do we handle the “project” parameter in our controller code? Without modifying the auto_complete plugin itself, you would have to write your own controller method and not use the “auto_complete_for” macro at all. Andrew shows how to do this on his blog. What I want to explore here now is whether there’s a way to change the auto_complete_for method to allow for customizations of the query used to load the auto complete options.

To understand the problem a bit better, let’s take a look at how “auto_complete_for” is implemented in the auto_complete plugin:

def auto_complete_for(object, method, options = {})
  define_method("auto_complete_for_#{object}_#{method}") do
    find_options = { 
      :conditions => [ "LOWER(#{method}) LIKE ?", '%' +
        params[object][method].downcase + '%' ], 
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)
    @items = object.to_s.camelize.constantize.find(:all, find_options)
    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end	
end

When this is called as your Rails application initializes, it adds a new method to your controller called something like “auto_complete_for_task_name” with your model and column names instead. What we want to do is filter the query results differently, by using a new HTTP parameter – so we need to modify the “conditions” hash passed into find :all. At first I tried to do this by passing in different values for the “options” parameter, since that’s merged with the default options and then passed into find :all. However, the problem with this approach is that whatever you pass in using “options” will not have access to the request parameters, since it’s passed in when the controller is initialized, and not when the HTTP request is received.

So the solution is to pass in a block that is evaluated when the request is received, and when the generated method is actually called. I wrote a variation on auto_complete_for called “filtered_auto_complete_for:”

def filtered_auto_complete_for(object, method)
  define_method("auto_complete_for_#{object}_#{method}") do
    find_options = { 
      :conditions => [ "LOWER(#{method}) LIKE ?", '%' +
        params[object][method].downcase + '%' ], 
      :order => "#{method} ASC",
      :limit => 10 }
    yield find_options, params
    @items = object.to_s.camelize.constantize.find(:all, find_options)
    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

Filtered_auto_complete_for takes a block and evaluates it when the actual HTTP Ajax request is received from the auto complete Javascript. The block is provided with the find options hash and also the request parameters. This enables the controller’s block to modify the find options in any way it would like, possibly using the HTTP request parameters provided. I’ve also removed the options parameter since that’s not necessary any more.

As an example, here’s my sample app’s controller code:

class ProjectsController < ApplicationController

  # Handle auto complete for project names as usual:
  auto_complete_for :project, :name

  # For task name auto complete, only display tasks
  # that belong to the given project: 
  filtered_auto_complete_for :task, :name do | find_options, params|
    find_options.merge!(
      {
        :include => :project,
        :conditions => [ "LOWER(tasks.name) LIKE ? AND projects.name = ?",
                         '%' + params['task']['name'].downcase + '%',
                         params['project'] ],
        :order => "tasks.name ASC"
      }
    )
  end

  def index
    @projects = Project.find(:all)
  end

  etc...

The code in this sample block modifies the find_options by adding “:include => :project”. This causes ActiveRecord to use a JOIN to include columns from the project table in the query (in this sample app Project has_many Tasks, and each Task belongs_to a Project). Then it matches on the project name, in addition to the portion of the task name typed in by the user so far. This limits the auto complete values to just the tasks that belong to the given project:

When I have time during the next few days I’ll add “filtered_auto_complete_for” to my forked version of the auto_complete plugin… first I need to write some unit tests for it, and be sure it works as intended. After that, I’ll post this sample app back on github and you can try it yourself.

3 comments Tags:

Sample app for auto complete on a complex form

January 29, 2009 · 13 comments

Update November 2009: My View Mapper gem now supports generating scaffolding code for a complex form with auto_complete behavior like the one I describe below right inside your application, using your models and attributes. For more info see: http://patshaughnessy.net/2009/11/25/scaffolding-for-auto-complete-on-a-complex-nested-form. You can read more about my fork of the auto_complete plugin here: http://patshaughnessy.net/repeated_auto_complete.

 

In his “Complex Forms” series (part 1, part 2 and part 3) Ryan Bates does a fantastic job explaining how to create a complex form containing a series of parent/child text fields while still using simple, clean code. Ryan also pushed the sample application from the screen cast onto github, here: http://github.com/ryanb/complex-form-examples

Here’s what Ryan’s sample complex form looks like:

One problem I ran into while using Ryan’s suggestions on a complex form I was writing was how to get auto complete behavior to work properly using the auto_complete plugin for fields that are repeated, like the “task” field here. As I explained in a previous blog post, this causes a lot of problems for the auto_complete plugin since the <input id=””> attributes are no longer unique, breaking the javascript used for auto complete. I was able to solve the problem by modifying the auto_complete plugin to generate unique <input id=””> attributes, among other things.

Here I want to take some time to show how to use my modified auto_complete plugin, using the same sample application from Ryan’s screencast. To get started, let’s clone the git repository for the sample app - this command refers to my fork of Ryan's complex-form-examples repository: http://github.com/patshaughnessy/complex-form-examples

$ git clone git://github.com/patshaughnessy/complex-form-examples.git
Initialized empty Git repository in /Users/pat/rails-apps/complex-form-examples/.git/
remote: Counting objects: 192, done.
remote: Compressing objects: 100% (122/122), done.
remote: Total 192 (delta 71), reused 159 (delta 58)
Receiving objects: 100% (192/192), 86.19 KiB | 68 KiB/s, done.
Resolving deltas: 100% (71/71), done.

Ryan had saved various versions of the sample app in different git branches, so to avoid confusion I’ve saved my auto complete related changes in a branch called “auto_complete.” So next you should switch to that branch:

$ cd complex-form-examples
$ git checkout origin/auto_complete
Note: moving to "origin/auto_complete" which isn't a local branch
If you want to create a new branch from this checkout, you may do so
(now or later) by using -b with the checkout command again. Example:
  git checkout -b <new_branch_name>
HEAD is now at 4f3e908... Sample app code changes for auto_complete

Now you will see my changes in Ryans’ code, except for one more detail: I saved my version of the auto_complete plugin in this git repository as a submodule. To get the plugin’s code for this sample app you need to run these commands:

$ git submodule init
Submodule 'vendor/plugins/auto_complete' (git://github.com/patshaughnessy/auto_complete.git) registered for path 'vendor/plugins/auto_complete'
$ git submodule update
Initialized empty Git repository in /Users/pat/rails-apps/complex-form-examples/vendor/plugins/auto_complete/.git/
remote: Counting objects: 22, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 22 (delta 5), reused 0 (delta 0)
Receiving objects: 100% (22/22), 7.65 KiB, done.
Resolving deltas: 100% (5/5), done.
Submodule path 'vendor/plugins/auto_complete': checked out '0814a25a754a235c5cf6f7a258fa405059a5ca6f'

(Note that normally to install the plugin in your app you would just run “script/plugin install git://github.com/patshaughnessy/auto_complete.git” – the submodule is only present in this sample app.) Now to setup and run the application you just need to:

  1. Enter your MySQL details in config/database.yml
  2. Run rake db:migrate
  3. Run script/server to launch the app

If you enter a few records you should be able to see the auto complete drop down, even for the repeated field:

Let’s review the changes I’ve made to Ryan’s code aside from adding my modified version of auto_complete to vendor/plugins. First, I added the standard auto_complete handlers to projects_controller.rb for both the project and task fields:

class ProjectsController < ApplicationController
  auto_complete_for :project, :name
  auto_complete_for :task, :name
…

Next I modified the project text field to use auto complete (in views/projects/_form.html.erb):

<p>
  <%= f.label :name, "Project:" %>
  <%= text_field_with_auto_complete :project, :name, {}, {:method => :get } %>
</p>

These two changes enable auto complete for the single project text field, just the same way you would with any text field and the standard auto_complete plugin. However, to get auto complete to work with the repeated tasks field, we need to use changes I’ve made to auto_complete. First, in helpers/projects_helper.rb change the “fields_for_task” method to use my new auto_complete_fields_for method, like this:

def fields_for_task(task, &block)
  new_or_existing = task.new_record? ? 'new' : 'existing'
  prefix = "project[#{new_or_existing}_task_attributes][]"
  auto_complete_fields_for(prefix, task, &block)
end

This causes my code in auto_complete to provide a custom form builder object, which we can use in the view as follows (views/projects/_task.html.erb):

<% fields_for_task task do |f| -%>
  <%= error_messages_for :task, :object => task %>
  <%= f.label :name, "Task:" %>
  <%= f.text_field_with_auto_complete :task, :name, {}, {:method => :get } %>
  <%= link_to_function "remove", "$(this).up('.task').remove()" %>
<% end -%>

Here I’ve called “text_field_with_auto_complete” as a method on the “f” form builder object yielded by fields_for_task. This will cause the auto complete script and HTML to be generated with unique <input id=””> attributes, allowing the auto complete behavior to work properly.

One other change I made was also to helpers/projects_helper.rb:

def add_task_link(name)
  link_to_remote "Add a task", :url => {
                                 :controller => "projects",
                                 :action => "add_task_script"
                               }
end

Here I’ve changed Ryan’s “link_to_function” call to “link_to_remote.” As Ryan explains in part 2 of his complex forms screen cast, link_to_function avoids an AJAX call to the server to obtain the HTML for each new task <input> tag, avoiding unnecessary load on the server since all of the task fields are the same. However, with my changes to auto_complete the HTML generated for the task field contains random numbers which are different for each copy of the field… meaning that we do need a separate call to the server to obtain the task field HTML and script. To handle the call from link_to_remote, I’ve added a new file, views/projects/add_task_script.rjs:

page.insert_html :bottom, :tasks, :partial => 'task', :object => Task.new

… which works essentially the same way as described by Ryan, but is called each time the user clicks “Add a task.”

The last change I made to the sample app is in routes.rb; these changes are required to allow the controller to map the Ajax requests, and to insure that these requests use GET, and not POST HTTP requests:

map.connect 'projects/auto_complete_for_project_name',
            :controller => 'projects',
            :action => 'auto_complete_for_project_name'
map.connect 'projects/auto_complete_for_task_name',
            :controller => 'projects',
            :action => 'auto_complete_for_task_name'
map.connect 'projects/add_task_script',
            :controller => 'projects',
            :action => 'add_task_script'
map.resources :projects,
              :collection => {
                :auto_complete_for_project_name => :get,
                :auto_complete_for_task_name => :get
              }

This certainly seems very ugly, and probably could be simplified! But for now, we need this code to avoid problems with CRSF protection; see http://www.ruby-forum.com/topic/128970.

13 comments Tags:

Repeated_auto_complete changes merged into auto_complete

January 29, 2009 · 7 comments

Update June 2009: I just added support to my version of auto_complete to support Rails 2.3 nested attributes; for more details see: http://patshaughnessy.net/repeated_auto_complete. The basic ideas below still apply, but my implementation of auto_complete has changed, and I’ve also simplified the usage.

 

In October I described how the auto_complete plugin doesn’t work when text fields are repeated more than once on a complex form. I went on to write a plugin called “repeated_auto_complete&rdquo; which modified the way the standard auto_complete plugin works and fixed this problem by adding random numbers to <input id=""> attributes among other changes.

Since it’s much cleaner to have a single auto_complete plugin rather than two separate plugins, I’ve merged my changes to auto_complete into the original version, and pushed them to github as a new fork: http://github.com/patshaughnessy/auto_complete

To install and use my modified version of auto_complete first remove the standard auto_complete plugin from your app if necessary, and install with:

script/plugin install git://github.com/patshaughnessy/auto_complete.git

To use auto complete in a complex form, you write “auto_complete_fields_for” or “auto_complete_form_for” in your view, and then call text_field_with_auto_complete on the form builder object, as follows:

<% for person in @group.people %>
  <% auto_complete_fields_for "group[person_attributes][]", person do |form| %>
    Person <%= person_form.label :name %><br />
    <%= form.text_field_with_auto_complete :person, :name, {},
                                           {:method => :get}  %>
  <% end %>
<% end %>

To understand my changes to the plugin, let’s first look at how the original auto_complete works. If you add this line to your view:

<%= text_field_with_auto_complete :project, :name, {}, {:method => :get } %>

…then you get HTML and script that looks like this (style sheet omitted):

<input id="project_name" name="project[name]" size="30" type="text" />
<div class="auto_complete" id="project_name_auto_complete"></div>
<script type="text/javascript">
//<![CDATA[
var project_name_auto_completer = new Ajax.Autocompleter('project_name',
'project_name_auto_complete', '/projects/auto_complete_for_project_name',
{method:'get'})
//]]>
</script>

The original text_field_with_auto_complete method looked like this:

def text_field_with_auto_complete(object, method, tag_options = {},
                                  completion_options = {})
    (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
    text_field(object, method, tag_options) +
    content_tag("div", "", :id => "#{object}_#{method}_auto_complete",
                :class => "auto_complete") +
    auto_complete_field(
        "#{object}_#{method}",
        { 
          :url => { :action => "auto_complete_for_#{object}_#{method}" }
        }.update(completion_options))
  end

You can see that it calls “text_field” in ActionView::Helpers::FormHelper to generate the actual <input> tag for the form, in addition to generating the HTML and script needed for the auto completion behavior.

What I wanted to achieve in the modified plugin was to allow the view to contain code like this:

<% auto_complete_fields_for task do |f| %>
  <%= f.label :name, "Task:" %>
  <%= f.text_field_with_auto_complete :task, :name, {}, {:method => :get } %>
<% end %>

To make this work, we need a new version of text_field_with_auto_complete that calls text_field from ActionView::Helpers::FormBuilder, and not ActionView::Helpers::FormHelper, generating an <input> tag similar to what this call would generate:

<% fields_for task do |f| %>
  <%= f.text_field :name %>
<% end %>

To do this, I first refactored the original text_field_with_auto_complete in auto_complete_macros_helper.rb:

def text_field_with_auto_complete(object, method, tag_options = {},
                                  completion_options = {})
  auto_complete_field_with_style_and_script(object, method, tag_options,
                                            completion_options) do
    text_field(object, method, tag_options)
  end
end

def auto_complete_field_with_style_and_script(object, method,
                                              tag_options = {},
                                              completion_options = {})
  (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
  yield +
  content_tag("div", "", :id => "#{object}_#{method}_auto_complete",
              :class => "auto_complete") +
  auto_complete_field(
    "#{object}_#{method}",
    {
      :url => { :action => "auto_complete_for_#{object}_#{method}" } 
    }.update(completion_options))
end

Here I’ve introduced a new utility function called “auto_complete_field_with_style_and_script” that generates the same Javascript and style sheet for the view as before, but instead calls a block to generate the actual text field. Then I changed text_field_with_auto_complete to call this, providing a block to make the call to “text_field” in ActionView::Helpers::FormHelper with the proper names and options.

Now my new form builder class in auto_complete_form_helper.rb contains a version of text_field_with_auto_complete that looks like this:

def text_field_with_auto_complete(object,
                                  method,
                                  tag_options = {},
                                  completion_options = {})
  unique_object_name = "#{object}_#{Object.new.object_id.abs}"
  completion_options_for_original_name =
    {
      :url => { :action => "auto_complete_for_#{object}_#{method}"},
      :param_name => "#{object}[#{method}]"
    }.update(completion_options)
  @template.auto_complete_field_with_style_and_script(
        unique_object_name,
        method,
        tag_options,
        completion_options_for_original_name
  ) do
    text_field(method,
               {
                 :id => "#{unique_object_name}_#{method}"
               }.update(tag_options))
  end
end

Here the call to auto_complete_field_with_style_and_script passes a block that calls the other text_field from ActionView::Helpers::FormBuilder (note the “object” parameter is not present as above).

To allow the text field to be repeated on a complex form, I insure the object’s name is unique by adding a random number to it (“unique_object_name”). This unique name is then passed into both auto_complete_field_with_style_and_script and text_field, insuring that the <input> and related Javascript all work without problems, even if the text field is repeated more than once on the same form.

The last important detail here is that the completion options passed into auto_complete_field_with_style_and_script are generated using the original, unchanged (non-unque) object name, so that the Ajax calls to the server are made using the original name. This means no changes are required on the server side, and the same single line of code in your controller still works as usual:

auto_complete_for :task, :name

Next time I’ll post a sample application that uses this new plugin, and explain what changes you will need to make to your own application for auto_complete in a complex form.

7 comments Tags:

The auto_complete plugin refactored to support repeated fields and named scopes

November 18, 2008 · 8 comments

(Updated June 2009)

This version of auto_complete will support text fields that are repeated more than once on a complex form. It allows you to call text_field_with_auto_complete on the form builder object yielded by fields_for or form_for. This will work for complex forms built with Rails 2.2 or earlier, and for the nested attributes feature introduced in Rails 2.3. Here's an example using nested attributes:

<% form_for @project do |project_form| %>
  <% project_form.fields_for :tasks do |task_form| %>
    <p>
      <%= task_form.label :name, "Task:" %>
      <%= task_form.text_field_with_auto_complete :name, {},
          { :method => :get, :skip_style => true } %>
    </p>
  <% end %>
<% end %>

It also allows you to provide a block to auto_complete_for in your controller that filters the drop down pick list in some custom way. For example, this block would display task names for the project the user had selected elsewhere on the same form, using a named scope by_project:

auto_complete_for :task, :name do | items, params |
  items.by_project(params['project'])
end

Code:  http://github.com/patshaughnessy/auto_complete

Install as a gem:

gem sources -a http://gemcutter.org
sudo gem install repeated_auto_complete

… and in config/environment.rb:

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

Install as a plugin:

script/plugin install git://github.com/patshaughnessy/auto_complete.git

More information:

8 comments Tags:

Testing is a lesson in humility

November 16, 2008 · 0 comments

I was working on a web site a few weeks ago and found that the auto_complete plugin didn’t work well when text fields were repeated on a form. Later I modified auto_complete to handle the case where text fields are repeated. I then refactored the code into a separate add-on plugin, but before I posted the code on github I decided to add a few tests just for the sake of completeness. I was confident that the text_field_with_auto_complete function I wrote last week was correct – after all, it was working inside my web site. I suppose you could call this method “Development Driven Testing:” write some code first, and then add a few tests after the fact to make yourself feel more confident that you have done your job correctly! Needless to say, I later realized this was a bad idea.

I wrote some simple test code similar to what Rails uses to test the FormHelper module: vendor/rails/actionpack/test/template/form_helper_test.rb. The idea behind my test was simple. As I explained last week, my code in text_field_with_auto_complete worked by insuring each generated <input> tag's id attribute was unique, and by using a name attribute taken from the surrounding call to fields_for, or form_for, instead of the standard name value “object[method].” (It also passes different values for the URL and “param_name” parameter into the Ajax code, but let’s skip that for now.) In other words, my code simply called into the original auto_complete plugin with some modified parameters. So to test this, I would just call the original and modified versions of text_field_with_auto_complete and compare the HTML after a search/replace for the desired changes. Here’s the test:

def test_auto_complete_fields_for_html
  standard_auto_complete_html =
    text_field_with_auto_complete :person,
                                  :name,
                                  {},
                                  { :param_name => 'person[name]' }
  _erbout = ''
  auto_complete_fields_for('group[person_attributes][]', @person) do |f|
    _erbout.concat f.text_field_with_auto_complete(:person, :name)
  end
  assert_dom_equal standard_auto_complete_html,
    _erbout.gsub(/group\[person_attributes\]\[\]/, 'person')
           .gsub(/person_[0-9]+_name/, 'person_name')
end

“Standard_auto_complete_html” contains the HTML the original auto_complete plugin generates. Then I call auto_complete_fields_for and my version of text_field_with_auto_complete, simulating a real ERB template, and write the HTML into _erbout. Finally we test that after search/replace on the expected changes the modified HTML is the same as the standard HTML. After the usual syntax errors and typos I ran the test again and completely expected it to work…

Of course, it failed! I couldn’t even discover what the error was until I wrote the HTML out on the console and took a close look at what was generated. Here’s the <input> tag the original auto_complete plugin generates:

<input id="person_name"
       name="person[name]"
       size="30"
       type="text"
       value="Someone Important" />

And here’s what my code generated before the search and replace:

<input id="person_15961340_name"
       name="group[person_attributes][][name]"
       size="30"
       type="text" />

Here we can see the expected changes to the id and name attributes, but the “value” attribute is missing from my HTML! I had no idea this was happening even when I used my plugin in a web site. I had noticed earlier that some values were missing in my site’s text fields in the browser but I had assumed this was normal behavior from auto_complete and simply added the values explicitly in the ERB.

Lesson learned: not only do tests insure your code works, but the process of writing the tests forces you to think much more deeply and carefully about what your code is really doing. Of course, once you start writing tests first the same thing applies to your design: you think much more carefully about what you are trying to do, and how to design a solution.

I went on to quickly fix the text_field_with_auto_complete function by passing in the original object and method parameters, so that Rails would get the proper value for me, and explicitly setting the id attribute with a unique number as follows:

@template.text_field_with_auto_complete(
      object,
      method,
      { :name => "#{@object_name}[#{method}]",
        :id => "#{object}_#{Object.new.object_id}_#{method}"
etc…

This got my test to pass! Relieved, I went on to write another test. This second test insures that the id attributes generated by the plugin are all unique:

def test_two_auto_complete_fields_have_different_ids
  id_attribute_pattern = /id=\"[^\"]*\"/i
  _erbout = ''
  _erbout2 = ''
  auto_complete_fields_for('group[person_attributes][]', @person) do |f|
    _erbout.concat f.text_field_with_auto_complete(:person, :name)
    _erbout2.concat f.text_field_with_auto_complete(:person, :name)
  end
  assert_equal
    [],
    _erbout.scan(id_attribute_pattern) & _erbout2.scan(id_attribute_pattern)
end

I call the text_field_with_auto_complete function twice and check that all of the <input> tags have unique id=”” attributes by scanning for the attributes and checking that the two arrays of matches have an empty intersection set. Sounds simple, right? Surely it will pass…

Getting surprised and humiliated by one unit test was bad enough… but the second test surprised me yet again! What happened here was that all of the <div> tags, also generated by text_field_with_auto_complete, had the same id attributes! I had written the test above to look for <input id=””> attributes but fortunately the code also matched <div id=””>, like this:

<div class="auto_complete" id="person_name_auto_complete"></div>

Since these id's were not unique, my test failed:

<[]> expected but was
<["id=\"person_name_auto_complete\""]>.
2 tests, 2 assertions, 1 failures, 0 errors

I finally solved the problem and got both of my tests to pass using this code:

def text_field_with_auto_complete(object, method,
                                  tag_options = {}, completion_options = {})
    object_value =
      ActionView::Helpers::InstanceTag.value_before_type_cast(@object,
                                                              method.to_s)
    @template.text_field_with_auto_complete(
      "#{object}_#{Object.new.object_id}",
      method,
      { :name => "#{@object_name}[#{method}]",
        :value => object_value
      }.update(tag_options),
      { :param_name => "#{object}[#{method}]",
        :url => { :action => "auto_complete_for_#{object}_#{method}" }
      }.update(completion_options)
    )
  end

To completely understand why the value attribute was missing and how to get it back I took a look at how the FormHelper module in Rails worked. A long story short: I get the object’s value by carefully using the same code that the FormHelper module does by calling InstanceTag.value_before_type_cast, and then pass the value in as a parameter to text_field_with_auto_complete. I was sure to obtain the proper object’s value by using “@object” from the FormBuilder base class. And now the <div id="">'s are unique since we pass in the modified object name into the original text_field_with_auto_complete.

0 comments Tags:

Modifying the auto_complete Plugin to Allow Repeated Fields

October 31, 2008 · 3 comments

Update March 2009: I reimplemented the code from this article in a better way and posted it in a fork of auto_complete on github; see: http://patshaughnessy.net/repeated_auto_complete.... however, the basic ideas below still apply.

Last week I ran into trouble trying to use the auto_complete plugin like this for a form containing a single group name field, but a series of repeated people name fields:
<p>
  Group <%= f.label :name %><br />
  <%= text_field_with_auto_complete :group, :name,
                                       {}, {:method => :get} %></p>
<% for person in @group.people %>
  <% fields_for "group[person_attributes][]", person do |person_form| %>
    <p>
      Person <%= person_form.label :name %><br />
     <%= text_field_with_auto_complete :person, :name, {:index => nil},
                                      {:method => :get}  %></p>
    <% unless person.new_record? %>
      <%= person_form.hidden_field :id, :index => nil %>
    <% end %>
  <% end %>
<% end %>

I want to just display a text field repeated once for each person record. The problem is that the text_field_with_auto_complete macro returns HTML and Javascript that doesn’t work when it’s repeated many times. How can I to get this to work?

Let’s start by writing the code we would like to use, or wished could work someday. The key detail in the form above is the name of the object in fields_for:
fields_for "group[person_attributes][]", person do |person_form|

This name is used on the server to mass-assign the attributes of all of the person records to the parent group record. The only way text_field_with_auto_complete could possibly work is if it generated an <input> tag with the desired name: “group[person_attributes][][name]”. I could just pass this value into text_field_with_auto_complete, but that not be very DRY since I would have to repeat the name more than once.

What if I could just call text_field_with_auto_complete directly on the person_form object yielded by fields_for? For example:
<% fields_for "group[person_attributes][]", person do |person_form| %>
  <%= person_form.text_field_with_auto_complete :person, :name %>

Then this call could generate an <input> tag with a name generated from the fields_for object name and my form code would remain very simple, readable and maintainable.

I found a way to do this following John Ford’s example (Writing a Custom FormBuilder in Rails) by adding a new version of form_for and fields_for to ActionView. First I modified the auto_complete plugin’s init.rb file and added a line to mixin a new form helper module into ActionView like this:
ActionView::Base.send :include, AutoCompleteFormHelper
Then our new AutoCompleteFormHelper class will look like this:
module AutoCompleteFormHelper
  [:form_for, :fields_for, :form_remote_for, :remote_form_for].each do |meth|
    src = <<-end_src
      def auto_complete_#{meth}(object_name, *args, &proc)
        options = args.last.is_a?(Hash) ? args.pop : {}
        options.update(:builder => AutoCompleteFormBuilder)
        #{meth}(object_name, *(args &lt;&lt; options), &proc)
      end
    end_src
    module_eval src, __FILE__, __LINE__
  end
end

What this does is create new ActionView methods called “auto_complete_form_for,” “auto_complete_fields_for,” etc. These methods simply call the original form_for and fields_for but pass in an additional option “:builder” that indicates ActionView should yield a different class to the form block… a new class I will write called “AutoCompleteFormBuilder,” instead of the original FormBuilder class. Now by writing AutoCompleteFormBuilder we have the ability to implement the behavior we need from text_field_with_auto_complete.

Here’s what I ended up with:

class AutoCompleteFormBuilder < ActionView::Helpers::FormBuilder
  def text_field_with_auto_complete(object, method, tag_options = {},
                                    completion_options = {})
    @template.text_field_with_auto_complete(
      "#{object}_#{Object.new.object_id}",
      method,
      { :name => "#{@object_name}[#{method}]" }.update(tag_options),
      { :param_name => "#{object}[#{method}]",
        :url => { :action => "auto_complete_for_#{object}_#{method}" }
      }.update(completion_options)
    )
  end  
end
This simply calls the original text_field_with_auto_complete with slightly different parameters:
  • We generate a unique number and add it as a suffix to the id attribute that will be generated for the <input> tag, and referenced in the Javascript:
    "#{object}_#{Object.new.object_id}"
    Now each <input> tag will have a unique id attribute, and the script.aculo.us autocomplete Javascript will work unmodified. Without this change, all of the repeated <input> tags would have the same id, and the autocomplete code would fail trying to find the ambiguous id on the page.
  • We pass in the object name from the fields_for or forms_for declaration as the name attribute for the <input> tag:
    { :name => "#{@object_name}[#{method}]" }.update(tag_options)
    Now our model code works as originally intended when the form is submitted, i.e. the "person_attributes" value will be mass-assigned to our child model objects.
  • We get the autocomplete behavior to work by telling it explicitly to use the simple “person[name]” object name in both the parameter name and URL for the AJAX request:
    { :param_name => "#{object}[#{method}]",
      :url => { :action => "auto_complete_for_#{object}_#{method}" }
    }.update(completion_options)
    This allow us to continue to use the same simple declaration in the controller that handles the auto complete requests:
    auto_complete_for :person, :name

Next step: Post this on GitHub.

3 comments Tags:

Autocomplete plugin doesn’t work for repeated fields

October 20, 2008 · 1 comment

Recently I tried using the autocomplete plugin from Rails for the first time. It was a great way to quickly implement type-ahead Ajax behavior by writing little or no code. By simply adding two lines – one to my controller:
auto_complete_for :group, :name
and another to my form:
<%= text_field_with_auto_complete :group, :name, {}, {:method => :get} %>
users of my form were able to easily pick from a long list of group names simply by typing the first few letters. What I love about Rails is not only was I able to implement this complex feature with only 2 lines, but those 2 lines were very easy to read: my controller has to handle “autocompletion for group names” and my form should contain a “text field with auto complete behavior for group names”. It’s very natural and intuitive.

The only ugly detail here is that the “{:method => :get}” hash is required to avoid errors related to form security… without it I get an error like this:

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
    /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.1/lib/action_controller/request_forgery_protection.rb:86:in 'verify_authenticity_token'
This discussion explains that we can avoid the security exception by forcing the autocomplete request to use GET instead of POST. Note that you also need to change config/routes.rb to map the GET request for the completion values to the autocomplete action in your controller. This is a bit ugly, but not a big deal.

However, once I tried adding additional fields to the form I ran into some real trouble. I enhanced my form by adding multiple child model objects; in this case I associated a series of people for each group. That is:

class Person < ActiveRecord::Base`
 belongs_to :group
and:
class Group < ActiveRecord::Base
 has_many :people
Following Ryan Bates’ great explanation on railcasts about how to implement a form for both parent and child records at the same time, I ended up with a form that looked like this:
<p>
  Group <%= f.label :name %><br />
  <%= text_field_with_auto_complete :group, :name, {}, {:method => :get} %>
</p>
<% for person in @group.people %>
  <% fields_for "group[person_attributes][]", person do |person_form| %>
    <p>
      Person <%= person_form.label :name %><br />
     <%= text_field_with_auto_complete :person, :name, {:index => nil}, {:method => :get}  %></p>
    <% unless person.new_record? %>
      <%= person_form.hidden_field :id, :index => nil %>
    <% end %>
  <% end %>
<% end %>
This looks a lot more complicated, but except for the “index => nil” hack (see part 3 of Ryan’s screen cast for an explanation) the code is very clean and straightforward to understand. We display the group name autocomplete text field as before, and then iterate over the people contained in this group, displaying a new autocomplete text field for each one.

Too bad it doesn’t work! Even worse, this code actually has 3 separate problems: First, the <input> tag generated by text_field_with_auto_complete is not what we want for the child objects. Without autocomplete, we would use:

<%= person_form.text_field :name %>
referring to the “fields_for” method just above and get this HTML:
<input id="group_person_attributes__name" name="group[person_attributes][][name]" size="30" type="text" />
But text_field_with_auto_complete :person, :name, … yields this instead:
<input id="person__name" name="person[][name]" size="30" type="text" />
along with CSS and Javascript for the auto complete behavior.

Second, the server side code added to my controller also doesn’t know where to look for the person name value in the actual parameter hash; instead it assumes it will receive the parameter as specified in the <input> above: params[“person”][“name”]. See vendor/plugins/autocomplete/lib/auto_complete.rb for details.

And the last and worst problem of all is that the DHTML/Javascript code inside of controls.js from script.aculo.us assumes that the <input> tag id passed in will be unique on the page. But since we will have many person fields for each group field, the script.aculo.us code will fail trying to location the <input> and <div> tags specified by text_field_with_auto_complete.

So what to do? The first time I solved this problem, I simply copied and hard coded the HTML and Javascript produced by the autocomplete plugin into my form erb file. Then I changed it to make it work. I also manually added a function to my controller on the server side to get it to find and process the parameter hash properly. If I were still writing PHP code, then I would probably assume this was sufficient and call it a day. If I happened to be using J2EE for this project, I’d probably look at creating a subclass of the autocomplete plugin somehow and extending/modifying the behavior as necessary. Then I would have yet another class and another JAR file to maintain and worry about – not quite as messy, but still ugly or at best complicated.

There must be a better way! Since I’m using Ruby I don’t need to settle for overly complicated code or ugly code. Instead I’ll try to develop a solution using what seems to me the Ruby philosophy: write the cleanest, tightest code you can that makes sense and then find a way to make it work. In my next post I’ll tell you what happened.

1 comment Tags: