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)