Rails' observe_field with :on - Rails 2.0

in Javascript, Prototype, ruby on rails, web development

In a previous post I showed how to get around a bug in Prototype that was ignoring the "on" parameter of the observe_field function. The solution I originally posted no longer works with the latest version of Rails and Prototype. Although the documentation for observe_field received a facelift, somehow the Rails team still missed the fact that the :on function doesn't work with the current version of Prototype. Or maybe its the Prototype guys we should be after here. :-)

The new fix requires a change to both prototype.js and a small Rails core update.

I will start with the update to prototype.js. Search for Abstract.EventObserver (around line 3632 in my version). That should now read

Abstract.EventObserver = Class.create({
  initialize: function(element, callback, trigger) {
    this.element  = $(element);
    this.callback = callback;
    this.trigger = trigger;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element, this.trigger);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element, trigger) {
    if (trigger == '') {
      if (element.type) {
        switch (element.type.toLowerCase()) {
          case 'checkbox':
          case 'radio':
            Event.observe(element, 'click', this.onElementEvent.bind(this));
            break;
          default:
            Event.observe(element, 'change', this.onElementEvent.bind(this));
            break;
        }
      }
    }
    else Event.observe(element, trigger, this.onElementEvent.bind(this));
  }
});

Since we're passing the parameters explicitly now, we must make a change to the build_observer method in ActionView::Helpers::PrototypeHelper. (vendor/rails/actionpack/lib/action_view/helpers/prototype_helper.rb)

This method is adding the "on" parameter to the EventObserver initialization only if the "on" option is passed in. Yes, even though prototype isn't supporting it in its current state. So, remove the if options[:on] from the end of line 779 of prototype_helper.rb. The method should now look like this:

def build_observer(klass, name, options = {})
  if options[:with] && (options[:with] !~ /[\{=(.]/)
    options[:with] = "'#{options[:with]}=' + value"
  else
    options[:with] ||= 'value' unless options[:function]
  end

  callback = options[:function] || remote_function(options)
  javascript  = "new #{klass}('#{name}', "
  javascript << "#{options[:frequency]}, " if options[:frequency]
  javascript << "function(element, value) {"
  javascript << "#{callback}}"
  javascript << ", '#{options[:on]}'"
  javascript << ")"
  javascript_tag(javascript)
end

That should do it. Someone please let me know if this doesn't work for them.

UPDATE: 12/14/2007

As an alternative to modifying core, you can always extend Prototype by creating your own customer observer that takes in the trigger argument shown above. Doing this prohibits you from using the built in Prototype helper method in rails (observe_field), but if you don't feel comfortable modifying core this is a way around it.

Comments

You rock! I can't wait to

You rock! I can't wait to try it :)

observer_field

Exactly which prototype.js is suppose to be modified as I found several in my project:

public/javascripts/prototype.js:Abstract.EventObserver = function() {}
scripts/server/public/javascripts/prototype.js:Abstract.EventObserver = function() {}
server/script/public/javascripts/prototype.js:Abstract.EventObserver = function() {}

Also, does this solution fix the problem with the radio button where only the first click invokes the callback and never after?

Post new comment

The content of this field is kept private and will not be shown publicly.