Click here to Skip to main content
15,908,455 members
Articles / Programming Languages / Javascript

Custom Events Using JavaScript

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
18 May 2010CPOL3 min read 11.8K   6   1
Describes a framework for creating events using JavaScript.

Introduction

This article will cover the event framework. There will be an explanation of the framework and an implementation example.

Background

When producing a drag and drop editor, I needed an event framework that would allow me to register multiple delegates with an event (excuse the .NET terminology). The following is the result of that requirement.

This framework also includes code for implementing timers. I'll explain why this was required in the "Points of Interest" section.

The Code

The following is the code for the event framework.

An Easy Life with Prototyping

Collections in most frameworks include methods for adding and removing items from the collection. JavaScript doesn't by default, but they can be added by using prototyping. The first section of this framework prototypes on top of Array to extend it with the Add, RemoveAt, and IndexOf functions.

JavaScript
Array.prototype.Add = function(obj) { this[this.length] = obj; }
Array.prototype.RemoveAt = function(index) {
  var result = new Array();
  var offset = 0;
  for (var x = 0; x < this.length; x++) {
    if (x != index)
      result[result.length] = this[x];
  }
  return result;
}

Array.prototype.IndexOf = function(value) { for (var x = 0; x < 
   this.length; x++) if (this[x] == value) return x; return -1; }
var uiEventRegistration = function() {
  this.ID = null;
  this.Method = null;
  this.Caller = null;
}

The following code creates a framework for using timers. There is a timer array for holding all the created timers. A function called "uiTimerExecute" is used to check the timer expired states. There is also the uiTimer class which recieves an interval (in milliseconds) in the constructor.

JavaScript
var uiTimers = new Array();
var uiTimerCount = 0;
function uiTimerExecute(timerID) {
    for(var x = 0; x < uiTimers.length; x++) {
      if (timerID == null || uiTimers[x].TimerID == timerID) {
        if (uiTimers[x].Expired()) {
          uiTimers[x].OnTick.Fire();
          var d = new Date();
          uiTimers[x].NextTime = new Date(d.getYear(),d.getMonth(),d.getDate(), 
                                 d.getHours(),d.getMinutes(),d.getSeconds(), 
                                 d.getMilliseconds() + (uiTimers[x].Interval));
        }
      }
    }
}

var uiTimer = function(interval) {
    uiTimers.Add(this);
    this.Interval = interval;
    this.StartDate = new Date();
    this.NextTime = new Date(this.StartDate.getYear(),this.StartDate.getMonth(),
                        this.StartDate.getDate(),this.StartDate.getHours(), 
                        this.StartDate.getMinutes(),
                        this.StartDate.getSeconds(),
                        this.StartDate.getMilliseconds() + (interval));
    this.OnTick = new uiEvent();
    this.OnTick.TimerEvent = true;
    this.Expired = function() { return this.NextTime < new Date(); };
    this.TimerID = uiTimerCount;
    setInterval('uiTimerExecute(' + this.TimerID + ');', interval);
}

The last section is the main section of the event framework. There is a class called uiEvent. This has a method "Register". This is passed a delegate and a context object. The context object is the object referred to as "this" when the delegate is called by the method. Register returns a registration ID. This can be used with the "Deregister" method to remove the delegate from the event stack. The last method is "Fire". When this is called, all parameters passed in are then sent as parameters to the registered delegates.

JavaScript
var uiEvent = function() {
  this.TimerEvent = false;
  this.__registered = new Array();
  this.__currentID = 0;
  this.Register = function(func, caller) {
    var reg = new uiEventRegistration();
    reg.ID = this.__currentID;
    reg.Method = func;
    reg.Caller = caller;
    this.__registered.Add(reg);
    var returnID = this.__currentID;
    this.__currentID++;
    return returnID;
  }
  this.Deregister = function(RegistrationID) {
    var index = -1;
    for (var x = 0; x < this.__registered.length; x++)
      if (this.__registered[x].ID == RegistrationID) {
        index = x;
        break;
      }
    if (index != -1) {
      this.__registered = this.__registered.RemoveAt(index);
    }
  }
  this.Fire = function() {
    if (!this.TimerEvent)
      uiTimerExecute();
    var a = arguments;
    for (var x = 0; x < this.__registered.length; x++) {
      this.__registered[x].Method.call(this.__registered[x].Caller,
      a[0] == null ? null : a[0],
      a[1] == null ? null : a[1],
      a[2] == null ? null : a[2],
      a[3] == null ? null : a[3],
      a[4] == null ? null : a[4],
      a[5] == null ? null : a[5],
      a[6] == null ? null : a[6],
      a[7] == null ? null : a[7],
      a[8] == null ? null : a[8],
      a[9] == null ? null : a[9])
    }
  }
}

For the Fire event, there were two possible approaches. When a function is passed as a parameter, you can execute the "call" method on the function. This has the same effect as invoking through Reflection. The second option was to use eval to call the method. eval is considerably slower than using the "call" method. When using the "call" method, there is a limitation. The parameters for call need to be explicitly included in the call. This means there is a fixed limit to the number of parameters an event can handle. The good news is the framework can be extended to handle as many as required. The call above includes parameters such as: a[0] == null ? null : a[0].

By continuing past index position 9, you can extend the parameter count limitation as far as required for your implementation.

Using the Code

The following is one of the most basic implementations of the framework. Try the following code:

JavaScript
var eventTest = new uiEvent();
var context1 = { Name: 'Dave' };
var context2 = { Name: 'Chris' };

function delegate1(mood) {
  alert(this.Name + ' is ' + mood);
}
function delegate2(mood) {
  alert(mood + ' is not a good thing for ' + this.Name);
}

eventTest.Register(delegate1, context1);
eventTest.Register(delegate2, context2);

eventTest.Fire('sad');

The following message boxes will be displayed:

  • "Dave is sad"
  • "sad is not a good thing for Chris"

Points of Interest

There are base events within the DOM such as onmousemove, onkeypress. You would assume that with a single thread, all code stacks would be assigned the same priority and processed in order. This is not the case, e.g., if you have a mouse move event which is quite an intensive process. If a subsequent mouse move event fires before the previous finishes (no idle time on the thread), the mouse move event will always be given priority over executions planned using setInterval and setTimeout. This became an issue when implementing the Animation Framework, that will be described in a later document.

Therefore in this framework, I have created a timer event. When a non-timer event fires, it checks all the registered timer events to see if they have expired, and if so, they get executed first. The following is an example implementation of the timer event.

This example implements a second timer which increments a count every second and updates the window status.

JavaScript
var secondTicks = 0;
function secondIncrement() {
  secondTicks++;
  window.status = secondTicks;
}

var secondTimer = new uiTimer(1000);
secondTimer.OnTick.Register(secondIncrement);

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralA few things [modified] Pin
Helbrax20-May-10 4:55
Helbrax20-May-10 4:55 
Your javascript syntax seems a little backwards in some places. These are nitpicky, but conventions are there for a reason Smile | :) .

Constructors should be uppercase(UIEvent).
Methods should be camel cased(Object.someMethod).
Properties should be camel cased(this.someProperty).
Don't use __ as prefix. It does nothing and looks ugly.
Why not just use [] instead of new Array?(no real reason to use one or the other, but I see you are using {} later in your example).

for (var x = 0; x < this.length; x++)

The length property is evaluated every iteration of the loop. If you modify the size of the array in your loop(or you are using an HTMLCollection, which has a length property but is a "live" collection), you could get weird results. You might want to get into the habit of:
for (var x = 0, z = this.length; x < z; x++)

The length property is not evaluated every iteration of the loop in this example.

In some of your test, you might want to use === instead of ==. === does not do type coercion(== does), so
var x = false;if( x == 0 ){alert('True');} //alerts true

var x = false;if( x === 0 ){alert('True');} //no alert


Also, do not use a literal string in setInterval/Timeout. This causes an eval and can mask errors. Just use the function name, or if you need to pass a value, an anonymous function.
var that = this;  //this will change in setInterval to point to the Window object.
setInterval(function(){uiTimerExecute(that.TimerID);}, interval);


modified on Thursday, May 20, 2010 12:17 PM

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.