Rails' observe_field with :function and :on

I've been fiddling with the observe_field element in Rails using the :function and :on parameters. Basically, I simply wanted to execute some javascript when the onkeyup event fired for a text box. The Rails Documentation lead me to the two parameters that I needed:

  • :function, Instead of making a remote call to a URL, you can specify a JavaScript function to be called.
  • :on, Specifies which event handler to observe. By default, it's set to "changed" for text fields and text areas and "click" for radio buttons and checkboxes. Use this parameter to change the watched event to whatever you want e.g. "blur", "focus", etc..

However, the :on parameter didn't appear to work, and after some further inspection of the Prototype code, I found that the parameter that the Rails code was generating wasn't even being used in Prototype.js.

So, I've modified the Prototype code slightly, to make things work. Here are my changes.

Abstract.EventObserver = function() {}
Abstract.EventObserver.prototype = {
  initialize: function() {
    this.element  = $(arguments[0]);
    this.callback = arguments[1];
    this.trigger  = arguments[2];

    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() {
    var elements = Form.getElements(this.element);
    for (var i = 0; i < elements.length; i++)
      this.registerCallback(elements[i]);
  },

  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));
    }
  }
}

Notice the 'trigger' argument in the constructor that gets passed into the registerCallback function. Its only used if its supplied. I suppose you could also define the :on parameter while specifying a :url parameter in the observe_field to take further control of when you do your ajax calls. Haven't tested that yet.

With these changes we are now able to add the observe_field in our view like so...

observe_field 'text_field_id', 
     {:function => "(value == '') ? 
         $('email_preview').innerHTML = 
              '<em>not entered yet</em>' :
         $('email_preview').innerHTML = 
              value.replace(/\\n/g, '<br/>');", 
     :on => 'keyup'}

I've added a little snippet that will replace carriage returns with HTML line breaks and also set a default value if no text is entered or the field gets cleared out.

Hope this helps someone else!

Comments

Holy cow, Thank You!!

I have been battling this stupid bug in the Prototype library all day. I came across your post, and finally realized that it was a bug, and not my programming skills. Thank you for saving me from banging my held against a brick wall even longer. Thanks even more for proposing a solution! I'm going to give it a shot.

-zul

zul, ditto! maybe i'll just

zul, ditto!
maybe i'll just stick with frequency => 0.5 for now in my text field, rather than patch prototype, although being event driven seems alot more sense.

Does this work with edge

Does this work with edge (2.0) prototype.js?

Much simpler option for fixing observe field

A much easier way to get around the observe field bug is to include the following js code. You just need to make sure it appears after prototype is loaded.


Abstract.EventObserver.prototype.__initialize = Abstract.EventObserver.prototype.initialize;
Abstract.EventObserver.prototype.__registerCallback = Abstract.EventObserver.prototype.registerCallback;
Abstract.EventObserver.prototype = Object.extend(Abstract.EventObserver.prototype, {
initialize: function(element, callback, trigger) {
this.trigger = trigger;
this.__initialize(element, callback);
},

registerCallback: function(element) {
if (this.trigger) {
Event.observe(element, this.trigger, this.onElementEvent.bind(this));
}
else {
this.__registerCallback(element);
}
}
});

and without changing anything else you can simply use observe_field as described in the documentation or as listed above.

I verified that it works in Rails 1.2.5 but I haven't tested other versions.

Eric