Exposing DOM Event Listeners in Context

Mar 19, 2009

I, for one, like the convenience of the old-style event-handling properties of javascript objects. Want to define what an element does when you click it? Just assign a function to the element's .onclick property. A key benefit of this is that, if you want to simulate a click on this element, you don't need to waste code building a DOM event object and then dispatching it upon the element. All you need to do is call the .onclick() function and the element will behave as if it had been clicked, albeit without an associated event object to work on.

document.onload = function() {
  var button = document.getElementById('myButton');

  button.onclick = function(e) {
    alert(this.className);
  };

  button.onclick();
}

...

<button id="myButton" class="HelloWorld">A Button</button>

However, the big drawback of applying event listeners this way is that you are limited to a single event of each type. If you assign another function to the onclick property, it will overwrite the previous code, even if you want both functions to be executed on each click of the element.

This problem is solved by the use of DOM event listeners. You can apply any number of listeners for the same event to a single element, so if you click it, two, five, one hundred or even more functions might be triggered all at once. Most excellent!

document.addEventListener('load', function() {
  var button = document.getElementById('myButton');

  button.addEventListener('click', function(e) {
    this.firstChild.nodeValue = "You clicked me!";
  }, false);

  button.addEventListener('click', function(e) {
    alert(this.className);
  }, false);
}, false);

...

<button id="myButton" class="HelloWorld">A Button</button>

In the case above, clicking the button will change the text on the button, and alert the className "HelloWorld", both handled by different listener functions.

What the DOM method lacks however, is a convenient way to trigger the listener without the user manually initiating the event. What if, for instance, we have a series of drop downs which, on the "change" event, paste their values to a different area of the page? We can apply listeners for the change event to all of the <select> elements, but to initialize the system we'd like to run the change event listener for the first dropdown once the page loads. How would we do this?

Doing it the DOM way, we would manually create a DOM event, initialise it and then dispatch it upon the element, like so:

var dropdown1 = document.getElementById('dropdown1');

var evt = document.createEvent("UIEvents");
evt.initUIEvent("change", true, true, null, 0);
dropdown1.dispatchEvent(evt);

This seems a messy way to do things. Three lines of code just to execute a function we already made? What we need to do is expose this function so that we can execute it anytime with a single call. So let's assign it to a variable within the scope just above the listener:

document.addEventListener('load', function() {
  var assignValue = function(e) {
    // extract the number of this dropdown from the id
    var num = this.id.replace(/^dropdown/, "");

    // Use the number to find the correct <p> element
    var p = document.getElementById('val' + num);

    // Assign the selected value of this dropdown to the <p> element
    p.firstChild.nodeValue = this.value;
  };

  var dropdown1 = document.getElementById('dropdown1');
  dropdown1.addEventListener('change', assignValue, false);

  var dropdown2 = document.getElementById('dropdown2');
  dropdown2.addEventListener('change', assignValue, false);

  var dropdown3 = document.getElementById('dropdown3');
  dropdown3.addEventListener('change', assignValue, false);

  assignValue();
}, false);

...

<select id="dropdown1">
  <option value="None" selected="selected">None</option>
  ...
</select>
<select id="dropdown2"> ... </select>
<select id="dropdown3"> ... </select>

<p id="val1"> - </p>
<p id="val2"> - </p>
<p id="val3"> - </p>

In the code above, we have set a listener on each of the dropdown elements so that once they are selected, their values are copied to the associated paragraph element below. When the document loads, we want to initialise the paragraph associated with dropdown1 to its default value "None".

First we assigned the handler to a variable named assignValue, then applied that handler to all of the dropdowns. Great! So now we can call this function just by calling assignValue() We have exposed the event listener function to the next higher scope.

But there is a problem. When we execute the assignValue() function by itself, the javascript engine starts complaining that this.id is undefined. After some thought we realise that this is because there is no context for the assignValue() function. We can execute it very easily, but when we do, the script has no clue to which dropdown element we are trying to refer.

Here then, is the trick! We need to be able to expose this function for execution, but at the same time keep it in context so that the script knows to execute the listener function of the correct dropdown. To do this, instead of just assigning the handler to a new variable, we'll also assign it as a method of the first dropdown object.

document.addEventListener('load', function() {
  var assignValue = function(e) {
    // extract the number of this dropdown from the id
    var num = this.id.replace(/^dropdown/, "");

    // Use the number to find the correct <p> element
    var p = document.getElementById('val' + num);

    // Assign the selected value of this dropdown to the <p> element
    p.firstChild.nodeValue = this.value;
  };

  var dropdown1 = document.getElementById('dropdown1');
  dropdown1.assignValue = assignValue;
  dropdown1.addEventListener('change', assignValue, false);

  var dropdown2 = document.getElementById('dropdown2');
  dropdown2.addEventListener('change', assignValue, false);

  var dropdown3 = document.getElementById('dropdown3');
  dropdown3.addEventListener('change', assignValue, false);

  dropdown1.assignValue();
}, false);

...

<select id="dropdown1">
  <option value="None" selected="selected">None</option>
  ...
</select>
<select id="dropdown2"> ... </select>
<select id="dropdown3"> ... </select>

<p id="val1"> - </p>
<p id="val2"> - </p>
<p id="val3"> - </p>

Now when we call the dropdown1.assignValue() function, since it is a method of the dropdown1 object, special variables like this are now treated as within the context of the parent object, in other words, within the context of the dropdown itself!

There you have it, using DOM event listener functions and exposing your listener functions in such a way that they can be called without the hassle of creating and dispatching events upon them.

The only thing to note about doing things this way (and even the old assign-to-onclick way) is that when you call the listener function as a method of the parent object, there will be no associated event object passed to the function. If you perform some action in the function that hinges on the contents of the event object, make sure to check whether it actually exists before using its properties.


Comments closed

Recent posts

  1. Customize Clipboard Content on Copy: Caveats Dec 2023
  2. Orcinus Site Search now available on Github Apr 2023
  3. Looking for Orca Search 3.0 Beta Testers! Apr 2023
  4. Simple Wheel / Tire Size Calculator Feb 2023
  5. Dr. Presto - Now with MUSIC! Jan 2023
  6. Archive