Skip links
Main content

1 Javascript event handler

Monday 25 October 2010 20:00

As I was implementing a datagrid GUI component, the number of events to be handled increased more and more. In order to cope with their complexity, I had to think of ways to organize these handlers to keep my code simple and readable.

By Patrick van Bergen

The component was designed by our Interaction Design team and it was up to me to make it work. It is an Ajax table, based primarily on an HTML <table> tag, with a number of interactive features like column sorting, column filtering, element selection and inline editing. The events that needed to be handled were mouseup, mousedown, click, keyup, keydown and mousewheel. The component had many handles like the one used to resize the column, the sort buttons, the open-the-filter button, the page-down button and the action buttons for each of the items.

 

To give you an idea of the datagrid component, here is a picture of it in one of its configurations:

 

I started out with 6 handlers attached to each component, to process the types of events I just named. I could have attached a separate handler to each and every button on the component but there are some reasons why this is not such a good idea:

  • Speed and memory: Each row would have its own handler, and even each cell, in the case of in-line editing; instantiating handlers takes time and memory.
  • Handler management: As new data rows are loaded into the component through Ajax, new handlers need to be attached to each cell. Others parts of the grid and even entire grids would be reloaded on the page and handler management itself could be somewhat of a problem.
  • Execution complexity: Some component handles may be nested. For example, the action buttons are inside the table row. When the button is clicked, the row is clicked as well. If they have separate handlers, it is necessary to remember that the event needs to be stopped when the button is clicked in order to prevent it from bubbling up and triggering the row click handler as well.


So in the first implementation I had one function per event type that first determined the source of the event and based on that type the event handled. I will take the mousedown handler as an example. We are using Prototype as a Javascript framework. I will add comments to make clear what happens.

function handleMousedown(event)
{
  // find the type of element that is the meaningful target of the event
  var target = findTarget(event);

  // execute code depending on the type of element
  switch (target.type) {
    case 'column-mover':
      moveColumn(event, target.element);
      break;
    case 'column-sorter':
      sortColumn(target.element);
      break;
    ...
  }
}

function findTarget(event)
{
  // find the source of the event
  var element = Event.element(event);

  // follow the path of the event as it bubbles up the dom
  // and stop when an interesting element is found, or when document is reached
  var type = 'none';
  while (element && element != document) {
    // some examples of testing if the element is meaningful to our component
    if (element.hasClassName('dg-col-move')) {
      // a column-move-button was hit
      type = 'column-mover';
      break;
    }
    if (element.hasClassName('dg-col-sort')) {
      // it's a column-sort-button
      type = 'column-sorter';
      break;
    }
    
      // continue with current element's parent
    element = element.up();   
  }
  return {type: type, element: element};
}

This scheme was working nicely for me until I started using context menus and other popups in the component. Opening popups was simple enough but closing them brought new complexity with it. When the user opens a context menu and later clicks somewhere outside it, it should disappear. There was no other way to do this then to add another event handler that handles clicks outside the component. That means adding a mousedown handler on the document (or body). The problem is that this handler handles events within the component as well. A problem that can be solved by stopping the event from bubbling up the dom as it was handled by the handleMousedown function shown above: Event.stop(event). At the same time the context menu needs to be removed when the user clicks inside the component as well. However, it should not be removed when the user is clicking inside the contextmenu, of course, or it will be removed before the click event is generated and the menu doesn't work.

This construction meant that an mousedown event inside a component was handled twice by the code. I got this to work for the context menu, but later on column selection was added, with its own popup, and inline editing, with edit boxes that appeared and disappeared on mouseclick. When all these cases were handled twice this became complex and the cause of several errors that were found out late.

Adding this problem to the fact that I now needed a document-level mousedown handler for each datagrid on the page and that I had some management to do to ensure that the right component-level handlers were loaded when the component was replaced or removed, it was time for me to take this thing to a different level.

Wouldn't it be nice if there was just a single mousedown handler for all components on the page? That would solve all these problems in an instance.

I wasn't sure if this was possible but it was worth the try. So what I did was I extended the function findTarget() so that it did not just locate the button that was hit, but also the datagrid component in which the button was located. So the latter part of findTarget() looks like this:

// find the datagrid component
var grid = null;
var e = element;
while (e != document) {
  if (e.hasClassName('datagrid')) {
    grid = e;
    break;
  }
  e = e.up();
}

return {type: type, element: element, grid: grid};


Next I created a single mousedown event handler and attached it to document. This handler calls findTarget() to determine the type of target and the exact component where the event has taken place. If the event took place inside a component, the event is sent to a mousedown handler of the object associated with the datagrid. If, on the other hand, the event has taken place outside all components, it will be sent to all datagrid components. So the event is always sent to the component's mousedown handler, with an extra parameter telling if the event has taken place inside or outside the component.

I keep track of all datagrids on the page by making them register themselves with a page-global registry. Each time an event occurs the registry is validated to see if some of the datagrids were removed in the meanwile.

And finally I merged all global handlers into a single function, just to reduce code duplication:

var handleAllEvents = function(event)
{
    var struct = findTarget(event);
    var functionName = 'handle_' + event.type;

    if (struct.grid) {
        // inside a component: lookup the object associated with the html structure
        var component = getComponentByElement(struct.grid);
        // pass the event to the component's designated handler
        component[functionName](event, struct);
    } else {
        // pass the event to all components
        var allGrids = getAllGrids();
        for (var i = 0; i < allGrids.length; i++) {
            allGrids[i][functionName](event, struct);
        }
    }    
}

So there it is: a single event handler for all events of the datagrid components on the page. All events? No, not all of them. I used some other constructions for exceptional cases. For example, I don't handle mousemove events the same way as the other events. That is because I don't want Javascript code to be executed each time the user moves the mouse and I don't want a number of variables to keep track of the fact that the mousemove is actually a drag event. More about this perhaps later in another article.

« Back

Reactions on "1 Javascript event handler"

No posts found

Log in to comment on news articles.