Pat Shaughnessy

Ribadesella, Spain

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:··

Auto complete for complex forms using nested attributes in Rails 2.3

June 14, 2009 · 20 comments

I just updated my fork of the auto_complete plugin to support Rails 2.3 nested attributes. Thanks to Anthony Frustagli for the code and ideas that I used as the basis for this fix.

Basic usage:

To use auto_complete on a complex form with nested attributes, just call “text_field_for_auto_complete” right on the form builder object yielded by form_for or fields_for, like in this example:

<% parent_form.fields_for :children do |child_form| %>
  <%= child_form.text_field_with_auto_complete :name, {},
        { :method => :get, :skip_style => true } %>
<% end %>

If you have Rails 2.3, this code will iterate over each child object and display a text field with auto complete support. My plugin will generate HTML and Javascript that works even when repeated in a loop like this. Also note that I’ve left off the object name parameter from text_field_with_auto_complete. It’s not needed now, since the object is indicated by the surrounding call to fields_for. The other parameters are optional and are taken unchanged from the original auto_complete plugin:

  • “:method => :get” indicates GET requests should be used by the AJAX calls to load the pick list values, avoiding problems with CSRF protection.
  • And “:skip => :true” indicates that the inline CSS stylesheet used by the auto complete drop down Prototype code should be skipped. Since we’re iterating over child objects we don’t want the same CSS code repeated once for each; instead include it once in a parent object’s call to text_field_for_auto_complete or else just include it manually somewhere.

That’s it – it should just work. If you’re interested in learning more about how to use nested attributes and what my plugin is actually doing, read on…

Details:

To learn more, let’s take a look at a simple nested attribute example, using the Projects/Tasks models from Ryan Bates' complex forms screen cast:

class Project < ActiveRecord::Base
  has_many :tasks
  accepts_nested_attributes_for :tasks, :allow_destroy => true
end
class Task < ActiveRecord::Base
  belongs_to :project
end

A project has many tasks, and each task belongs to a project. Here I’ve also declared that each project “accepts nested attributes for” tasks. This is a new method added to ActiveRecord in Rails 2.3… for lots of examples and explanation just take a look directly at the new nested_attributes.rb code file in Rails 2.3. In a nutshell, “accepts_nested_attributes_for” tells ActiveRecord that the project model should be able to save the attributes of the associated task model objects when a project is saved. This means that when I submit my project form, it can also contain a series of task fields as well. For example, my view code might look something like this:

<% form_for @project do |project_form| %>
  <p>
    <%= project_form.label :name, "Project:" %>
    <%= project_form.text_field :name %>
  </p>
  <% project_form.fields_for :tasks do |task_form| %>
    <%= task_form.label :name, "Task:" %>
    <%= task_form.text_field :name %>
  <% end %>
<% end %>

This displays a name text field for the project, and then calls “fields_for” again right on the form builder yielded by form_for. This is new for Rails 2.3. In earlier versions of Rails you had to explicitly iterate over the child objects and call fields_for for each one. Now in Rails 2.3, you can call fields_for as a method of the parent form and it will automatically iterate over all of the child objects and call fields_for. If we take a look at the HTML generated by this example form, we’ll find something like:

<input id="project_name" name="project[name]"
  size="30" type="text" value="Some project" />
<input id="project_tasks_attributes_0_id"
  name="project[tasks_attributes][0][id]" type="hidden" value="1" />
<input id="project_tasks_attributes_0_name"
  name="project[tasks_attributes][0][name]" type="text" value="Task one" />
<input id="project_tasks_attributes_1_id"
  name="project[tasks_attributes][1][id]" type="hidden" value="2" />
<input id="project_tasks_attributes_1_name"
  name="project[tasks_attributes][1][name]" type="text" value="Task two" />

I’ve simplified this to make it more readable. You can see the iteration by project_form.fields_for :tasks, and that for each task there’s an <input> tag for the “name” field, along with another hidden <input> tag containing the task’s “id” attribute. The most important detail here is the name given to each of these tags: “project[tasks_attributes][0][name]” for example. Since the tasks are nested attributes of the project, they are displayed using the PARENT_OBJECT[CHILD_OBJECTS_attributes][INDEX][FIELD] pattern, while for the project we get the simple OBJECT[FIELD] pattern. This is the key to making nested attributes work. In our project model, when we called “has_many :tasks”, Rails defined some new methods for us on the Project class to handle tasks: tasks, tasks=, task_ids, task_ids= and a couple of others as well. Now with Rails 2.3, when we call “accepts_nested_attributes_for :tasks” Rails defines another new method for Project called tasks_attributes= in order to process all of the new nested parameters for tasks when the complex project form is submitted. This is the reason for the “_attributes” in the naming pattern used in the form.

Now… how do we get auto complete to work for this form? The problem with auto complete on a complex form has always been that the Javascript and HMTL used by the Prototype library assumes that the <input> tag, <div> tag and related Javascript code would be unique on the HTML page. If you just call the text_field_with_auto_complete macro from the standard auto_complete plugin like this…

<% project_form.fields_for :tasks do |task_form| %>
  <%= text_field_with_auto_complete :task, :name, {},
        { :method => :get, :skip_style => true } %>
<% end %>

… it will not work. The first problem is that text_field_with_auto_complete does not know that fields_for is iterating over the child tasks, or which task is currently being processed in the iteration. But even if you were able to identify the current task object somehow, you would still get HTML like this:

<input id="task_name" name="task[name]" size="30" type="text" />
<div class="auto_complete" id="task_name_auto_complete"></div>
<script type="text/javascript">
//<![CDATA[
var task_name_auto_completer = new Ajax.Autocompleter('task_name',
  'task_name_auto_complete', '/projects/auto_complete_for_task_name',
  {method:'get'})
//]]>
</script>

…

<input id="task_name" name="task[name]" size="30" type="text" />
<div class="auto_complete" id="task_name_auto_complete"></div>
<script type="text/javascript">
//<![CDATA[
var task_name_auto_completer = new Ajax.Autocompleter('task_name',
  'task_name_auto_complete', '/projects/auto_complete_for_task_name',
  {method:'get'})
//]]>
</script>

Now the <input id=“task_name”> tag is repeated on the same page, and the Javascript call to Ajax.Autocompleter('task_name', … ) will not work since the browser will not be able to identify which <input> tag to use.

If you use my plugin instead of the original auto_complete plugin…

$ rm -rf vendor/plugins/auto_complete
$ ./script/plugin install git://github.com/patshaughnessy/auto_complete.git
Initialized empty Git repository in /Users/pat/rails-app/vendor/plugins/auto_complete/.git/
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 20 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (20/20), done.
From git://github.com/patshaughnessy/auto_complete
 * branch            HEAD       -> FETCH_HEAD

… and restart your Rails app, then you can change your view to call text_field_with_auto_complete as a method of the form builder, like this:

<% project_form.fields_for :tasks do |task_form| %>
  <%= task_form.text_field_with_auto_complete :name, {},
  { :method => :get, :skip_style => true } %>
<% end %>

Note that I’ve also dropped :task as a parameter since that’s implicit in the call to fields_for. In fact, since text_field_with_auto_complete is now a method of the FormBuilder object (“task_form”), it has access to the task object currently being processed in the iteration. Now if you refresh the same form you’ll instead get this HTML instead:

<input id="project_tasks_attributes_0_name"
  name="project[tasks_attributes][0][name]"
  size="30" type="text" value="Task one" />
<div class="auto_complete" id="project_tasks_attributes_0_name_auto_complete">
</div><script type="text/javascript">
//<![CDATA[
var project_tasks_attributes_0_name_auto_completer =
  new Ajax.Autocompleter('project_tasks_attributes_0_name',
  'project_tasks_attributes_0_name_auto_complete',
  '/projects/auto_complete_for_task_name',
  {method:'get', paramName:'task[name]'})
//]]>
</script>

…

<input id="project_tasks_attributes_1_name"
  name="project[tasks_attributes][1][name]"
  size="30" type="text" value="Task two" />
<div class="auto_complete" id="project_tasks_attributes_1_name_auto_complete">
</div><script type="text/javascript">
//<![CDATA[
var project_tasks_attributes_1_name_auto_completer =
  new Ajax.Autocompleter('project_tasks_attributes_1_name',
  'project_tasks_attributes_1_name_auto_complete',
  '/projects/auto_complete_for_task_name',
  {method:'get', paramName:'task[name]'})
//]]>
</script>

This looks much better, and will actually work for the following reasons:

  • The <input> tags have the correct names, using the PARENT_OBJECT[CHILD_OBJECTS_attributes][INDEX][FIELD] pattern from fields_for. This means that the field values will be processed properly by ActiveRecord when the form is submitted.
  • My changes to the auto_complete plugin have picked up the child object index, 0 and 1 in this example, and included it in the <input> tag’s id, the <div> tag id and well as the associated Javascript code that calls Ajax.Autocompleter. Since all of the tag id’s are unique, the auto complete behavior works properly again for each text field.
  • The original “task” class name and “name” field name are passed unchanged into the Ajax calls to the server. This means that in your controller you can continue to use “auto_complete_for :task, :name” as usual, without worrying about the complex form and the fact that the task fields are repeated multiple times, etc.:
    Ajax.Autocompleter('project_tasks_attributes_1_name',     
      'project_tasks_attributes_1_name_auto_complete',
      '/projects/auto_complete_for_task_name',
      {method:'get', paramName:'task[name]'})
    Here the third parameter to Ajax.Autocompleter, "/projects/auto_complete_for_task_name", is the AJAX URL which you need to account for in routes.rb, and paramName:'task[name]' tells the auto_complete_for handler in your controller to get the task names as usual, and protects the server side code from all of the complexity around the tag id, names, child object index, etc.

20 comments Tags:

Repeated auto complete plugin usage change

June 14, 2009 · 4 comments

I’ve forked the auto_complete plugin to support repeated text fields in a complex form; see http://patshaughnessy.net/repeated_auto_complete for more details.

If you had downloaded my plugin in the past, I’ve just made a couple of changes that will require some simple code changes to your app:

  • You no longer need to or are able to use “auto_complete_form_for” or “auto_complete_fields_for.” I decided this was confusing and unnecessary. Now my plugin just mixes the text_field_with_auto_complete method right into the standard FormBuilder class. Just use form_for or fields_for as usual.
  • I also dropped the object name parameter from text_field_with_auto_complete. Since text_field_with_auto_complete is a method of the form builder, the target object is indicated by the surrounding call to fields_for or form_for and so doesn’t need to be repeated. Now using form.text_field_with_auto_complete is very similar to using form.text_field or the other form builder methods: you just need to specify the column/field name.

So if you are using my old plugin with a Rails 2.2 or earlier app like this:

<% 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 %>

… you should drop “auto_complete_” and “:person” and just use code like this instead:

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

And if you have Rails 2.3 or later and are using nested attributes, this would become:

<% form_for @group do |group_form| -%>
  <% group_form.fields_for :people do |person_form| %>
    Person <%= person_form.label :name %><br />
    <%= person_form.text_field_with_auto_complete :name, {},
          { :method => :get, :skip_style => true } %>
  <% end %>
<% end %>

4 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:·

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:·