Pat Shaughnessy

Ribadesella, Spain

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.

Tags:·

13 responses so far ↓

  • 1 Javix // Feb 18, 2009 at 08:33 AM

    Hi Pat ! What if I use has_many : somethings, :through=> :something relation to assign one or many Members to a Project, each Member can participate in the Project under different Profile. So I should save a project_id, member_id, profile_id in a join table Participations.How should I create multiple auto_complete fields to choose a Member? How will it work?
  • 2 pat // Feb 19, 2009 at 11:26 AM

    Hi Javix,

    Auto_complete should work just the same way. With a has_many :through relationship ActiveRecord is smart enough to expose the other model in the same way it does with a simple has_many. Using your example, if Project has_many :members, :through => :participations and if p = Project.new you could just call p.members to get the associated members. (Note you have to also put has_many :participations in Project also.)

    The form and auto_complete related code will all be the same: “auto_complete_for” in one of your controllers to get the selection list (for example ProjectsController might have auto_complete_for :member, :name) and the rest of the form would work the same way it does with a simple has_many.

    The only question you have to think about would be the design of the form: would the user be able to edit a project, add members and also select profiles all at the same time? Sounds very complicated to me. I would maybe let the user first add members to the project, and then have a second form appear after the first allowing the user to select a profile for each new member. Or maybe there’s a default profile each member is assigned initially, and then later the user can optionally change it. The auto complete part of this might be the simplest part…

    I did modify my sample app to test this, at least for the simple case of adding members to a project, associated through participations; if you’re interested I could post this in a new branch on github so you can take a look. Or let me know how else I can help. Good luck!

  • 3 Javix // Feb 24, 2009 at 10:22 AM

    Hi, Pat! Yes it would be very nice of you to post some lines. My problem is that I can't really imagine how to do it on my page of creating a new project in my case: Project has_many :participations, has_many :members_profiles, :through=>:particpations ------------------- Member has_many:member_profiles has_many:profiles,:through=>:member_profiles ------------------- Profiles has_many: member_profiles has_many:members,:through=>member_profiles ---------------------- Participation belongs_to:project belogs_to:member_profile ------------------------------ Like this any Member can participate in one or more Projects under different Profile via member_profile_id in Participations table. Thank you very much in advance!
  • 4 Javix // Feb 24, 2009 at 10:32 AM

    Sorry for my formatting style.I think I have an idea. In my case each Project must have a definitive kind of contacts: Technical Contact; HR Contact, Network Contact, etc. So what if I put all these kinds of contcats as textfields (like you did with tasks in your example). So I'l have, for example, 5 textfields under Project name textfield, only their labels shall be different. And on every of these textfields I'll puts something like that: <% fields_for_member member do |f| -%> SomeContactType <%= f.label :name %>
    f.text_field_with_auto_complete :member, :name, {}, {:method => :get } %> <%end> The problem - is that I'll have to get all selected members IDs + Profile but I don't see how for the moment.
  • 5 Erik // Feb 24, 2009 at 09:53 PM

    Looks interesting - I'm looking forward to trying this. Do you have any plans to update it with the nested forms coming in rails 2.2.3? Also, for people copy and pasting from your discussion, you accidentally included an extra "s" in the plugin install command above; it should read script/plugin install git://github.com/pat... instead of what's there now.
  • 6 pat // Feb 24, 2009 at 11:47 PM

    @Erik – yup… typo on script/plugin – thanks for that. I just updated the text. Turns out I was also missing a step for rake db:migrate; just run that after getting the code from github to create the projects and tasks tables.

    I have heard about the recent changes to Rails for complex forms, which sound great. Ryan Bates wrote the original complex forms sample app way back in July, and it uses Rails 2.1.0 so it’s a bit dated by now... Seems to me the auto complete behavior should work exactly the same way, but I’ll have to try it to be sure. I’m guessing some or all of the model code (e.g. save_tasks, and existing_task_attributes=) will be simpler or completely unnecessary. I’ll update the sample when I have time. Thanks for your interest... let me know if it works for you or not.

  • 7 pat // Feb 24, 2009 at 11:53 PM

    @Javix… The code you posted here (no worries about the formatting) is just the same as what I had tried last week after reading your earlier comment. The auto complete behavior should be working fine for you, since it just queries the “name” column on the members table, and really isn’t impacted by the has_many through associations you have setup or anything else.

    I’m not sure exactly how to make this work, but for now one suggestion I have for you is to try using a separate virtual attribute on your project model for each contact/member field. Ryan Bates did a screen cast explaining this idea and I believe his screen casts on complex forms also mentions it at one point. My idea is that your project model could have a virtual attribute for each type of contact, so 5 methods for each of Tech, HR, Network, etc… for example: def tech_contact= and def hr_contact= … then the code inside these methods would create the necessary participation and member records as needed using your ActiveRecord associations. And your form would be simple and easy to read since the 5 field names would correspond to each of these methods. Does this make sense? I might have time later this week to try to add this sort of behavior to the sample app. Or let me know if you can get it to work somehow.

  • 8 SpirosK // Feb 26, 2009 at 07:40 AM

    Hi Pat! I reached this post at last, checked out the sample app from the git, BUT when i try to "ruby script/server" it, i get the following error: undefined method `auto_complete_for' for ProjectsController:Class RAILS_ROOT: C:/Workspace/complex-form-examples app/controllers/projects_controller.rb:3 Any ideas..? Is anything i am missing? Something i should have done prior to running the app?? I supposed i just git-pull it and run it...?!
  • 9 SpirosK // Feb 26, 2009 at 08:20 AM

    Oops, seems like i forgot a step of the process... :-S That's why one should get more sleep before trying something and posting it :-) Anyway, the "init submodule" thing, is it something that should be done with the normal version of the auto_complete too? Because i was getting that message too, when i was trying to run the "normal" auto_complete plugin in the past few days...
  • 10 pat // Feb 26, 2009 at 10:09 AM

    Hi Spiros, Sorry for all the confusion. “git submodule” is only required for this particular sample app because of the way it was setup in git. Looks like your first error message happened because you forgot the submodule steps, and so the plugin was missing. Undefined "auto_complete_for" just means you don't have the auto_complete plugin, or it is not loading for some reason.

    For a normal Rails project all you need to do to get my version of the auto_complete plugin is:

    1. Remove the original auto_complete plugin if you already have that…. Just delete the “auto_complete” folder inside of vendor/plugins.
    2. Then install my version of auto_complete: script/plugin install git://github.com/patshaughnessy/auto_complete.git

  • 11 SpirosK // Feb 27, 2009 at 03:29 AM

    Thank you! After some (sleep and) fiddling about, I found out that this whole Undefined "auto_complete_for" is because of the "forgery protection" thing, which of course you had already mentioned... In any case, I am autocompleting in a combined field (that is, i need to autocomplete in both the name, surname and "official title") and not just one attribute, so this "auto_complete_for" is not needed anyway, I have written my own function for auto_completion. Thanks for your answer, i am sure i will make good use of your plugin ;-)
  • 12 fahri // Aug 24, 2009 at 12:22 PM

    Hi pat, thanks for the great post. I am new into RoR and can't seem to get your version of auto_complete running. When I install your version of the plugin and rename the old version to something else. I get this error from my form view: undefined method `auto_complete_fields_for' for #<actionview::base:0x26e4054> I also wanted to peek into the example app you provided bu the second git command: git checkout origin/auto_complete returns: fatal: Not a git repository (or any of the parent directories): .git so its another dead end for me there as well. What do you suggest I should do?
  • 13 pat // Aug 24, 2009 at 11:11 PM

    Hi Fahri, sorry I haven’t gotten around to updating this sample app yet; I will definitely get to it soon. As I mentioned at the top, in June I made some changes to the my auto_complete plugin that make it easier to use; see: http://patshaughnessy.net/2009/6/15/repeated-auto-complete-plugin-usage-change. In a nutshell, you don’t need “auto_complete_fields_for” any more; just use form_for or fields_for. This would explain your error message. Also, don’t rename and keep the original auto_complete plugin – just delete it. Rails will load any code under vendor/plugins, so having both copies of the same plugin with different names will probably cause problems.

    With regard to the git commands: looks like I forgot “cd complex-form-examples” between the git clone command and the git checkout command. After running “git clone…” you should have a new subfolder called “complex-form-examples” – just cd into that folder before running the rest of the commands. I'll add this missing command to the article right now...

    Thanks for trying the plugin!

Leave a Comment