Event delegation

  1. The Ba gua example
  2. The menu example
  3. The nested menu example
  4. Actions in the markup
  5. Summary

The event delegation helps to simplify event handling by smart use of bubbling. It is one of the most important JavaScript patterns.

The delegation concept

If there are many element inside one parent, and you want to handle events on them of them - don’t bind handlers to each element.

Instead, bind the single handler to their parent, and get the child from event.target.

The Ba gua example

For example, here’s a Ba gua chart. The ancient Chinese philosophy table.

Click on the cells. They are highlightable.

The table contains 9 cells. Each cell has text and STRONG tag to format directions: South, North etc. The source playground is at tutorial/browser/events/delegation/bagua/index.html.

The important point is how highlighting is implemented.

The example uses event delegation. Instead of attaching a handler to each cell, we attach a single handler to the whole table, which uses event.target to get the originating element.

table.onclick = function(event) {
  event = event || window.event
  var target = event.target || event.srcElement
  // ...
}

A click can happen on any tag inside the table. For example, on the <STRONG> tag. It then bubbles up:

To find the cell, we need to follow parentNode chain:

table.onclick = function(event) {
  event = event || window.event
  var target = event.target || event.srcElement
  
  while(target != table) { // ( ** )
    if (target.nodeName == 'TD') { // ( * )
       toggleHighlight(target)
    }
    target = target.parentNode
  }
}

The code above follows the general delegation scheme:

  1. In the handler we can grab the originating target. It can be a TD, but also any other tag, like STRONG. A click can also occur between cells, in this case, the target may be TR or a TABLE.
  2. We go up to the parent chain until we either meet TD or hit the table.
  3. ( * ) If we meet TD - process it.
    ( * ) If we hit the table, it means the click was between table cells or on table caption. Do nothing with it.

The menu example

Event delegation allows to graciously handle trees and nested menus.

Let’s first discuss a one-level clickable menu from the list:

<ul id="menu">
  <li><a class="button" href="/php">PHP</a></li>
  <li><a class="button" href="/html">HTML</a></li>
  <li><a class="button" href="/javascript">JavaScript</a></li>
  <li><a class="button" href="/flash">Flash</a></li>
</ul>

The example above is just a semantic HTML/CSS. Clicks on menu elements will be handled by JavaScript, but items are represented as A to ensure accessibility for visitors without JavaScript (which are mostly search engines).

The links should become clickable, like in the example below:

Mouse over (hover) state

Hover state indication in the menu above is implemented with pure CSS.
As of now, all browsers allow that. IE6 has problems with hover on arbitrary elements, but for A tags :hover works fine.

With delegation, we can set a single handler on the whole menu:

document.getElementById('menu').onclick = function(e) {
  e = e || event   
  var target = e.target || e.srcElement 

  if (target.nodeName != 'A') return
    
  var href = target.href
  alert( href.substr(href.lastIndexOf('/')+1) )

  return false // prevent url change
}

The code does not ascend the parentNode chain, because there are no nested tags inside A.

Full source in the playground: tutorial/browser/events/delegation/menu/index.html.

The nested menu example

A nested menu has similar semantic structure:

<ul id="menu">
<li><a class="button" href="/php">PHP</a>
  <ul>
    <li><a href="/php/manual">Manual</a></li>
    <li><a href="/php/snippets">Snippets</a></li>
  </ul>
</li>
<li><a class="button" href="/html">HTML</a>
  <ul>
    <li><a href="/html/information">Information</a></li>
    <li><a href="/html/examples">Examples</a></li>
  </ul>
</li>
</ul>

With the help of CSS, second-level UL can be hidden until a mouse hovers over outer LI.

The nested menu actually causes *no changes in JavaScript at all.

Full source in the playground: tutorial/browser/events/delegation/menu-nested/index.html.

We can also add/remove menu items in runtime, without any handlers management.

We can even take the menu and replace it’s innerHTML with another menu. And it will work fluently. Thanks delegation.

There is a message list. Add a delete button to each message to remove it.

Use event delegation. Single event handler for everything.

The result should work like this (click on the right-top image):

As the source, you can use either a sample page tutorial/browser/events/messages-src/index.html, or take a working example with per-element handlers tutorial/browser/events/messages/index.html and modify it to use delegation.

Open hint 1
Hint 1

The click handler on container should first check if the click actually happened on the delete button.

Use target/srcElement for that.

If yes, then remove the parent div.

Open solution
Solution

The solution is shown here.


Create an image gallery that will change main image on thumbnail click.

Should look like this:

You can see and export the source HTML, images and thumbnails at tutorial/browser/events/gallery-src/index.html.

Open solution
Solution

The solution is to put a handler on either individual images or to the #thumbs container.

On event it should change #largeImg src to href of the link and change alt to it’s title.

The border on hover is handled by pure CSS. That works everywhere except IE6. We could use an IE-only behavior for IE6, or just skip it if not strictly required.

The image can be accessed without JavaScript through the link.

That’s all a result of proper HTML/CSS structure.

The result may look like this:

var largeImg = document.getElementById('largeImg')
		
document.getElementById('thumbs').onclick = function(e) {
  e = e || window.event
  var target = e.target || e.srcElement

  if (target.nodeName != 'IMG') return

  var anchor = target.parentNode
	
  largeImg.src = anchor.href
  largeImg.alt = anchor.title
			
  return false
}

See it in action in the task text or here.


Make all links inside the <DIV id="content"> ask if the visitor wants to leave and stop if he doesn’t.

Below is how it should work (in iframe):

  • The DIV content may be AJAX-loaded. It’s innerHTML can be replaced any time, and the links behavior should not change. Use delegation.
  • The DIV contents can contain nested tags both outside the links ( like <P>) and inside them (like I or B).

The source page is at tutorial/browser/events/links-src.html

Open solution
Solution

The task is classical for event delegation.

In real life, we could catch also add AJAX-logging to the server and see where our users leave.

We catch the event on contents and ascend the parentNode until either process A or hit the container.

document.getElementById('contents').onclick = function(evt) {
    var evt = evt || event
    var target = evt.target || evt.srcElement

    function handleLink(href) {    
      var isLeaving = confirm('Leave to '+href+'?')      
      if (!isLeaving) return false
    }
    
    while(target != this) {
      if (target.nodeName == 'A') {
        return handleLink(target.href)
      }
      target = target.parentNode
    }
  }

See the full solution at tutorial/browser/events/links.html.

Actions in the markup

Table cells and nested menu items are the examples of similar elements handling. It worked so well, because actions on children elements were same.

But event delegation can be also used to handle complely different actions.

For example, we need to create an menu with different buttons: Save, Load, Search etc.

An obvious solution would be to write a JavaScript code to find each button and bind to it the unique handler.

A smart way is to assign a single handler for the whole menu. All clicks inside the menu will get into the handler.

But how it will know which button is clicked and how to handle it? For that purpose, in every button, we put the triggered method name into a custom attribute named data-action (could be any name, but data- is valid in HTML5):

<button data-action="Save">Click to Save</button>

The handler reads the attribute and executes the method. See the working demo below:

<div id="menu">
  <button data-action="Save">Click to Save</button>
  <button data-action="Load">Click to Load</button>
</div>

<script>
function Menu(elem) {
  this.onSave = function() { alert('saving') }
  this.onLoad = function() { alert('loading') }

  var self = this

  elem.onclick = function(e) {
    var target = e && e.target || event.srcElement
*!*
    var action = target.getAttribute('data-action')
    if (action) {
      self["on"+action]()
    }
*/!*
  }
}

new Menu(document.getElementById('menu'))
</script>

Note how the var self = this trick is used to keep a reference to the Menu object. Otherwise, the handler would be unable to call Menu methods, because it’s own this references the element.

data-* attributes

HTML attributes starting with data-... are valid in HTML5.

There is a section in the specification about it. Also, there is an API which helps to manipulate such attributes, but it is not supported by all major browsers yet.

So, as of now, the main idea of using data-* attributes is to pass HTML5 validator and be future-compatible.

What did we win by using event delegation here?

  • No need to write JavaScript code to assign a handler to each button. Less code, less time spent in initialization.
  • HTML structure becomes is really flexible. We can add/remove buttons any time. An action corresponds to "onMethod". Simple.
  • The approach integrates with semantic markup. We could use classes “action-save”, “action-load” instead of data-action. The handler would check for action- class and call the corresponding method. Very convenient indeed.

Summary

Event delegation is cool. It is one of the most useful JavaScript patterns.

The prerequisite is a single container with elements which allow common handling.

The algorithm:

  1. Bind a handler to the container.
  2. In the handler: get event.target.
  3. If necessary, climb up the target.parentNode chain, until either the first suitable target is found (and handle it), or the container (this) is reached.

Use-cases:

  • Single handler for similar actions on many children
  • Simplifies architecture for different actions, if the action can be figured out from the target.

Benefits:

  • Simplifies initialization, saves memory from extra handlers.
  • Simplifies updates.
  • Allows to use innerHTML without additional processing.

Of course, as any pattern, event delegation has it’s limits.

  • First, an event should bubble in IE. Most of events do bubble, but not all of them. For other browsers, capturing phase is also suitable.
  • Second, delegation in theory puts extra load on the browser, because the handler runs when an event happens anywhere inside the container. So, most of time the handler may do idle loops. Usually it’s not a big deal.

Tutorial

Donate

Donate to this project