Two text_field_with_auto_complete field with the same search content

Hi all,

I have a form with two text_field_with_auto_complete s, both of which do a lookup from the same table:

          <!-- good guys --><td><%= text_field_with_auto_complete :person, :name %></td>           <!-- bad guys --><td><%= text_field_with_auto_complete :person, :name %></td>

Unsurprisingly, this produces identical output for both:

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

Is there a way to set the id of the field to something other than the default?

Thanks,

Todd

text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {})

Well, you can change the id with the tag_options hash, but the generated javascript won't know about it. So, let's improve text_field_with_auto_complete so that it does work.

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

The content_tag and the auto_complete_field methods both set the ids of elements without consulting what id you passed to the tag_options hash. We should change those two calls to match whatever :id option was passed in, if in case it was passed in.

So...

def text_field_with_auto_complete_with_id_checking(object, method, tag_options = {}, completion_options = {})   # lets just set the id now and not worry about it throughout   id = tag_options[:id] || "#{object}_#{method}"

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

To get this to work just place it in your application_helper.rb (and call it with text_field_with_auto_complete_with_id_checking(:person, :name, {:id => 'bad_person'})

This seems to work for me but it doesn't have any tests written for it as I just threw it together... I'll leave that to someone else.

Julian

Brilliant! That works a treat.

So a follow on question:

How do I get access to the completion_options in my view? I need to add some conditions to the search in my person controller ("filter on org")?

view:      <!-- good guys --><td><%= text_field_with_auto_complete :person, :name, { :id => 'goodguy' } { :org => "justice_league" } %></td>      <!-- bad guys --><td><%= text_field_with_auto_complete :person, :name, { :id => 'badguy' } { :org => "legion_of_doom" } %></td>

controller:      def auto_complete_for_person_name         ...         find_options = { :conditions => "org='" + ??? + "'", :order => "name ASC" }         @entities = Person.find(:all, find_options).collect(&:name).select { |person| person.match re }

As an aside: coming from Java (and Eclipse), the hardest part of picking up Rails is the lack of a "discoverable API". If you have any pointers (books, links, etc), I'd be much obliged.

Thanks again,

Todd

One of the completion_options is :with. If you don't use the :with option the default parameter available to the action is the value of the text field. However, once you do use the :with option you have to specify all the parameters available to the action including the one that was sent by default (the "'person[name]=' + $('goodguy').value" part).

text_field_with_auto_complete :person, :name, { :id => 'goodguy' } { :with => "'person[name]=' + $('goodguy').value + '&person[org]=justice_league'"}

What's a "discoverable API"?

I just use api.rubyonrails.org

One of the completion_options is :with. If you don't use the :with option the default parameter available to the action is the value of the text field. However, once you do use the :with option you have to specify all the parameters available to the action including the one that was sent by default (the "'person[name]=' + $('goodguy').value" part).

text_field_with_auto_complete :person, :name, { :id => 'goodguy' } { :with => "'person[name]=' + $('goodguy').value + '&person[org]=justice_league'"}

Thanks again! Once again, this did the trick.

What's a "discoverable API"?

I just use api.rubyonrails.org

Not to send this thread spinning off into another universe, but: it seems that in a number of Rails methods, one or more arguments is a Hash. No problem with it in principle, but it's not transparent ("discoverable") from the signature what values will do what. For comparison, with Java + Eclipse, it's clear what data should be sent to the method.

I'm quite enamored with Rails, but this point is the hardest adjustment coming from a statically-typed language.

Todd

It's quite easy once you understand how it works.

As syntactic sugar Ruby builds a single hash from any trailing hash-like arguments and passes *it* as the corresponding positional argument. For example in

   def foo(x, y=0)      ...    end

the call

   foo(:a => 1, :b => 2, :c => 3)

is equivalent to

   foo({:a => 1, :b => 2, :c => 3})

and both of them result in

   x = {:a => 1, :b => 2, :c => 3}    y = 0

In that situation, how can you pass a -7 for y? You have to make explicit the 1st argument:

   foo({:a => 1, :b => 2, :c => 3}, -7)

That's the basic stuff. Other than that what you need to do with those helpers, as with any other API, is to read its documentation. In particular their signature.

After some further tinkering, I'm able to get 'goodguy' and 'badguy' values passed to the controller (the 'method per id' is far from ideal....):

entry.rhtml

        <p>             <label for="attr_badguy_name">Bad Guy:</label>             <%= text_field_with_auto_complete_with_id_checking :entity, :name, { :id => "badguy" }, { :with => "'badguy[name]=' + $('badguy').value + '&entity[schema_type]=complex'"} %>         </p>         <p>             <label for="attr_type_name">Good Guy:</label>             <%= text_field_with_auto_complete_with_id_checking :goodguy, :name, { :id => "goodguy" }, { :with => "'goodguy[name]=' + $('goodguy').value" } %>         </p>

After some further tinkering, I'm able to get 'goodguy' and 'badguy' values passed to the controller (the 'method per id' is far from ideal....):

don't forget to use encodeURIComponent or bad things will happen if you type & into the box.

Fred

A couple of suggestions:

First, you'll be better of using the database to search through the names for matches as that should be faster then pulling all the names and running them through a regular expression. You can use (works with mysql, though I think it's standard SQL) :conditions => ["name LIKE ?", "#{name}%"]. The LIKE will match case insensitive and allows the wildcard character (the percent sign) and the placeholder substitution will prevent sql injection attacks.

Second, I think the point of my hack to text_field_with_auto_complete was to allow the controller to have one method. So you could have just auto_complete_for_person_name where it's something like this.

def auto_complete_for_person_name   @people = Person.find(:all,     :conditions => ["name LIKE ? AND org = ?", "#{params[:person] [:name]}%", params[:person][:org]],     :order => "name ASC")   unless @people.blank?     render :inline => "<%= content_tag(:ul,       @people.map{|person|         content_tag(:li, h(person)) }) %>"   else     render :inline => ""   end end

Assuming you always pass the [person][org] parameter, which you don't seem to be doing anymore.

Also, Frederick's point is very important! (My mistake)

So the final text_field_with_auto_complete_with_id_checking call should look like this:

text_field_with_auto_complete_with_id_checking :person, :name, {:id => "badguy"},   {:with => "'person[name]=' + encodeURIComponent($('badguy').value) + '&person[org]=legion_of_doom'"}

Finally, in your controller you will want to make sure that @people is not blank before sending it to map to avoid:

NoMethodError: undefined method `map' for nil:NilClass

so:

unless @people.blank?   render ... else   render :inline => "" end

If you do need two different controller methods for some other reason then you should still take the similar stuff and stick it in a helper or private method.

Julian

Julian,

Your solution works for providing and filtering the completion suggestions. The problem I'm running into is what params get sent on submit:

  Parameters: {"commit"=>"Add People", "person"=>{"name"=>"Superman"}, "authenticity_token"=>"033d880da069739bd1b80f42e11c6008e 54971b9", "action"=>"save_matchup", "controller"=>"matchup", "matchup"=>{"name"=>"foo", "description"=>"sdafasdf"}}

In this case, "Superman" is the value in the first text field. The generated HTML reveals that both text auto-complete boxes have the same name:

<input autocomplete="off" id="goodguy" name="person[name]" ...

<input autocomplete="off" id="badguy" name="person[name]" ...

It looks like I need a unique "name" attribute as well as a unique id (if I actually need a unique id at all).

Todd

Well, now that you've brought up the submit part... yeah, the names would have to be different too. So let's just take my text_field_with_auto_complete hack and crumple it up and throw it in the trash (or maybe hold on to it for another day).

Let's go back to using the straight up text_field_with_auto_complete but move common stuff into the model.

Model:

def self.search_by_name(name, org = nil)   conditions =     if org      ["name LIKE ? AND org = ?",        "#{name}%",        "#{org}"]     else       ["name LIKE ?", "#{name}%"]     end   Person.find(:conditions => conditions,     :order => "name ASC") end

Controller:

def auto_complete_for_goodguy_name   people = Person.search_by_name(     params[:goodguy][:name], "justice_league")   render :partial => 'auto_complete_results',     :locals => {:people => people} end

def auto_complete_for_badguy_name   people = Person.search_by_name(     params[:goodguy][:name], "legion_of_doom")   render :partial => 'auto_complete_results',     :locals => {:people => people} end

View:

the form:

<!-- good guys --><%= text_field_with_auto_complete :goodguy, :name %> <!-- bad guys --><%= text_field_with_auto_complete :badguy, :name %>

And a partial _auto_complete_results.html.erb:

<% unless people.blank? -%>   <%= content_tag(:ul,     people.map{|person| content_tag(:li, h(person))} ) %> <% end -%>

Or something like that.

As you can tell, I haven't really tested any of this code I've been tossing up here... :frowning:

the Person.find call in the search_by_name method should be

Person.find(:all, :conditions => conditions,   :order => "name ASC")

I think I may have cracked it.

In the view, I explicitly set the id in the :with clause. In the person_controller, I get the value of the text field by grabbing the 'id' name out of the parameters, and using the value of 'id' as key to get the value of the textbox. In the application_helper, I pass the value of tag_options[:id] to the text_field generator instead of :object.

Thanks for your help, hopefully this will help the next guy :).

Todd

view: <%= text_field_with_auto_complete_with_id_checking :person, :name, { :id => "badguy" }, { :with => "'id=badguy&badguy[name]=' + encodeURIComponent($('badguy').value) + '&person[org]=legionofdoom"} %>

person_controller:   def auto_complete_for_person_name     id = params[:id]     @people = person.find(:all,                    :conditions => ["name LIKE ? AND org=?", "#{params[id][:name]}%", "#{params[person][:org]}%"],                    :order => "name ASC")     unless @people.blank?       render :inline => "<%= content_tag(:ul, @people.map{|person| content_tag(:li, h(person)) }) %>"     else       render :inline => ""     end   end

application_helper:   def text_field_with_auto_complete_with_id_checking(object, method,                                                      tag_options = {}, completion_options = {})     # lets just set the id now and not worry about it throughout     id = tag_options[:id] || "#{object}_#{method}"

     (completion_options[:skip_style] ? "" : auto_complete_stylesheet)