Browser Event

  • A browser event is a signal that something has happened. All DOM nodes generate such signals (but events are not limited to DOM).
  • Examples:
    • Mouse: click, contextmenu, mousemove
    • Keyboard: keydown, keyup
    • Form: submit, focus
    • Document: DOMContentLoaded (DOM is fully built)
    • CSS: transitionend (CSS animation finishes)

Event Handler

1. HTML attribute

  • The onclick value which is string
  • It is internally wrapped inside another function and then called.
<input value="Click me" onclick="alert('Click!')" type="button">
<button onclick="alert(this.innerHTML)">Click me</button>
<input value="Click me" onclick="alert(event.type)" type="button">
<!-- Handler internally will be same as:
input.onclick = function(event) {
  alert(event.type);
}
-->

2. DOM property

  • Get the HTML node and assign handler
  • Use onclick not onClick since DOM properties are case sensitive
<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
	alert('Thank you');
  };
 
  // remove handler
  elem.onclick = null; 
</script>

3. Methods

  • addEventListener and removeEventListener
  • Using methods we can add multiple events to the same node
  • This should be the preferred way to add event handlers
function handler() {
  alert('Thanks!');
}
elem.addEventListener("click", handler);
 
// remove handler (requires same fn reference)
elem.removeEventListener("click", handler); 

Event properties

  • event.type: for example click
  • event.clientX, event.clientY: relative coords of pointer
  • event.currentTarget: same as this
  • event.target:
    • element which initiated the event.
    • It does not change on bubbling process
    • most deeply nested element that caused the event

this

  • It is element itself and same as currentTarget
<div id="outer">
  <h1>Hello World!</h1>
  <p>This is a paragraph.</p>
</div>
 
<script>
	outer.addEventListener('click', function(e) {
    	alert(`target = ${e.target.tagName}, this = ${this.tagName}, currentTarget = ${e.currentTarget.tagName}`);
    	// target = H1, this = DIV, currentTarget = DIV
    	// target = P, this = DIV, currentTarget = DIV
    })
</script>

Event methods

  • event.stopPropagation(): stops propagating upwards but all handlers run on current element
  • event.stopImmediatePropagation(): stops propagating upwards and no other handler runs
  • event.preventDefault(): prevent default action by browser

Default action

  • event.preventDefault(): prevent default action by browser after handler is executed
    • For <a> tag it is navigation to new url
    • For <button> on form it is submit action
  • event.defaultPrevented is true if default action was prevented
  • For event types with pattern: on<EventName> we can also return false to prevent default action

Event Phases

  • Capturing Phase - Event goes down the element. Traverses the tree to find the target element
  • Target Phase - Event reached the target element
  • Bubbling Phase - Event bubbles up from the element

Event Bubbling

  • When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors
  • This bubbling occurs up the element’s ancestors all the way to the document
  • Event bubbling is the mechanism behind event delegation
  • Almost all events bubble. For instance focus doesn’t bubble

Event Delegation

  • The idea is that if we have a lot of elements handled in a similar way, then instead of assigning a handler to each of them – we put a single handler on their common ancestor.
  • This pattern is based on bubbling and capturing
  • We put event handler on ancestor and determine who initiated the event using event.target
  • HTML
<ul id="item-list">
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
</ul>
  • JS
const itemList = document.getElementById('item-list');
 
itemList.addEventListener('click', (event) => {
  if (event.target.tagName === 'LI') {
    console.log(`Clicked on ${event.target.textContent}`);
  }
});

The Behavior pattern

  • Using event delegation we can implement behavior pattern
    • We add custom attribute to element to describe behavior
    • A document wide handler tracks the event and determine if the event happens on the element with the custom attribute
  • The following example illustrates how we add single event listener and able to implement sort functionality on click of table header
  • The custom attribute used here is data-type to determine the data type for sorting
<!DOCTYPE HTML>
<html>
 
<head>
  <meta charset="utf-8">
  <style>
    table {
       border-collapse: collapse;
     }
     th, td {
       border: 1px solid black;
       padding: 4px;
     }
     th {
       cursor: pointer;
     }
     th:hover {
       background: yellow;
     }
  </style>
</head>
 
<body>
  <table id="grid">
    <thead>
      <tr>
        <th data-type="number">Age</th>
        <th data-type="string">Name</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>5</td>
        <td>John</td>
      </tr>
      <tr>
        <td>2</td>
        <td>Pete</td>
      </tr>
      <tr>
        <td>12</td>
        <td>Ann</td>
      </tr>
      <tr>
        <td>9</td>
        <td>Eugene</td>
      </tr>
      <tr>
        <td>1</td>
        <td>Ilya</td>
      </tr>
    </tbody>
  </table>
 
  <script>
    grid.addEventListener("click", function(e) {
      if (e.target.tagName === 'TH') {
        // calculate the column number
        let col = Array.from(e.target.parentNode.children).indexOf(e.target);
 
        // calculate the column data type
        let colDataType = e.target.dataset.type;
 
        // Get parent of all the rows
        let tbody = this.querySelector('tbody');
 
        // sort all the rows
        Array.from(tbody.children).sort((tr1, tr2) => {
          let data1 = colDataType == "number" ? Number(tr1.children[col].innerText) : tr1.children[col].innerText;
          let data2 = colDataType == "number" ? Number(tr2.children[col].innerText) : tr2.children[col].innerText
 
          if (data1 < data2) {
            return -1;
          } else if (data1 > data2) {
            return +1;
          } else {
            return 0;
          }
        }).forEach((node) => tbody.appendChild(node)) // re-order nodes
      }
    })
  </script>
 
</body>
</html>