I have been doing a lot of javascript lately after a long time of not doing any javascript. I mean a long time and I found
myself trying to implement patterns that have become familiar and trusted in the strongly typed class based OOP world.
I had some success but a lot of frustration, especially trying to enforce encapsulation to force the use of messages (events/delegate/commands
etc).
I am sure there are very simple and elegant ways to to the things I have been trying to do but they were not in my rusty
toolbox after so long.
What I really needed to build what I consider a robust application in any language is a delegate implementation that is
intuitive and instrumented. I have had passing exposure, thankfully, to YUI and Google, and as I am sharpening my js chops,
jQuery, but none of them really fit my needs. And the overhead involved with playing with MS Ajax, as so very cool 4.0 is,
is just more than I can bear. I want infrastructure code that is lean, open and airy but strong as nails.
So it's time to implement an event model in javascript and then build a delegate implementation on top of it. I am not really
concerned that there are implementations that almost suit my needs out there. I wanted to get back in shape, javascript-wise,
and this looked to be the way to do it.
Here it is. It works great and I can prove it. ;-) I am not a teacher and do not pretend to be. I write code, some would
say, so I will let the code do the talking.
I have build a delegate implementation on top of this as well as an enhanced implementation of INotifyPropertyChange in js that work quite well. Will be posting those soon.
/// <reference path="salient.event.js" />
/// <reference path="thirdparty/ba-debug.js" />
//
// salientJS JavaScript Library v1.0
// http://skysanders.net/code/salientJs/
//
// Copyright (c) 2009 Sky Sanders - sky@skysanders.net
// Dual licensed under the MIT and GPL licenses.
// http://skysanders.net/code/salientJs/license.txt
//
var go = function()
{
// lets first hook up all of our logging hooks. you dont need to catch all
// of these, most of the time, but having them in place is sure handy when
// something not so much cool is going on and you need a peek at what is going on.
// attach all of the event hooks to debug so we can watch what is happening.
// It is a little verbose but a little work wont kill ya.
// And ask yourself.. "Self? would you rather have the info and not need it,
// or need it and not have it?" go ahead, i will wait.......
// anyway - there is more information than you *think* you may ever need in
// these hooks.
// i will create 2 versions of hooks, verbose and terse and we will use terse
// for demonstration purposes. verbose is a bit noisy in firebug.
// once the pattern is proven you can set verbose and examine the format of the
// messages in the console
bindLog(false);
// we will use the log as output. you can step through this or just open firebug
// (or firebug lite using the link) and watch
// -- >> THESE WILL BE LOG MESSAGES
debug.log("starting");
// -- >> starting
// create event. a typical scenario is that an event will be created in the
// constructor of an object.
// hmmm... looks like we are in the constructor of an object. funny that....
var e = new salient.event("onHelloWorld", "this", "just saying hi.");
// -- >> onCreateEvent
// we need a patsy.. i mean observer.
var fooType = function(name)
{
this.name = name;
// get a context reference for internal methods other wise an identity
// crisis will ensue
var _self = this;
this.curious = function(sender, args)
{
debug.info(sender.name + " says " + args.message + " to " + _self.name);
if (_self.selfish)
{
// wait for it......
args.cancel = true;
}
if (_self.angry)
{
throw new Error(0, "Foo angry");
// wait for it......
}
};
this.selfish = false;
// wait for it......
this.angry = false;
// wait for it......
}
var foo = new fooType("FOO");
// and make him observe. variations on this pattern include passing the event
// object to the observer and letting it handle the binding itself.
// The way we are implementing here is an assembly pattern - we have arbitary
// bits that don't know and don't need to know about each other and we are wiring
// together to serve our evil purposes.
e.addHandler(foo.curious, "foo", "external binding", "arbitrary info");
// -- >> onAddHandler foo
// an event can have more than one eventHandler.
// they are call in order of binding. not a good idea to depend on execution order.
// perhaps i will implement some helper methods that can pin a binding to the
// top or bottom of the list.
// TODO: allow pinning a eventHandler to either end of the list. throw error if
// contention for spot.
// anyway..... as i way saying...
// give 'this' a name for our contrived example
this.name = "ME";
var etoken = e.addHandler(function(sender, args)
{
debug.info(sender.name + " says " + args.message + "to " + this.name);
},
"anonymous", "inline function closure", "the price of tea in china");
// -- >> onAddHandler anonymous
// we don't have very good reflection in javascript so we need to tell the event
// who is listening. In this case it is 'anonymous' - the rest of the parameters are
// informational. actually all of them are. the eventing system doesn't use them.
// they are provided for your pleasure.
// create some event args
var args1 = { message: "Hello World!" };
// all params are optional but we will specify all for edification and fire the event
e.raise(this, args1, false, "no info");
// should get a log from both 'me' and 'foo'
// -- >> onEventBegin onHelloWorld Object message=Hello World!
// -- >> onEventExecute on foo Object message=Hello World!
// -- >> ME says Hello World! to FOO
// -- >> onEventExecute on anonymous Object message=Hello World!
// -- >> ME says Hello World!to anonymous
// -- >> onEventEnd onHelloWorld
// let's try that agin but since we know that foo is first,
// let's make foo selfish.
foo.selfish = true; // there it is.
// create some more event args. generally not a good idea to resuse args.
// unless you need to or it makes sense. lol.
var args2 = { message: "Hello World!" };
e.raise(this, args2, false, "no info");
// should get a message from foo and a cancellation notice.
// me is left out of the loop.
// -- >> onEventBegin onHelloWorld Object message=Hello World!
// -- >> onEventExecute on foo Object message=Hello World!
// -- >> ME says Hello World! to FOO
// -- >> onEventCancelled by foo
// -- >> onEventEnd onHelloWorld
foo.selfish = false;
// case in point: foo just cancelled. if we reuse the args, even
// though foo is no longer selfish the event will still be canccelled.
var args3 = { message: "I said 'Hello World!'" };
e.raise(this, args3, false, "no info");
// -- >> onEventBegin onHelloWorld Object message=I said 'Hello World!'
// -- >> onEventExecute on foo Object message=I said 'Hello World!'
// -- >> ME says I said 'Hello World!' to FOO
// -- >> onEventExecute on anonymous Object message=I said 'Hello World!'
// -- >> ME says I said 'Hello World!'to anonymous
// -- >> onEventEnd onHelloWorld
// can removeHandler using a token or the function itself. in the case
// of an anonymous closure you must save the token if you wish to easily removeHandler.
// what a coincidence, we have a token rightchere...
e.removeHandler(etoken);
// -- >> onRemoveHandler onHelloWorld from anonymous
var args4 = { message: "Hello FOO!" };
e.raise(this, args4, false, "no info");
// -- >> onEventBegin onHelloWorld Object message=Hello FOO!
// -- >> onEventExecute on foo Object message=Hello FOO!
// -- >> ME says Hello FOO! to FOO
// -- >> onEventEnd onHelloWorld
// lets say foo doesn't like the messages it is getting and
// throws an exception while handling the event
foo.angry = true; // and there it is.
// lets set ignore to true - should just get a log entry
var args5 = { message: "how you feeling foo?" };
e.raise(this, args5, true, "feeling liberal");
// -- >> onEventBegin onHelloWorld Object message=how you feeling foo?
// -- >> onEventExecute on foo Object message=how you feeling foo?
// -- >> ME says how you feeling foo? to FOO
// -- >> onEventError onHelloWorld on foo : 0
// -- >> onEventEnd onHelloWorld
try
{
var args6 = { message: "sorry to hear that?" };
// set ignore to false because we are .....
e.raise(this, args6, false, "feeling republican");
// -- >> onEventBegin onHelloWorld Object message=sorry to hear that?
// -- >> onEventExecute on foo Object message=sorry to hear that?
// -- >> ME says sorry to hear that? to FOO
// OOOPS!
}
catch (ex)
{
// -- >> onEventError onHelloWorld on foo : 0
debug.warn("we caught an unhandled exception", ex);
// -- >> we caught an unhandled exception Error: 0 message=0 fileName=Foo angry lineNumber=75
}
// the binding to foo.curious returned a token that we ignored, but we have the function
e.removeHandler(foo.curious);
// -- >> onRemoveHandler onHelloWorld from foo
// fire the event again, this time into the wind
var args7 = { message: "the sound of one hand clapping....." };
e.raise(this, args7, false, "no info");
// -- >> onEventBegin onHelloWorld Object message=the sound of one hand clapping.....
// -- >> onEventEnd onHelloWorld
// disposal pattern. the event cannot assign null to itself so we just
// return null from removeHandler to help establish the pattern.
e = e.destroy();
// -- >> onDestroy onHelloWorld
debug.log("finished");
// -- >> finished
}
// here is the log in its original uncut form
// starting
// onCreateEvent
// onAddHandler foo
// onAddHandler anonymous
// onEventBegin onHelloWorld Object message=Hello World!
// onEventExecute on foo Object message=Hello World!
// ME says Hello World! to FOO
// onEventExecute on anonymous Object message=Hello World!
// ME says Hello World!to anonymous
// onEventEnd onHelloWorld
// onEventBegin onHelloWorld Object message=Hello World!
// onEventExecute on foo Object message=Hello World!
// ME says Hello World! to FOO
// onEventCancelled by foo
// onEventEnd onHelloWorld
// onEventBegin onHelloWorld Object message=I said 'Hello World!'
// onEventExecute on foo Object message=I said 'Hello World!'
// ME says I said 'Hello World!' to FOO
// onEventExecute on anonymous Object message=I said 'Hello World!'
// ME says I said 'Hello World!'to anonymous
// onEventEnd onHelloWorld
// onRemoveHandler onHelloWorld from anonymous
// onEventBegin onHelloWorld Object message=Hello FOO!
// onEventExecute on foo Object message=Hello FOO!
// ME says Hello FOO! to FOO
// onEventEnd onHelloWorld
// onEventBegin onHelloWorld Object message=how you feeling foo?
// onEventExecute on foo Object message=how you feeling foo?
// ME says how you feeling foo? to FOO
// onEventError onHelloWorld on foo : 0
// onEventEnd onHelloWorld
// onEventBegin onHelloWorld Object message=sorry to hear that?
// onEventExecute on foo Object message=sorry to hear that?
// ME says sorry to hear that? to FOO
// onEventError onHelloWorld on foo : 0
// we caught an unhandled exception Error: 0 message=0 fileName=Foo angry lineNumber=88
// onRemoveHandler onHelloWorld from foo
// onEventBegin onHelloWorld Object message=the sound of one hand clapping.....
// onEventEnd onHelloWorld
// onDestroy onHelloWorld
// finished
// The rest is the wiring of the event hooks to the debugger. Nothing to be
// frightened of and can be ignored for now.
function bindLog(verbose)
{
if (verbose)
{
salient.event.onCreateEvent = function(e, args)
{
// when the event is created on the source
debug.log("onCreateEvent", e, args);
};
salient.event.onAddHandler = function(e, args, fn, name, source, info)
{
// when a eventHandler is attached
debug.log("onAddHandler", e, args, fn, name, source, info);
};
salient.event.onEventBegin = function(e, args, sender, eventArgs, ignoreExceptions, info)
{
// when the source (or someone else) fires the event
debug.log("onEventBegin", e, args, sender, eventArgs, ignoreExceptions, info);
};
salient.event.onEventCancelled = function(e, args, sender, eventArgs, ignoreExceptions, info, eventHandler)
{
// if a eventHandler cannot forever hold it's peace it needs only to add a 'cancel=true' property to the args before returning
// and any remaining eventHandlers will be left in the dark.
debug.warn("onEventCancelled", e, args, sender, eventArgs, ignoreExceptions, info, eventHandler);
};
salient.event.onEventExecute = function(e, args, sender, eventArgs, ignoreExceptions, info, eventHandler)
{
// when the handler is actually called on the eventHandler
debug.info("onEventExecute", e, args, sender, eventArgs, ignoreExceptions, info, eventHandler);
};
salient.event.onEventError = function(e, args, sender, eventArgs, ignoreExceptions, info, eventHandler, ex)
{
// if the handler throws we log and throw. unless... ignoreExceptions is true. there is a use case for this. somewhere
debug.error("onEventError", e, args, sender, eventArgs, ignoreExceptions, info, eventHandler, ex);
};
salient.event.onEventEnd = function(e, args, sender, eventArgs, ignoreExceptions, info)
{
// after the event has finished calling all it's handlers
debug.log("onEventEnd", e, args, sender, eventArgs, ignoreExceptions, info);
};
salient.event.onRemoveHandler = function(e, args, handle, eventHandler)
{
// when a handler detaches
debug.log("onRemoveHandler", e, args, handle, eventHandler);
};
salient.event.onDestroy = function(e, args)
{
// when the event is destroyed
debug.log("onDestroy", e, args);
};
}
else
{
salient.event.onCreateEvent = function(e, args)
{
// when the event is created on the source
debug.log("onCreateEvent");
};
salient.event.onAddHandler = function(e, args, fn, name, source, info)
{
// when a eventHandler is attached
debug.log("onAddHandler " + name);
};
salient.event.onEventBegin = function(e, args, sender, eventArgs, ignoreExceptions, info)
{
// when the source (or someone else) fires the event
debug.log("onEventBegin " + e.name, eventArgs);
};
salient.event.onEventCancelled = function(e, args, sender, eventArgs, ignoreExceptions, info, eventHandler)
{
// if a eventHandler cannot forever hold it's peace it needs only to add a 'cancel=true' property to the args before returning
// and any remaining eventHandlers will be left in the dark.
debug.warn("onEventCancelled by " + eventHandler.name);
};
salient.event.onEventExecute = function(e, args, sender, eventArgs, ignoreExceptions, info, eventHandler)
{
// when the handler is actually called on the eventHandler
debug.info("onEventExecute on " + eventHandler.name, eventArgs);
};
salient.event.onEventError = function(e, args, sender, eventArgs, ignoreExceptions, info, eventHandler, ex)
{
// if the handler throws we log and throw. unless... ignoreExceptions is true. there is a use case for this. somewhere
debug.error("onEventError " + e.name + " on " + eventHandler.name + " : " + ex.message);
};
salient.event.onEventEnd = function(e, args, sender, eventArgs, ignoreExceptions, info)
{
// after the event has finished calling all it's handlers
debug.log("onEventEnd " + e.name);
};
salient.event.onRemoveHandler = function(e, args, handle, eventHandler)
{
// when a handler detaches
debug.log("onRemoveHandler " + e.name + " from " + eventHandler.name);
};
salient.event.onDestroy = function(e, args)
{
// when the event is destroyed
debug.log("onDestroy " + e.name);
};
}
}
go();
//
// salientJS JavaScript Library v1.0
// http://skysanders.net/code/salientJs/
//
// Copyright (c) 2009 Sky Sanders - sky@skysanders.net
// Dual licensed under the MIT and GPL licenses.
// http://skysanders.net/code/salientJs/license.txt
//
/***********************************************************************
salient.event
NOTE: the js xml docs are all out of whack after some refactoring and I am not
in the mood. You don't need to be reading this anyway - the sample is clear no? .lolz.
***********************************************************************/
// declare the namespace if it hasn't already
var salient = salient || {};
salient.event = function(name, source, info)
{
/// <summary>
/// A simple event delegate implementation
/// </summary>
/// <param name="name" type="String" mayBeNull="true" optional="true">The name of the event.</param>
/// <param name="source" type="Sting" mayBeNull="" optional=""></param>
/// <param name="info" type="Sting" mayBeNull="" optional=""></param>
/// <field name="name" type="Sting" mayBeNull="" ></field>
/// <field name="source" type="Sting" mayBeNull="" ></field>
/// <field name="info" type="String" mayBeNull="" ></field>
/// <field name="id" type="String" mayBeNull="" ></field>
/// <field name="eventHandlers" type="Function[]" mayBeNull="false" >An array of function references representing our eventHandlers</field>
// start a counter
if (!salient.event.guid)
{
salient.event.guid = 0;
}
if (typeof (salient.event.ignoreExceptions) == "undefined")
{
salient.event.ignoreExceptions = false;
}
this.source = source;
this.name = name;
this.info = info;
this.id = salient.event.guid++;
this.eventHandlers = [];
if (typeof (salient.event.onCreateEvent) == 'function')
{
salient.event.onCreateEvent(this, arguments);
}
}
salient.event.prototype.addHandler = function(fn, name, source, info)
{
/// <summary>
/// Adds supplied function reference to the list of functions to be called upon raise of
/// this event and returns a token that can be used to remove the eventHandler
/// </summary>
/// <param name="fn" type="Function" mayBeNull="false" optional="false"></param>
/// <returns type="String">handle token. to be used when removing eventHandler if the function itself cannot be used.</returns>
// TODO: test case: functions of same name in different object are equal??
// check to see if this function has already been here
var eventHandler;
for (var i = 0; i < this.eventHandlers.length; i++)
{
eventHandler = this.eventHandlers[i];
if (eventHandler.token == fn || eventHandler.fn == fn)
{
return eventHandler.token;
}
}
token = this.name + "_" + this.id + "_" + salient.event.guid++; // create a new handle
eventHandler = new salient.event.eventHandler(fn, token, name, source, info);
if (typeof (salient.event.onAddHandler) == 'function')
{
salient.event.onAddHandler(this, arguments, fn, name, source, info, eventHandler);
}
this.eventHandlers.push(eventHandler);
return token;
};
salient.event.prototype.removeHandler = function(handle)
{
/// <summary>
/// Removes a function reference from the list of eventHandlers to this event.
/// You can pass the numeric token issued when the eventHandler was attached or the eventHandler itself. Closures obviously cannot be passed.
/// Be sure to retain the handle if needed.
/// </summary>
/// <param name="handle" type="Object" mayBeNull="false" optional="false">the handle token issued or the function reference it was assigned to</param>
var tokenToRemove;
// check to see if the function itself was passed
var eventHandler;
for (var i = 0; i < this.eventHandlers.length; i++)
{
eventHandler = this.eventHandlers[i];
if (eventHandler.token == handle || eventHandler.fn == handle)
{
this.eventHandlers.splice(i, 1);
if (typeof (salient.event.onRemoveHandler) == 'function')
{
salient.event.onRemoveHandler(this, arguments, handle, eventHandler);
}
eventHandler = eventHandler.destroy();
break;
}
}
};
salient.event.prototype.destroy = function()
{
/// <summary>
/// detaches all eventHandlers from this event.
/// </summary>
/// <returns type="null">for use in destroy pattern</returns>
for (var i = 0; i < this.eventHandlers.length; i++)
{
eventHandler = this.eventHandlers[i];
this.eventHandlers.splice(i, 1);
if (typeof (salient.event.onRemoveHandler) == 'function')
{
salient.event.onRemoveHandler(this, arguments, eventHandler.name);
}
eventHandler = eventHandler.destroy();
break;
}
if (typeof (salient.event.onDestroy) == 'function')
{
salient.event.onDestroy(this, arguments);
}
return null;
}
salient.event.prototype.raise = function(sender, eventArgs, ignoreExceptions, info)
{
/// <summary>
/// calls the function ref for each eventHandler that subscribed to this event with sender and args
/// </summary>
/// <param name="sender" type="Object" mayBeNull="true" optional="true"></param>
/// <param name="args" type="Object" mayBeNull="true" optional="true">the event args are shared by all eventHandlers and can be modified or cancelled by any of them and raise is stopped.
/// To cancel execution of an event set or add a field named 'cancel' to true.
/// </param>
/// <param name="ignoreExceptions" type="Boolean" mayBeNull="true" optional="true">if true exceptions throw by eventHandlers are swallowed</param>
/// <todo>Log errors<todo>
if (typeof (salient.event.onEventBegin) == 'function')
{
salient.event.onEventBegin(this, arguments, sender, eventArgs, ignoreExceptions, info);
}
for (var i = 0; i < this.eventHandlers.length; i++)
{
var eventHandler;
try
{
eventHandler = this.eventHandlers[i];
if (typeof (salient.event.onEventExecute) == 'function')
{
salient.event.onEventExecute(this, arguments, sender, eventArgs, ignoreExceptions, info, eventHandler);
}
eventHandler.fn(sender, eventArgs);
if (eventArgs && eventArgs.cancel && eventArgs.cancel == true)
{
if (typeof (salient.event.onEventCancelled) == 'function')
{
salient.event.onEventCancelled(this, arguments, sender, eventArgs, ignoreExceptions, info, eventHandler);
}
break;
}
}
catch (ex)
{
if (typeof (salient.event.onEventError) == 'function')
{
salient.event.onEventError(this, arguments, sender, eventArgs, ignoreExceptions, info, eventHandler, ex);
}
if (!ignoreExceptions && !salient.event.ignoreExceptions)
{
throw ex;
}
}
}
if (typeof (salient.event.onEventEnd) == 'function')
{
salient.event.onEventEnd(this, arguments, sender, eventArgs, ignoreExceptions, info, arguments);
}
}
salient.event.eventHandler = function(fn, token, name, source, info)
{
this.fn = fn;
this.token = token;
this.name = name;
this.source = source;
this.info = info;
this.destroy = function()
{
this.fn = null;
this.token = null;
this.name = null;
this.source = null;
this.info = null;
return null;
}
};
/************************************
************************************/