views:

125

answers:

1

I am looking to route/redirect all events which occur on a target origTarget to another object otherObject. I want to accomplish something like this:

/* Magic binding (doesn't work!). */
$(origTarget).bind('*', function(e) {
    $(otherObject).trigger(e);
});

Is this possible with jQuery without looping through every possible event type?

This example should alert with 'hello':

var origTarget = { };
var otherObject = { };

/* Do magic binding here. */

$(otherObject).bind('alert', function() {
    alert('hello');
});

$(origTarget).trigger('alert');
+2  A: 

I think what you are looking for can be accomplished by re-evaluating how you bind your event in the first place. If you really want to see what is happening in the target object, you should be binding the event to the target object.

Having said that, lets try to find you a solution

Digging into the jQuery.event stuff a little bit I don't see any "easy way" to accomplish this. However it seems the events are cached in the object using jQuery.data(elem,'events'). There are a few options for you to explore

Copy bound events from one object to another

A potential event copying solution written and tested in jQuery 1.3.2:

(function($){ 
  $.fn.proxyEventsFrom = function(target) {
    // cache this for later
    var $target = $(target);

    return this.each(function() {
      // store a copy of the element to use in the event handler callback
      var elem = this; 

      // get a list of events that I subscribe to
      var events = $(this).data("events") || {};

      $.each(events, function(type, handlers) {
        // bind onto this event on the target
        $target.bind(type, function(event) {
          var evArgs = arguments;
          // store the element that event originally fired on in the event object.
          event.proxyFrom = this; 
          $.each(handlers, function(guid, handler) {
            var ret = handler.apply(elem, evArgs);
            // to behave like other event handlers.
            if( ret !== undefined ){
              event.result = ret;
              if ( ret === false ) {
                event.preventDefault();
                event.stopPropagation();
                return false; // stop our each loop too
              }
            }
          }); // end $.each(handlers, function(guid, handler) 
          return event.result;
        }); // end $target.bind(type, function(event) 
      });// end $.each(events, function(type, handlers) 
    });// end this.each(function() {});
  }; // end $.fn.proxyEventsFrom
})(jQuery);

$(someObject).proxyEventsFrom(targetObject);

This works by reading all the events that someObject is listening for, and subsequently binding a new event on the targetObject of the same event type that will call all of the event handlers on someObject. The event handler on someObject can look like this:

$(someObject).bind('alert', function(e) {
  if (e.proxyFrom) { alert('Proxied Succesfully!'); }
  alert('test');
});
$(someObject).proxyEventsFrom(targetObject);
$(targetObject).trigger('alert');

This approach could easily generate a recursive loop of event handlers if you proxied events to something that proxied back. Be careful.

Overriding jQuery.event.trigger

It is possible to override the event triggering function to accomplish what you desire, although I suggest against it for a few reasons: It is dangerous to override things in libraries, You'll end up with a lot more stuff than you want, and this function will only get called for uses of .trigger() obviously - therefore doesn't apply to standard DOM events.

  (function() { // wrap this in a closure so origTrigger can't be overwritten.
    var origTrigger = jQuery.event.trigger;
    jQuery.event.trigger = function(event, data, elem, bubbling)
    {
      if (elem === origTarget) {
        if (origTrigger.call(this, event, data, otherObject, bubbling) === false) return;
      } 
      return origTrigger.apply(this,arguments);
    }
  })();

Make the events of the target === the events of the other object

If the origTarget wont ever have events of its own bound you could copy the events object by reference.

  // copy the events table by reference from otherObject "events" data 
  // (create if neccesary)
  $.data(origTarget, 'events', 
    $.data(otherObject,'events') || $.data(otherObject,'events',{}));

  // need to setup handler since jQuery isn't creating it for us
  // function source found on line 2465 of jquery-1.3.2.js

  var handle = $.data(origTarget, 'handle', function(){
    return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
      jQuery.event.handle.apply(arguments.callee.elem, arguments) :
      undefined;
  });
  handle.elem = origTarget;

Custom 'handle' function

It was a long road to this solution, internally all jQuery events get passed through a 'handle' function that is stored in the $.data(elem, 'handle'). So if you override that function on your target, you should be able to proxy the events over to the other object. This solution assumes you only ever have one extra listener, it could be adapted to have more than one listener pretty easily though. I think this is the best approach I've come up with yet:

  jQuery.event.proxyEvents = function(fromObject, toObject) {
    // create an new handle function
    var newHandle = function(event) {
      // the test in the original handler returns undefined if jQuery.event.triggered
      if (jQuery.event.triggered) return undefined;
      // event should be safe - but just in case lets take a trick from
      // jQuery.event.handle
      event = arguments[0] = jQuery.event.fix(event || window.event);
      // if we have a "listener"
      if (arguments.callee.listener) {
        // set proxyFrom for access in bound event handlers
        event.proxyFrom = arguments.callee.elem;
        // call the root "handle" function on the listener
        jQuery.event.handle.apply(arguments.callee.listener, arguments);
        // if we got a result of false from the event callbacks - exit early
        if (event.result === false) return;
        // clean that proxy property out
        delete event.proxyFrom;
      }
      // this is all the basic 'handle' function does:
      jQuery.event.handle.apply(arguments.callee.elem, arguments);
    };
    // properties of the handler function
    newHandle.elem = fromObject;
    newHandle.listener = toObject;
    // store it
    jQuery.data(fromObject, 'handle', newHandle);
  };
  jQuery.event.proxyEvents(origTarget, otherObject);

It even works with DOM Elements (although you need to pass the element, not a jQuery object)

  jQuery.event.proxyEvents($('span')[0], $('input')[0]);
gnarf
It can't be guaranteed that `otherObject` binds its handler before the magic stuff, so I don't think this method would work. Sorry.
strager
On your edit: I'm working with Javascript objects, and not DOM nodes/elements, so as far as I know there's no event propagation, and events are handled solely by jQuery.
strager
Saw that in your further edit, thanks for the clarification - more updates and ideas posted here now and removed the DOM implementation.
gnarf
I only tried a few of your solutions, but they didn't fit my needs and the solutions were much more messy than a hardcoded solution. I'll use the hardcoded solution until jQuery comes up with native support for this, I suppose, or there's a convenient plugin. Thanks for your help. =]
strager
The last solution isn't that messy, and is quite elegant IMO. All the events get routed through $.data(origTarget, 'handle') - overload that function and you can proxy them to the other object.
gnarf