views:

44

answers:

2

I'm building a webpage that is composed of several controls, and trying to come up with an effective somewhat generic client side sibling control communication model. One of the controls is the menu control. Whenever an item is clicked in here I wanted to expose a custom client side event that other controls can subscribe to, so that I can achieve a loosely coupled sibling control communication model.

To that end I've created a simple Javascript event collection class (code below) that acts as like a hub for control event registration and event subscription. This code certainly gets the job done, but my question is is there a better more elegant way to do this in terms of best practices or tools, or is this just a fools errand?

/// Event collection object - acts as the hub for control communication.
function ClientEventCollection()
{
    this.ClientEvents = {};
    this.RegisterEvent = _RegisterEvent;
    this.AttachToEvent = _AttachToEvent;
    this.FireEvent = _FireEvent;

    function _RegisterEvent(eventKey)
    {
        if (!this.ClientEvents[eventKey])
           this.ClientEvents[eventKey] = [];
    }

    function _AttachToEvent(eventKey, handlerFunc)
    {
        if (this.ClientEvents[eventKey])
             this.ClientEvents[eventKey][this.ClientEvents[eventKey].length] = handlerFunc;
    }

    function _FireEvent(eventKey, triggerId, contextData )
    {
        if (this.ClientEvents[eventKey])
        {
            for (var i = 0; i < this.ClientEvents[eventKey].length; i++) 
            {
                var fn = this.ClientEvents[eventKey][i];
                if (fn)
                    fn(triggerId, contextData);
           }
        }
    }
}

// load new collection instance.
var myClientEvents = new bsdClientEventCollection();

// register events specific to the control that owns it, this will be emitted by each respective control.
myClientEvents.RegisterEvent("menu-item-clicked");

Here is the part where this code above is consumed by source and subscriber controls.

// menu control 
$(document).ready(function() 
{    
    $(".menu > a").click( function(event) 
                    { 
                        //event.preventDefault(); 
                        myClientEvents.FireEvent("menu-item-clicked", $(this).attr("id"), null);

                    });
});


<div style="float: left;" class="menu">
 <a id="1" href="#">Menu Item1</a><br />
 <a id="2" href="#">Menu Item2</a><br />
 <a id="3" href="#">Menu Item3</a><br />
 <a id="4" href="#">Menu Item4</a><br />
</div>



// event subscriber control
$(document).ready(function() 
{    
    myClientEvents.AttachToEvent("menu-item-clicked", menuItemChanged);
    myClientEvents.AttachToEvent("menu-item-clicked", menuItemChanged2);
    myClientEvents.AttachToEvent("menu-item-clicked", menuItemChanged3);
});


function menuItemChanged(id, contextData) 
{
    alert('menuItemChanged ' + id);
}

function menuItemChanged2(id, contextData) 
{
    alert('menuItemChanged2 ' + id);
}

function menuItemChanged3(id, contextData) 
{
    alert('menuItemChanged3 ' + id);
}
+1  A: 

jQuery's event system can pass additional handler parameters when you trigger events. We also separate the control namespace from jQuery selectors by creating a registry that maps control names to selectors. To deal with handlers binding to a control before the control is registered, we implement a binding delay mechanism.

var controls = {};
(function ControlRegistry(controls) {
  controls.items = {};
  function bindNow(selector, event, eventData, handler) {
      $(selector).bind(event, eventData, handler);
  }
  function delayBinding(queue, event, eventData, handler) {
      queue.push([event, eventData, handler]);
  }
  function bindAll(queue, selector) {
      for (var i=0; i<queue.length; ++i) {
          var args = queue[i];
          args.unshift(selector);
          bindNow.apply(controls, args);
      }
  }
  controls.register = function (name, selector) {
    if (typeof this.items[name] == 'object') {
       bindAll(this.items[name], selector);
    }
    this.items[name] = selector;
  };
  controls.bind = function (control, event, eventData, handler) {
    jQuery.isFunction( eventData ) {
        handler = eventData;
        eventData = null;
    }
    switch (typeof this.items[control]) {
      case 'undefined':
        this.items[control] = [];
        // FALLTHRU
      case 'object':
        delayBinding(this.items[control], event, eventData, handler);
        break;

      case 'string':
        bindNow(this.items[control], event, eventData, handler);
        break;
    }
  }
})(controls);

$(document).ready(function() 
{
    controls.register('menuItem', '.menu > a');
    $(".menu > a").click( function(event) 
                    {
                        $(this).trigger("menu-item-clicked", [$(this).attr("id"), 'cow', 'moo']);

                    });
});

Elsewhere:

function menuItemChanged(evt, id, animal, speech) 
{
    alert('menuItemChanged ' + id 
          + '\nThe ' + animal + ' says "' + speech + '."');
}

function menuItemChanged2(evt, id, animal, speech)) 
{
    alert('menuItemChanged2 ' + id 
          + '\nThe ' + animal + ' says "' + speech + '."');
}

function menuItemChanged3(evt, id, animal, speech)) 
{
    alert('menuItemChanged3 ' + id 
          + '\nThe ' + animal + ' says "' + speech + '."');
}

$(document).ready(function() 
{
    controls.bind('menuItem', "menu-item-clicked", menuItemChanged);
    controls.bind('menuItem', "menu-item-clicked", menuItemChanged2);
    controls.bind('menuItem', "menu-item-clicked", menuItemChanged3);
});

Update

if you include the restriction that a control be registered before handlers are bound to its events, the control registry can be vastly simplified:

var controls = {
  register: function (name, selector) {
    if (typeof this[name] != 'function') {
      this[name] = selector;
    }
  };
};
...

controls.register('menuItem', '.menu > a');
$(document).ready(function() 
{
    $(".menu > a").click( function(event) 
                    {
                        $(this).trigger("menu-item-clicked", [$(this).attr("id"), 'cow', 'moo']);

                    });
});
...

$(document).ready(function() 
{
    $(controls.menuItem).bind("menu-item-clicked", menuItemChanged);
    $(controls.menuItem).bind("menu-item-clicked", menuItemChanged2);
    $(controls.menuItem).bind("menu-item-clicked", menuItemChanged3);
});

This is a reasonable restriction, as you can register early (within the script for the control) and bind late (in $(document).ready).

outis
+1 I like this approach, from a purist standpoint does the registration and binding have to be done in the same script fragment or can they be done in separate fragments without timing issues? Ideally each control should emit it's own independent script if at all possible.
James
There's no real event registration to be done, just binding of event handlers. You can bind each handler completely separately. There shouldn't be timing issues, though (obviously) the "menu-item-clicked" event won't fire until the anonymous handler is bound to ".menu > a"'s click event.
outis
I guess the only other thing that bugs me about this is that the subscriber control must have knowledge of the html structure of the menu control, which introduces a tight coupling, and makes things brittle to change. If I wanted to change the html structure or css of the menu control I'd gave to remember to also change it for all event subscribers.
James
You can quite easily factor out the control selector. A control can register itself under a different name in some notional namespace, along with the selector. At this point, there might be timing issues. I'll update the example.
outis
Thanks I appreciate your efforts. I think i'll stick with what I already have because it achieves the same thing in simpler way.
James
Note that `ClientEventCollection` requires that `RegisterEvent` be called before `AttachToEvent`, while `ControlRegistry.bind` can be called before `ControlRegistry.register`. The complexity of `ControlRegistry` comes from this. If we require that a control be registered before binding to an event on it, `ControlRegistry` can be much simpler (see update).
outis
A: 

My original solution ended up being the right one for me because it achieves the loose coupling I was after in a straight-forward and simple way.

James