Ruby on Rails
AssociationHelper (Version #2)

Objects rarely stand on their own. Commonly they refer to others via associations, resulting in the equally common requirement to choose an associated—referred to—object from a list of candidates. The code on this page helps with just that. It provides helper methods for inserting fields and popups for displaying and selecting associated objects. Eligible candidates are retrieved from the server behind the scenes with an “AJAX” request; only candidates matching given search criteria are returned. For the time being, only objects related through belongs_to are supported.

—MichaelSchuerig

lib/association_helper.rb:


# Author: Michael Schuerig, <a href="mailto:michael@schuerig.de">michael@schuerig.de</a>, 2005
#
# Free for all uses. No warranty or anything. Comments welcome.
#
#  What is it?
#
# The Association Helper methods create widgets for choosing
# associated objects. Available choices are retrieved "AJAX-style" 
# behind the scenes.
#
# Currently, only +belongs_to+ references are supported.
# Keep in mind, though, that +has_one+ references are just the other end
# of +belongs_to+ references.
#
#  Installation
#
#  public/
#      javascripts/
#          association-helper.js
#          prototype.js  –  included in Rails distribution
#
# Then, in the head section of your page templates, possibly in a layout,
# include the necessary files like this:
#
#  <= javascript_include_tag ‘prototype’, ‘association-helper’ >
#
#  Usage
#
# = In Views
#
# See the documentation of the individual methods.
#
# === In Controllers
#
# In controllers of views that use the helper methods, you have to make
# these methods known like this.
#
#  class SomethingController? < ApplicationController
#    helper :association
#
# In the controllers queried for candidate associate objects, you need
# to provide an action method that handles the respective requests.
# You can implement them manually, of course.
# For a large number of cases this will not be necessary, hopefully,
# when you use RailsExtensions?::AjaxSupport.define_search_action.
#
#
# == TODO
#
#  All the other kinds of associations
#  Unit tests…
#
module AssociationHelper # Creates a popup for choosing a “belongs_to”-associated object # displayed in a preexisting field. # The popup is triggered by clicking or focussing the display field. # # Options are: # # ontroller – controller to be used for AJAX requests; default: downcased name of the associated class. # ction – action to be used for AJAX requests; default: ‘ajax’. (FIXME: really?) # uery_param – request parameter name for the query string; default: ‘query’. # opup_class – class attribute for the main popup div; default: ‘popup’. # ptional – is the associated object optional (allowed to be null)? default: false. # def belongs_to_popup(object_name, association_name, options = {}) controller = options[:controller] || controller_for_search(object_name, association_name) action = options[:action] || ‘ajax’ query_param = options[:query_param] || ‘query’ popup_div_class = options[:popup_class] || ‘popup’ optional = options[:optional] || false fk_attribute = fk_attribute_for(object_name, association_name) target_field_id = object_name ’_’ association_name popup_id = target_field_id + ’_popup’ query_field_id = popup_id + ’_query’ result_list_id = popup_id + ’_list’ fkField = hidden_field(object_name, fk_attribute) observer = observe_field(query_field_id, :frequency => 0.5, :update => result_list_id, :url => { :controller => controller, :action => action }, :with => ”’#{query_param}=’ + escape(value)”)

<


#{observer}
END end

  1. Creates a text field with associated popup for choosing a
  2. “belongs_to”-associated object.
  3. The popup is triggered by clicking or focussing the display field. #
  4. A block, when given, is used to set the initial contents of
  5. the text field. The block receives the initially selected object,
  6. which may be nil. #
  7. Example: #
  8. <= belongs_to_field(‘project’, ‘manager’) { |it| it.name if it } > #
  9. This snippet creates a read-only text field. When that field
  10. gains the focus or is clicked, a query input field pops up
  11. on top of it. As soon as the user starts to type, matching
  12. objects are retrieved from the server and displayed in a
  13. list below the query field. #
  14. Options are: #
  15. ontroller – controller to be used for AJAX requests; default: downcased name of the associated class.
  16. ction – action to be used for AJAX requests; default: ‘ajax’. (FIXME: really?)
  17. uery_param – request parameter name for the query string; default: ‘query’.
  18. opup_class – class attribute for the main popup div; default: ‘popup’.
  19. ptional – is the associated object optional (allowed to be null)? default: false.
  20. +:size – size of the created field; default: none. # def belongs_to_field(object_name, association_name, options = {}) raw_value = value_for(object_name, association_name) value = block_given? ? yield(raw_value) : raw_value.to_s

    field = text_field(object_name, association_name, :size => options[:size], :readonly => true, :name => ’’, :value => value)

    field + belongs_to_popup(object_name, association_name, options)

    end
private
def object_for(object_name)
  self.instance_variable_get("@#{object_name}")
end
def class_for(object_name)
  if object = object_for(object_name)
    object.class
  end
end
def value_for(object_name, method_name)
  if object = object_for(object_name)
    object.send(method_name)
  end
end
def options_for_association(object_name, association_name, &block)
  if klass = class_for(object_name)
    if meta = klass.reflect_on_association(association_name.to_sym)
      options = meta.options
      if block_given?
        yield options
      else
        options
      end
    end
  end
end
  1. Heuristically uses the downcased name of the associated class
  2. as the controller name. def controller_for_search(object_name, association_name) options_for_association(object_name, association_name) do |options| Inflector.demodulize(options[:class_name] || association_name).downcase end end
def fk_attribute_for(object_name, association_name)
  options_for_association(object_name, association_name) do |options|
    options[:foreign_key] || association_name + '_id'
  end
end
def association_for(object_name, association_name)
  if object = object_for(object_name)
    object.send(association_name)
  end
end

end

public/javascripts/association-helper.js:


// AUTHOR: Michael Schuerig, <a href="mailto:michael@schuerig.de">michael@schuerig.de</a>, 2005
// Free for all uses. No warranty or anything. Comments welcome.
//
// REQUIREMENTS:
// * prototype.js - <a href="http://prototype.conio.net/">http://prototype.conio.net/</a> (included with Ruby on Rails)
//

var <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span> = Class.create();

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getTargetElement = function(evt) {
    var f = (window.event) ? window.event.srcElement : evt.target;
    while (f.nodeType != 1)
        f = f.parentNode;
    return f;
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent = function(evt) {
    evt || (evt = window.event);
    if (evt.preventDefault) {
        evt.preventDefault();
        evt.stopPropagation();
    } else if (evt.cancelBubble) {
        evt.cancelBubble = true;
        evt.returnValue = false;
    }
    return false;
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent = function(el, evtname, func) {
    if (el.attachEvent) {
        el.attachEvent("on" + evtname, func);
    } else {
        el["on" + evtname] = func;
    }
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent = function(el, evtname, func) {
    if (el.detachEvent) {
        el.detachEvent("on" + evtname, func);
    } else if (el.removeEventListener) {
        el.removeEventListener(evtname, func, true);
    } else {
        el["on" + evtname] = null;
    }
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getAbsolutePos = function(el) {
    var SL = 0, ST = 0;
    var is_div = /^div$/i.test(el.tagName);
    if (is_div && el.scrollLeft)
        SL = el.scrollLeft;
    if (is_div && el.scrollTop)
        ST = el.scrollTop;
    var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
    if (el.offsetParent) {
        var tmp = this.getAbsolutePos(el.offsetParent);
        r.x += tmp.x;
        r.y += tmp.y;
    }
    return r;
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.prototype = {

  initialize: function(targetFieldId, optional, targetFkFieldId, popupId, queryId, listId) {
    this.targetTextField = $(targetFieldId);
    this.optional = optional || false;

    this.targetFkField = targetFkFieldId ? $(targetFkFieldId) : $(targetFieldId + '_id');
    var realPopupId = popupId || targetFieldId + '_popup';
    this.popup = $(realPopupId);
    this.queryField = queryId ? $(queryId) : $(realPopupId + '_query');
    this.listDiv = listId ? $(listId) : $(realPopupId + '_list');

    if (! (this.popup && this.targetTextField && this.targetFkField && this.queryField && this.listDiv)) {
      this.popup = null; // mark as invalid
      return;
    }

    this.showPopup = this._showPopup.bindAsEventListener(this);
    this.hidePopup = this._hidePopup.bindAsEventListener(this);
    this.handleClick = this._handleClick.bindAsEventListener(this);
    this.handleKeyDown = this._handleKeyDown.bindAsEventListener(this);
    this.handleMouseDown = this._handleMouseDown.bindAsEventListener(this);

    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(this.targetTextField, 'focus', this.showPopup);

    this._reparentToBody();
  },

  _reparentToBody: function() {
    if (!this.popup) return;
    var __reparent = function(popup) {
      popup.parentNode.removeChild(popup);
      document.body.appendChild(popup);
    }
    setTimeout(__reparent, 250, this.popup);
  },

  _showPopup: function(evt) {
    if (!this.popup) return;
    if (this.popup.style.display == 'block') return;

    // For whatever reason, control gets here twice in khtml/kjs
    // Apparently visibility lags behind when setting the display property.
    // There's just a single thread -- or not?

    __showPopup = function() {
      if (this.popup.style.display == 'block') return;
      var pos = <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getAbsolutePos(this.targetTextField);
      this.popup.style.top = pos.y + 'px';
      this.popup.style.left = pos.x + 'px';

      this.queryField.value = this.targetTextField.value

      <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(this.listDiv, 'click', this.handleClick);
      <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(document, 'keydown', this.handleKeyDown);
      <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(document, 'mousedown', this.handleMouseDown);

      this.popup.style.display = 'block';

      Field.activate(this.queryField);
    }
    setTimeout(__showPopup.bind(this), 10);
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent(evt);
  },

  _hidePopup: function() {
    this.popup.style.display = 'none';
    this.queryField.value = '';
    this.listDiv.innerHTML = '';

    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent(this.listDiv, 'click', this.handleClick);
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent(document, 'keydown', this.handleKeyDown);
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent(document, 'mousedown', this.handleMouseDown);
  },

  _handleClick: function(evt) {
    var target = <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getTargetElement(evt);
    var choice = null;
    if (target.tagName == 'OPTION') {
        choice = target;
    } else if (target.tagName == 'SELECT') {
        var opts = target.options;
        for (var i = 0; i < opts.length; i++) {
            if (opts[i].selected) {
                choice = opts[i];
                break;
            }
        }
    }
    if (choice) {
        this.targetTextField.value = choice.text;
        this.targetFkField.value = choice.value;
    } else if (this.optional) {
        this.targetTextField.value = '';
        this.targetFkField.value = '';
    }
    this.hidePopup();
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent(evt);
  },

  _handleMouseDown: function(evt) {
    var el = <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getTargetElement(evt);
    while (el != null && el != this.popup) {
        el = el.parentNode;
    }
    if (el == null) {
        this.hidePopup();
    }
  },

  _handleKeyDown: function(evt) {
    switch (evt.keyCode) {
        case 27: // esc
            this.hidePopup();
            break;
        case 13: // enter
            this.handleClick(evt);
            break;
        default:
            return true;
    }
    return <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent(evt);
  }
}

lib/ajax_support.rb:


# Author: Michael Schuerig, <a href="mailto:michael@schuerig.de">michael@schuerig.de</a>, 2005
#
# Free for all uses. No warranty or anything. Comments welcome.
#
module <span class="newWikiWord">RailsExtension<a href="http://wiki.rubyonrails.org/rails/pages/RailsExtension">?</a></span>
  module <span class="newWikiWord">AjaxSupport<a href="http://wiki.rubyonrails.org/rails/pages/AjaxSupport">?</a></span> # :nodoc:

    def self.append_features(base)
      super
      base.extend(ClassMethods)
    end

    # == Installation
    #
    # Either
    #
    #  require_dependency 'ajax_support'
    #
    #  class <span class="newWikiWord">SomethingController<a href="http://wiki.rubyonrails.org/rails/pages/SomethingController">?</a></span> < <a href="http://wiki.rubyonrails.org/rails/pages/ApplicationController" class="existingWikiWord">ApplicationController</a>
    #    include <span class="newWikiWord">RailsExtension<a href="http://wiki.rubyonrails.org/rails/pages/RailsExtension">?</a></span>::AjaxSupport
    #
    #
    # in your controller source files
    # or, at the end of +config+/+environment+.+rb+ put
    #
    #  require 'ajax_support'
    #
    #  <a href="http://wiki.rubyonrails.org/rails/pages/ActionController" class="existingWikiWord">ActionController</a>::Base.class_eval do
    #    include <span class="newWikiWord">RailsExtension<a href="http://wiki.rubyonrails.org/rails/pages/RailsExtension">?</a></span>::AjaxSupport
    #  end
    #
    # in order to have this facility available in all controllers.
    #
    module <span class="newWikiWord">ClassMethods<a href="http://wiki.rubyonrails.org/rails/pages/ClassMethods">?</a></span>

      # Adds an action method to a controller.
      # The added method is suitable for handling AJAX requests
      # sending a query containing search values for attributes
      # and expecting a response consisting of a +select+ element
      # with #option#s matching the query.
      #
      # In the controller use this method like this:
      #
      #  class <span class="newWikiWord">SomethingElseController<a href="http://wiki.rubyonrails.org/rails/pages/SomethingElseController">?</a></span> < <a href="http://wiki.rubyonrails.org/rails/pages/ApplicationController" class="existingWikiWord">ApplicationController</a>
      #    define_search_action :ajax, Person, '%lastname%, %firstname%', :limit => 10, :select_size => 10
      #
      # The above code endows your controller with an action method that
      # handles 'ajax' requests by calling +find+ on the +Person+ +ActiveRecord+
      # class.
      # The third parameter is the +format+ string used for parsing queries
      # and formatting result items. In the format string, placeholders for
      # attributes are marked as +%++attribute_name++%+ (yes, '+%+' at both ends.)
      #
      # The +query+ parameter string is split into items separated by non-word
      # characters (such as punctuation). The items, unless empty, are used
      # to search in the attributes with the corresponding positions in the
      # format string.
      # Found objects are sorted by the attributes given in the
      # format string with attributes present in the query given higher
      # importance.
      # Also, the format string is used to format the response text.
      #
      def define_search_action(name, klass, format, options = {})
        columns = format.scan(/%(\w+?)%/iou).map { |c| c[0] }
        template = format.gsub('%', '%%').gsub(/%%\w+?%%/iou, '%s')

        define_method(name) do
          do_ajax_search(klass, columns, template, options)
        end
      end

    end

    private

    def do_ajax_search(klass, columns, template, options)
      query = @params[:query]
      if query.nil? or (query_values = query.split(/\s*\W\s*/iou)).empty?
        render_nothing
        return
      end

      select_size = options[:select_size] || '5'

      condition_items = []
      values = []
      order = []
      columns.zip(query_values).each do |col, value|
        if value and !value.empty?
          condition_items << "lower(#{col}) like ?" 
          values << '%' + value.downcase + '%'
          order << col
        end
      end

      conditions = [ condition_items.join(' and ') ] + values
      order += (columns - order)
      order = order.join(',')

      hits = klass.find(:all, options.merge(:conditions => conditions, :order => order))

      response = "<select size=\"#{select_size}\">\n" 
      hits.each do |hit|
        response << "<option value=\"#{hit.id}\">" 
        response << template % columns.collect { |c| hit.send(c) }
        response << "</option>\n" 
      end
      response << "</select>\n" 

      render_text response
    end

  end
end

Objects rarely stand on their own. Commonly they refer to others via associations, resulting in the equally common requirement to choose an associated—referred to—object from a list of candidates. The code on this page helps with just that. It provides helper methods for inserting fields and popups for displaying and selecting associated objects. Eligible candidates are retrieved from the server behind the scenes with an “AJAX” request; only candidates matching given search criteria are returned. For the time being, only objects related through belongs_to are supported.

—MichaelSchuerig

lib/association_helper.rb:


# Author: Michael Schuerig, <a href="mailto:michael@schuerig.de">michael@schuerig.de</a>, 2005
#
# Free for all uses. No warranty or anything. Comments welcome.
#
#  What is it?
#
# The Association Helper methods create widgets for choosing
# associated objects. Available choices are retrieved "AJAX-style" 
# behind the scenes.
#
# Currently, only +belongs_to+ references are supported.
# Keep in mind, though, that +has_one+ references are just the other end
# of +belongs_to+ references.
#
#  Installation
#
#  public/
#      javascripts/
#          association-helper.js
#          prototype.js  –  included in Rails distribution
#
# Then, in the head section of your page templates, possibly in a layout,
# include the necessary files like this:
#
#  <= javascript_include_tag ‘prototype’, ‘association-helper’ >
#
#  Usage
#
# = In Views
#
# See the documentation of the individual methods.
#
# === In Controllers
#
# In controllers of views that use the helper methods, you have to make
# these methods known like this.
#
#  class SomethingController? < ApplicationController
#    helper :association
#
# In the controllers queried for candidate associate objects, you need
# to provide an action method that handles the respective requests.
# You can implement them manually, of course.
# For a large number of cases this will not be necessary, hopefully,
# when you use RailsExtensions?::AjaxSupport.define_search_action.
#
#
# == TODO
#
#  All the other kinds of associations
#  Unit tests…
#
module AssociationHelper # Creates a popup for choosing a “belongs_to”-associated object # displayed in a preexisting field. # The popup is triggered by clicking or focussing the display field. # # Options are: # # ontroller – controller to be used for AJAX requests; default: downcased name of the associated class. # ction – action to be used for AJAX requests; default: ‘ajax’. (FIXME: really?) # uery_param – request parameter name for the query string; default: ‘query’. # opup_class – class attribute for the main popup div; default: ‘popup’. # ptional – is the associated object optional (allowed to be null)? default: false. # def belongs_to_popup(object_name, association_name, options = {}) controller = options[:controller] || controller_for_search(object_name, association_name) action = options[:action] || ‘ajax’ query_param = options[:query_param] || ‘query’ popup_div_class = options[:popup_class] || ‘popup’ optional = options[:optional] || false fk_attribute = fk_attribute_for(object_name, association_name) target_field_id = object_name ’_’ association_name popup_id = target_field_id + ’_popup’ query_field_id = popup_id + ’_query’ result_list_id = popup_id + ’_list’ fkField = hidden_field(object_name, fk_attribute) observer = observe_field(query_field_id, :frequency => 0.5, :update => result_list_id, :url => { :controller => controller, :action => action }, :with => ”’#{query_param}=’ + escape(value)”)

<


#{observer}
END end

  1. Creates a text field with associated popup for choosing a
  2. “belongs_to”-associated object.
  3. The popup is triggered by clicking or focussing the display field. #
  4. A block, when given, is used to set the initial contents of
  5. the text field. The block receives the initially selected object,
  6. which may be nil. #
  7. Example: #
  8. <= belongs_to_field(‘project’, ‘manager’) { |it| it.name if it } > #
  9. This snippet creates a read-only text field. When that field
  10. gains the focus or is clicked, a query input field pops up
  11. on top of it. As soon as the user starts to type, matching
  12. objects are retrieved from the server and displayed in a
  13. list below the query field. #
  14. Options are: #
  15. ontroller – controller to be used for AJAX requests; default: downcased name of the associated class.
  16. ction – action to be used for AJAX requests; default: ‘ajax’. (FIXME: really?)
  17. uery_param – request parameter name for the query string; default: ‘query’.
  18. opup_class – class attribute for the main popup div; default: ‘popup’.
  19. ptional – is the associated object optional (allowed to be null)? default: false.
  20. +:size – size of the created field; default: none. # def belongs_to_field(object_name, association_name, options = {}) raw_value = value_for(object_name, association_name) value = block_given? ? yield(raw_value) : raw_value.to_s

    field = text_field(object_name, association_name, :size => options[:size], :readonly => true, :name => ’’, :value => value)

    field + belongs_to_popup(object_name, association_name, options)

    end
private
def object_for(object_name)
  self.instance_variable_get("@#{object_name}")
end
def class_for(object_name)
  if object = object_for(object_name)
    object.class
  end
end
def value_for(object_name, method_name)
  if object = object_for(object_name)
    object.send(method_name)
  end
end
def options_for_association(object_name, association_name, &block)
  if klass = class_for(object_name)
    if meta = klass.reflect_on_association(association_name.to_sym)
      options = meta.options
      if block_given?
        yield options
      else
        options
      end
    end
  end
end
  1. Heuristically uses the downcased name of the associated class
  2. as the controller name. def controller_for_search(object_name, association_name) options_for_association(object_name, association_name) do |options| Inflector.demodulize(options[:class_name] || association_name).downcase end end
def fk_attribute_for(object_name, association_name)
  options_for_association(object_name, association_name) do |options|
    options[:foreign_key] || association_name + '_id'
  end
end
def association_for(object_name, association_name)
  if object = object_for(object_name)
    object.send(association_name)
  end
end

end

public/javascripts/association-helper.js:


// AUTHOR: Michael Schuerig, <a href="mailto:michael@schuerig.de">michael@schuerig.de</a>, 2005
// Free for all uses. No warranty or anything. Comments welcome.
//
// REQUIREMENTS:
// * prototype.js - <a href="http://prototype.conio.net/">http://prototype.conio.net/</a> (included with Ruby on Rails)
//

var <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span> = Class.create();

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getTargetElement = function(evt) {
    var f = (window.event) ? window.event.srcElement : evt.target;
    while (f.nodeType != 1)
        f = f.parentNode;
    return f;
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent = function(evt) {
    evt || (evt = window.event);
    if (evt.preventDefault) {
        evt.preventDefault();
        evt.stopPropagation();
    } else if (evt.cancelBubble) {
        evt.cancelBubble = true;
        evt.returnValue = false;
    }
    return false;
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent = function(el, evtname, func) {
    if (el.attachEvent) {
        el.attachEvent("on" + evtname, func);
    } else {
        el["on" + evtname] = func;
    }
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent = function(el, evtname, func) {
    if (el.detachEvent) {
        el.detachEvent("on" + evtname, func);
    } else if (el.removeEventListener) {
        el.removeEventListener(evtname, func, true);
    } else {
        el["on" + evtname] = null;
    }
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getAbsolutePos = function(el) {
    var SL = 0, ST = 0;
    var is_div = /^div$/i.test(el.tagName);
    if (is_div && el.scrollLeft)
        SL = el.scrollLeft;
    if (is_div && el.scrollTop)
        ST = el.scrollTop;
    var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
    if (el.offsetParent) {
        var tmp = this.getAbsolutePos(el.offsetParent);
        r.x += tmp.x;
        r.y += tmp.y;
    }
    return r;
};

<span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.prototype = {

  initialize: function(targetFieldId, optional, targetFkFieldId, popupId, queryId, listId) {
    this.targetTextField = $(targetFieldId);
    this.optional = optional || false;

    this.targetFkField = targetFkFieldId ? $(targetFkFieldId) : $(targetFieldId + '_id');
    var realPopupId = popupId || targetFieldId + '_popup';
    this.popup = $(realPopupId);
    this.queryField = queryId ? $(queryId) : $(realPopupId + '_query');
    this.listDiv = listId ? $(listId) : $(realPopupId + '_list');

    if (! (this.popup && this.targetTextField && this.targetFkField && this.queryField && this.listDiv)) {
      this.popup = null; // mark as invalid
      return;
    }

    this.showPopup = this._showPopup.bindAsEventListener(this);
    this.hidePopup = this._hidePopup.bindAsEventListener(this);
    this.handleClick = this._handleClick.bindAsEventListener(this);
    this.handleKeyDown = this._handleKeyDown.bindAsEventListener(this);
    this.handleMouseDown = this._handleMouseDown.bindAsEventListener(this);

    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(this.targetTextField, 'focus', this.showPopup);

    this._reparentToBody();
  },

  _reparentToBody: function() {
    if (!this.popup) return;
    var __reparent = function(popup) {
      popup.parentNode.removeChild(popup);
      document.body.appendChild(popup);
    }
    setTimeout(__reparent, 250, this.popup);
  },

  _showPopup: function(evt) {
    if (!this.popup) return;
    if (this.popup.style.display == 'block') return;

    // For whatever reason, control gets here twice in khtml/kjs
    // Apparently visibility lags behind when setting the display property.
    // There's just a single thread -- or not?

    __showPopup = function() {
      if (this.popup.style.display == 'block') return;
      var pos = <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getAbsolutePos(this.targetTextField);
      this.popup.style.top = pos.y + 'px';
      this.popup.style.left = pos.x + 'px';

      this.queryField.value = this.targetTextField.value

      <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(this.listDiv, 'click', this.handleClick);
      <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(document, 'keydown', this.handleKeyDown);
      <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.addEvent(document, 'mousedown', this.handleMouseDown);

      this.popup.style.display = 'block';

      Field.activate(this.queryField);
    }
    setTimeout(__showPopup.bind(this), 10);
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent(evt);
  },

  _hidePopup: function() {
    this.popup.style.display = 'none';
    this.queryField.value = '';
    this.listDiv.innerHTML = '';

    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent(this.listDiv, 'click', this.handleClick);
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent(document, 'keydown', this.handleKeyDown);
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.removeEvent(document, 'mousedown', this.handleMouseDown);
  },

  _handleClick: function(evt) {
    var target = <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getTargetElement(evt);
    var choice = null;
    if (target.tagName == 'OPTION') {
        choice = target;
    } else if (target.tagName == 'SELECT') {
        var opts = target.options;
        for (var i = 0; i < opts.length; i++) {
            if (opts[i].selected) {
                choice = opts[i];
                break;
            }
        }
    }
    if (choice) {
        this.targetTextField.value = choice.text;
        this.targetFkField.value = choice.value;
    } else if (this.optional) {
        this.targetTextField.value = '';
        this.targetFkField.value = '';
    }
    this.hidePopup();
    <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent(evt);
  },

  _handleMouseDown: function(evt) {
    var el = <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.getTargetElement(evt);
    while (el != null && el != this.popup) {
        el = el.parentNode;
    }
    if (el == null) {
        this.hidePopup();
    }
  },

  _handleKeyDown: function(evt) {
    switch (evt.keyCode) {
        case 27: // esc
            this.hidePopup();
            break;
        case 13: // enter
            this.handleClick(evt);
            break;
        default:
            return true;
    }
    return <span class="newWikiWord">BelongsToPopup<a href="http://wiki.rubyonrails.org/rails/pages/BelongsToPopup">?</a></span>.stopEvent(evt);
  }
}

lib/ajax_support.rb:


# Author: Michael Schuerig, <a href="mailto:michael@schuerig.de">michael@schuerig.de</a>, 2005
#
# Free for all uses. No warranty or anything. Comments welcome.
#
module <span class="newWikiWord">RailsExtension<a href="http://wiki.rubyonrails.org/rails/pages/RailsExtension">?</a></span>
  module <span class="newWikiWord">AjaxSupport<a href="http://wiki.rubyonrails.org/rails/pages/AjaxSupport">?</a></span> # :nodoc:

    def self.append_features(base)
      super
      base.extend(ClassMethods)
    end

    # == Installation
    #
    # Either
    #
    #  require_dependency 'ajax_support'
    #
    #  class <span class="newWikiWord">SomethingController<a href="http://wiki.rubyonrails.org/rails/pages/SomethingController">?</a></span> < <a href="http://wiki.rubyonrails.org/rails/pages/ApplicationController" class="existingWikiWord">ApplicationController</a>
    #    include <span class="newWikiWord">RailsExtension<a href="http://wiki.rubyonrails.org/rails/pages/RailsExtension">?</a></span>::AjaxSupport
    #
    #
    # in your controller source files
    # or, at the end of +config+/+environment+.+rb+ put
    #
    #  require 'ajax_support'
    #
    #  <a href="http://wiki.rubyonrails.org/rails/pages/ActionController" class="existingWikiWord">ActionController</a>::Base.class_eval do
    #    include <span class="newWikiWord">RailsExtension<a href="http://wiki.rubyonrails.org/rails/pages/RailsExtension">?</a></span>::AjaxSupport
    #  end
    #
    # in order to have this facility available in all controllers.
    #
    module <span class="newWikiWord">ClassMethods<a href="http://wiki.rubyonrails.org/rails/pages/ClassMethods">?</a></span>

      # Adds an action method to a controller.
      # The added method is suitable for handling AJAX requests
      # sending a query containing search values for attributes
      # and expecting a response consisting of a +select+ element
      # with #option#s matching the query.
      #
      # In the controller use this method like this:
      #
      #  class <span class="newWikiWord">SomethingElseController<a href="http://wiki.rubyonrails.org/rails/pages/SomethingElseController">?</a></span> < <a href="http://wiki.rubyonrails.org/rails/pages/ApplicationController" class="existingWikiWord">ApplicationController</a>
      #    define_search_action :ajax, Person, '%lastname%, %firstname%', :limit => 10, :select_size => 10
      #
      # The above code endows your controller with an action method that
      # handles 'ajax' requests by calling +find+ on the +Person+ +ActiveRecord+
      # class.
      # The third parameter is the +format+ string used for parsing queries
      # and formatting result items. In the format string, placeholders for
      # attributes are marked as +%++attribute_name++%+ (yes, '+%+' at both ends.)
      #
      # The +query+ parameter string is split into items separated by non-word
      # characters (such as punctuation). The items, unless empty, are used
      # to search in the attributes with the corresponding positions in the
      # format string.
      # Found objects are sorted by the attributes given in the
      # format string with attributes present in the query given higher
      # importance.
      # Also, the format string is used to format the response text.
      #
      def define_search_action(name, klass, format, options = {})
        columns = format.scan(/%(\w+?)%/iou).map { |c| c[0] }
        template = format.gsub('%', '%%').gsub(/%%\w+?%%/iou, '%s')

        define_method(name) do
          do_ajax_search(klass, columns, template, options)
        end
      end

    end

    private

    def do_ajax_search(klass, columns, template, options)
      query = @params[:query]
      if query.nil? or (query_values = query.split(/\s*\W\s*/iou)).empty?
        render_nothing
        return
      end

      select_size = options[:select_size] || '5'

      condition_items = []
      values = []
      order = []
      columns.zip(query_values).each do |col, value|
        if value and !value.empty?
          condition_items << "lower(#{col}) like ?" 
          values << '%' + value.downcase + '%'
          order << col
        end
      end

      conditions = [ condition_items.join(' and ') ] + values
      order += (columns - order)
      order = order.join(',')

      hits = klass.find(:all, options.merge(:conditions => conditions, :order => order))

      response = "<select size=\"#{select_size}\">\n" 
      hits.each do |hit|
        response << "<option value=\"#{hit.id}\">" 
        response << template % columns.collect { |c| hit.send(c) }
        response << "</option>\n" 
      end
      response << "</select>\n" 

      render_text response
    end

  end
end