DOM

Document Object Model

Preface: Browser environment, specs

Read more from source.

The JavaScript language was initially created for web browsers. Since then, it has evolved into a language with many uses and platforms.

A platform may be a browser, or a web-server or another host, or even a “smart” coffee machine if it can run JavaScript. Each of these provides platform-specific functionality. The JavaScript specification calls that a host environment.

A host environment provides its own objects and functions in addition to the language core. Web browsers give a means to control web pages. Node.js provides server-side features, and so on.

Browser environment

Here’s a bird’s-eye view of what we have when JavaScript runs in a web browser:

There’s a “root” object called window. It has two roles:

  1. First, it is a global object for JavaScript code.
  2. Second, it represents the “browser window” and provides methods to control it.

Specs

DOM Essentials

!!! info ""

+ W3S Course
+ MDN: DOM Events
+ Document(JAVASCRIPT.INFO)
+ Event Delegation(JAVASCRIPT.INFO)
+ Javascript Char Codes (Key Codes)

Document Object Model, or DOM for short, represents all ^^page content^^ as objects that can be modified.

!!! note

DOM is not only for browsers
For instance, server-side scripts that download HTML pages and process them can also use the DOM. They may support only a part of the specification though.

DOM Tree

There are 12 node types. In practice we usually work with 4 of them:

  1. document object - the main “entry point” to the page(= into DOM), represents the whole document.
  2. element nodes – HTML-tags, the tree building blocks.
  3. text nodes – contain text(spaces and newlines too!).
  4. comment nodes – sometimes we can put information there, it won’t be shown, but JS can read it from the DOM.

node.nodeType - “old-fashioned” way to get the “type” of a DOM node, read only:

<body><!-- this is a comment -->
  <script>
    let elem = document.body;

    // let's examine: what type of node is in elem?
    console.log(elem.nodeType); // 1 => element

    // its first child is...
    console.log(elem.firstChild.nodeType); // 8 => comment

    // and its second child is...
    console.log(elem.childNodes[1].nodeType) // 3 => text(newline)

    // for the document object, the type is 9
    console.log(document.nodeType); // 9
  </script>
</body>

In modern scripts, we can use #!js instanceof and other class-based tests to see the node type.

node.nodeName - returns:

<body><!-- comment -->
  <script>
    // for <body> element
    console.log(document.body.nodeName); // BODY

    // for comment
    console.log(document.body.firstChild.nodeName); // #comment

    // for document
    console.log(document.nodeName); // #document
  </script>
</body>

Walking the DOM

Searching the DOM

Using getElement(s)*

!!! note "This methods used for older code bases."

!!! note "All methods getElementsBy* below return a live collection."

Such collections always reflect the current state of the document and “auto-update” when it changes.

Using querySelector*

!!! note "This methods can select anything inside quotes exactly like selecting in CSS. They are more powerfull than the first three above."

DOM selectors summary table

Method Searches by... Retruns Can call on an element? Live collection?
querySelector CSS-selector One obect :material-check: :material-close:
querySelectorAll CSS-selector Collection of objects :material-check: :material-close:
getElementById id One obect :material-close: - Searches the whole document by calling on document object :material-close:
getElementsByName name Collection of objects :material-close: - Searches the whole document by calling on document object :material-check:
getElementsByTagName tag or '*' Collection of objects :material-check: :material-check:
getElementsByClassName class Collection of objects :material-check: :material-check:

!!! tip "It is important to CACHE selectors in variables."

This is in order to reduce memory usage by js engine(by going to DOM each time when we use selector), e.g: #!js var h1 = document.querySelector("h1");

Additional useful methods

Changing the DOM

Changing nodes content

Creation, insertion, removal of nodes

Creation

Insertion and removal

!!! note "The above insertion methods can only be used to insert DOM nodes or text pieces."

To insert an HTML string “as html”, with all tags and stuff working, in the same manner as #!js element.innerHTML does it use following method:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  // 1. Create <div> element
  let div = document.createElement('div');
  // 2. Set its class to "alert"
  div.className = "alert";
  // 3. Fill it with the content
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

  document.body.append(div);
</script>

<!-- alternative variant using insertAdjacentHTML -->
<script>
  document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
    <strong>Hi there!</strong> You've read an important message.
  </div>`);
</script>
<!-- inserting multiple nodes and text pieces in a single call -->
<div id="div"></div>
<script>
  div.before('<p>Hello</p>', document.createElement('hr'));
</script>

!!! tip "All insertion methods automatically remove the node from the old place."

For instance, let’s swap elements:

html <div id="first">First</div> <div id="second">Second</div> <script> // no need to call remove second.after(first); // take #second and after it insert #first </script>

Insertion and removal(“old school” methods)

!!! note "All these methods return the inserted/removed node."

But usually the returned value is not used, we just run the method.

Changing element properties: class, style

class

style

styles: working tips

HTML attributes vs. DOM properties

!!! tip "For most situations using DOM properties is preferable."

We should refer to attributes only when DOM properties do not suit us, when we need exactly attributes, for instance:

+ We need a non-standard non-“data-*” attribute. See Non-standard attributes use cases
+ We want to read the value “as written” in HTML. For instanse: see note about href attribute later on this page.

DOM properties

DOM nodes are regular JavaScript objects.

HTML attributes

When the browser parses the HTML to create DOM objects for tags, it recognizes standard attributes and creates the corresponding DOM properties from them. But that doesn’t happen if the attribute is non-standard.

Most standard HTML attributes have the corresponding DOM properties. They described in the specification for the corresponding element class(see WHATWG: HTML Living Standard).
For instance, HTMLInputElement class is documented at https://html.spec.whatwg.org/#htmlinputelement.

!!! tip "Alternative way to get DOM properties"
If we’d like to get them fast or are interested in a concrete browser specification – we can always output the element using console.dir(element) and read the properties.
Or explore “DOM properties” in the Elements tab of the browser developer tools.

HTML attributes have the following features:

<body id="test" something="non-standard">
  <script>
    alert(document.body.id); // test
    // non-standard attribute does not yield a property
    alert(document.body.something); // undefined
  </script>
</body>

A standard attribute for one element can be unknown for another one. For instance, "type" is standard for #!html <input>(HTMLInputElement specification class), but not for #!html <body> (HTMLBodyElement specification class).

<body id="body" type="...">
  <input id="input" type="text">
  <script>
    alert(input.type); // text
    alert(body.type); // undefined: DOM property not created, because it's non-standard
  </script>
</body>

Examples of standard attributes and their corresponing DOM nodes properties(depending on their specification class):

^^All^^ attributes are accessible by using the following methods:

Property-attribute synchronization

When a standard attribute changes, the corresponding property is auto-updated, and (with some exceptions) vice versa.

In the example below id is modified as an attribute, and we can see the property changed too. And then the same backwards:

<input>

<script>
  let input = document.querySelector('input');

  // attribute => property
  input.setAttribute('id', 'id');
  alert(input.id); // id (updated)

  // property => attribute
  input.id = 'newId';
  alert(input.getAttribute('id')); // newId (updated)
</script>

But there are exclusions, for instance:

Non-standard attributes use cases

#!js dataset DOM property

Possible problem with custom(non-standard) attributes: they can appear in standard specifications in the future and therefore become unavailable for our use. To avoid conflicts, there exist "data-*" attributes. They are actually a safe way to pass custom data.

All attributes starting with “data-” are reserved for programmers’ use.
They are available in the element.dataset.[“data-“ attribute(with ommited “data-” part) in camelCase]* property.

<style>
  .order[data-order-state="new"] {
    color: green;
  }

  .order[data-order-state="pending"] {
    color: blue;
  }

  .order[data-order-state="canceled"] {
    color: red;
  }
</style>

<div id="order" class="order" data-order-state="new">
  A new order.
</div>

<script>
  // read
  alert(order.dataset.orderState); // new

  // modify
  order.dataset.orderState = "pending"; // we can not only read, but also modify data-attributes
</script>

DOM Events

An event is a signal that something has happened(user actions, document events, CSS events etc.). All DOM nodes generate such signals(but events are not limited to DOM).

Event handlers

Handler - is a function that assigned to an event and runs when event happens.

There are 3 ways to assign event handlers:

  1. HTML attribute: on<event>="..."(... - JavaScript code).

    The browser reads it, creates a new function from the attribute content and writes it to the DOM property.

    <!-- inside onclick we use single quotes, because the attribute itself is in double quotes -->
    <input value="Click me" onclick="alert('Click!')" type="button">
    
    <!-- An HTML-attribute is not a convenient place to write a lot of code,
    so we’d better create a JavaScript function and call it there. -->
    <script>
      function countRabbits() {
        for(let i=1; i<=3; i++) {
          alert("Rabbit number " + i);
        }
      }
    </script>
    
    <input type="button" onclick="countRabbits()" value="Count rabbits!">
    

    Accessing the element using #!js this

    <!-- The value of 'this' inside a handler is the element. The one which has the handler on it. -->
    <button onclick="alert(this.innerHTML)">Click me</button> <!-- Click me -->
    

    !!! note "HTML attributes are used sparingly."

    Because JavaScript in the middle of an HTML tag looks a little bit odd and alien. Also can’t write lots of code in there.

  2. DOM property: element.on<event> = function.

    <!-- we can’t assign more than one handler of the particular event -->
    <input type="button" id="elem" onclick="alert('Before')" value="Click me">
    <script>
      let elem = document.querySelecor("#elem");
      elem.onclick = function() { // overwrites the existing handler
        alert('After'); // only this will be shown
      };
    </script>
    

    Set an existing function as a handler.

    function sayThanks() {
      alert('Thanks!');
    }
    // function should be assigned without parentheses
    elem.onclick = sayThanks;
    
    <!-- On the other hand, in the markup we do need the parentheses -->
    <input type="button" id="button" onclick="sayThanks()">
    
    /* When the browser reads the attribute, it creates a handler function
    with body from the attribute content. So the markup generates this property: */
    button.onclick = function() {
      sayThanks(); // <-- the attribute content goes here
    };
    

    To remove a handler – assign element.on<event> = null

  3. Methods: element.addEventListener(event, handler[, options]) to add handler,
    element.removeEventListener(event, handler[, options]) to remove handler.

    • event - Event name, e.g. "click".

    • handler - The handler function.

    • options - An additional optional object with properties:

      • once: if true, then the listener is automatically removed after it triggers.
      • capture: the phase where to handle the event. See in Bubbling and capturing point.
        For historical reasons, options can also be false/true, that’s the same as {capture: false/true}.
      • passive: if true, then the handler will not call #!js preventDefault()(trying to do this will throw an error).
        That’s useful for some mobile events, like touchstart and touchmove, to tell the browser that it should not wait for all handlers to finish before scrolling.

      For some browsers (Firefox, Chrome), passive is true by default for touchstart and touchmove events.

      See more about #!js preventDefault() in Preventing browser actions point.

    To remove a handler:

    • we should pass exactly the same function as was assigned

      // The handler won’t be removed, because 'removeEventListener' gets another function
      //  – with the same code, but that doesn’t matter, as it’s a different function object.
      elem.addEventListener("click" , () => alert('Thanks!'));
      // ....
      elem.removeEventListener("click", () => alert('Thanks!'));
      
      // Here’s the right way:
      function handler() {
        alert('Thanks!');
      }
      
      input.addEventListener("click", handler);
      // ....
      input.removeEventListener("click", handler);
      // Please note – if we don’t store the function in a variable, then we can’t remove it.
      // There’s no way to “read back” handlers assigned by 'addEventListener'.
      
    • also the phase should be the same

      If we #!js addEventListener(..., true), then we should mention the same phase in
      #!js removeEventListener(..., true) to correctly remove the handler.

    element.addEventListener(event, handler[, options]) allows to assign multiple handlers to one event.

    <input id="elem" type="button" value="Click me"/>
    
    <script>
      function handler1() {
        alert('Thanks!');
      };
    
      function handler2() {
        alert('Thanks again!');
      }
    
      let elem = documnet.querySelecor("#elem");
      elem.onclick = () => alert("Hello");
      elem.addEventListener("click", handler1); // Thanks!
      elem.addEventListener("click", handler2); // Thanks again!
    </script>
    <!-- We can set handlers both using a DOM-property and 'addEventListener'.
    But generally we use only one of these ways. -->
    

    For some events, handlers only work with element.addEventListener

    • DOMContentLoaded event - triggers when the document is loaded and DOM is built

      // will never run
      document.onDOMContentLoaded = function() {
        alert("DOM built");
      };
      
      // this way it works
      document.addEventListener("DOMContentLoaded", function() {
        alert("DOM built");
      });
      
      • transitionend event

    Also element.addEventListener supports objects as event handlers. In that case the method #!js handleEvent is called in case of the event.

    <button id="elem">Click me</button>
    
    <script>
      let obj = {
        handleEvent(event) {
          alert(event.type + " at " + event.currentTarget);
        }
      };
    
      let elem = document.querySelecor("#elem");
      elem.addEventListener('click', obj);
    </script>
    <!-- As we can see, when 'addEventListener' receives an object as the handler,
    it calls 'obj.handleEvent(event)' in case of an event. -->
    

    We could also use a class for that:

    <button id="elem">Click me</button>
    
    <script>
      class Menu {
        handleEvent(event) {
          switch(event.type) {
            case 'mousedown':
              elem.innerHTML = "Mouse button pressed";
              break;
            case 'mouseup':
              elem.innerHTML += "...and released.";
              break;
          }
        }
      }
    
      let menu = new Menu();
      elem.addEventListener('mousedown', menu);
      elem.addEventListener('mouseup', menu);
    </script>
    

    Here the same object handles both events. Please note that we need to explicitly setup the events to listen using #!js addEventListener. The #!js menu object only gets mousedown and mouseup here, not any other types of events.

    The method #!js handleEvent does not have to do all the job by itself. It can call other event-specific methods instead, like this:

    <button id="elem">Click me</button>
    
    <script>
      class Menu {
        handleEvent(event) {
          // mousedown -> onMousedown OR mouseup -> onMouseup, etc.
          let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
          // call one of the methods that defined below(onMousedown() OR onMouseup(), etc.)
          this[method]();
        }
    
        onMousedown() {
          elem.innerHTML = "Mouse button pressed";
        }
    
        onMouseup() {
          elem.innerHTML += "...and released.";
        }
        // Now event handlers are clearly separated, that may be easier to support.
      }
    
      let menu = new Menu();
      elem.addEventListener('mousedown', menu);
      elem.addEventListener('mouseup', menu);
    </script>
    

Event object

No matter how you assign the handler – it gets an event object as the first argument. That object contains the details about what’s happened.

Here’s an example of getting pointer coordinates from the event object:

<input type="button" value="Click me" id="elem">

<script>
  elem.onclick = function(event) {
    // show event type, element and coordinates of the click
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
  // 'event.type' - Event type, here it’s "click".
  /* 'event.currentTarget' - Element that handled the event. That’s exactly the same as 'this',
  unless the handler is an arrow function, or its 'this' is bound(using 'bind') to something else,
  then we can get the element from 'event.currentTarget'. */
  // 'event.clientX / event.clientY' - Window-relative coordinates of the cursor, for pointer events.
</script>

!!! note "The event object is also available in HTML handlers."

js <input type="button" onclick="alert(event.type)" value="Event type">

That’s possible because when the browser reads the attribute, it creates a handler like this: #!js function(event) { alert(event.type) }. That is: its first argument is called event, and the body is taken from the attribute.

Bubbling and capturing

Всплытие и погружение

When an event happens – the most nested element where it happens gets labeled as the “target element” (#!js event.target). Then:

Example of both capturing and bubbling in action:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

The code sets click handlers on every element in the document to see which ones are working.

If you click on #!html <p>, then the sequence is:

  1. HTMLBODYFORMDIVP (capturing phase, the first listener):
  2. PDIVFORMBODYHTML (bubbling phase, the second listener).

Please note, the P shows up twice, because we’ve set two listeners: capturing and bubbling. The target triggers at the end of the first and at the beginning of the second phase.

!!! note "Listeners on same element and same phase run in their set order."

If we have multiple event handlers on the same phase, assigned to the same element with addEventListener, they run in the same order as they are created:

js elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first elem.addEventListener("click", e => alert(2));

Each handler can access event object properties:

Any event handler can stop the event capturing/bubbling by calling:

<!-- here 'body.onclick' doesn’t work if you click on <button> -->
<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>

!!! note "The event.stop[Immediate]Propagation() during the capturing also prevents the bubbling."

In other words, normally the event goes first down (“capturing”) and then up (“bubbling”). But if event.stop[Immediate]Propagation() is called during the capturing phase, then the event travel stops, no bubbling will occur.

!!! warning "Don’t stop bubbling without a need!"

Bubbling is convenient. Don’t stop it without a real need: obvious and architecturally well thought out.
Всплытие – это удобно. Не прекращайте его без явной нужды, очевидной и архитектурно прозрачной.

Sometimes event.stopPropagation() creates hidden pitfalls that later may become problems.

For instance:

1. We create a nested menu. Each submenu handles clicks on its elements and calls stopPropagation so that the outer menu won’t trigger.
2. Later we decide to catch clicks on the whole window, to track users’ behavior (where people click). Some analytic systems do that. Usually the code uses #!js document.addEventListener('click'…) to catch all clicks.
3. Our analytic won’t work over the area where clicks are stopped by stopPropagation. Sadly, we’ve got a “dead zone”.

There’s usually no real need to prevent the bubbling. A task that seemingly requires that may be solved by other means. One of them is to use custom events, we’ll cover them later. Also we can write our data into the event object in one handler and read it in another one, so we can pass to handlers on parents information about the processing below.

Bubbling and capturing lay the foundation for “event delegation” – an extremely powerful event handling pattern.

Event delegation

Event delegation is of the most powerful event handling patterns.

It’s often used to add the ^^same handling for many similar elements^^, but not only for that.

^^same handling for many similar elements^^ - 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.

The algorithm:

  1. Put a single handler on the container.
  2. In the handler – check the source element #!js event.target.
  3. If the event happened inside an element that interests us, then handle the event.

Benefits:

Limitations:

Delegation examples

basic example: highlight a cell <td> on click
<!DOCTYPE HTML>
<html>

<body>
  <style>
    #bagua-table th {
      text-align: center;
      font-weight: bold;
    }

    #bagua-table td {
      width: 150px;
      white-space: nowrap;
      text-align: center;
      vertical-align: bottom;
      padding-top: 5px;
      padding-bottom: 12px;
    }

    #bagua-table .nw {
      background: #999;
    }

    #bagua-table .n {
      background: #03f;
      color: #fff;
    }

    #bagua-table .ne {
      background: #ff6;
    }

    #bagua-table .w {
      background: #ff0;
    }

    #bagua-table .c {
      background: #60c;
      color: #fff;
    }

    #bagua-table .e {
      background: #09f;
      color: #fff;
    }

    #bagua-table .sw {
      background: #963;
      color: #fff;
    }

    #bagua-table .s {
      background: #f60;
      color: #fff;
    }

    #bagua-table .se {
      background: #0c3;
      color: #fff;
    }

    #bagua-table .highlight {
      background: red;
    }
  </style>

  <table id="bagua-table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <script>
    let table = document.getElementById('bagua-table');

    let selectedTd;

    // code for basic explanation, see after this codeblock
    table.onclick = function(event) {
      let td = event.target.closest('td'); // (1)
      if (!td) return; // (2)
      if (!table.contains(td)) return; // (3)
      highlight(td); // (4)
    };

    // more advaced code that do the same as 'table.oncklick = ...' block above
    table.onclick = function(event) {
      let target = event.target; // where was the click?
      while (target != this) {
        if (target.tagName == 'TD') {
          highlight(target); // highlight it
          return;
        }
        // while we are not on 'TD' we "level up" our target variable
        // to the next 'parentNode' until it reaches 'TD' node
        target = target.parentNode;
      }
    }

    function highlight(node) {
      if (selectedTd) { // remove the existing highlight if any
        selectedTd.classList.remove('highlight');
      }
      selectedTd = node;
      selectedTd.classList.add('highlight'); // highlight the new td
    }
  </script>

</body>

</html>
  1. The method element.closest(selector) returns the nearest ancestor that matches the selector. In our case we look for #!html <td> on the way up from the source element.
  2. If #!js event.target is not inside any #!html <td>, then the call returns immediately, as there’s nothing to do.
  3. In case of nested tables, #!js event.target may be a #!html <td>, but lying outside of the current table. So we check if that’s actually our table’s #!html <td>.
  4. And, if it’s so, then highlight it.

As the result, we have a fast, efficient highlighting code, that doesn’t care about the total number of #!html <td> in the table.

actions in markup

действия в разметке

Let’s say, we want to make a menu with buttons “Save”, “Load”, “Search” and so on. And there’s an object with methods save, load, search… How to match them?
The first idea may be to assign a separate handler to each button. But there’s a more elegant solution. We can add a handler for the whole menu and data-action attributes for buttons that has the method to call.
The handler reads the attribute and executes the method.

<div id="menu">
  <button data-action="save">Save</button>
  <button data-action="load">Load</button>
  <button data-action="search">Search</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem; // underscore in '_this.elem' is a naming convention for private vatiables
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('saving');
    }

    load() {
      alert('loading');
    }

    search() {
      alert('searching');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action](); // (**)
      }
    };
  }

  new Menu(menu);
</script>

Please note that #!js this.onClick is bound to #!js this in (*). That’s important, because otherwise #!js this inside it would reference the DOM element(#!js elem), not the #!js Menu object, and #!js this[action] in (**) would not be what we need.

So, what advantages does delegation give us here?

We could also use classes #!css .action-save, #!css .action-load, but an attribute data-action is better semantically. And we can use it in CSS rules too.

The “behavior” pattern

We can also use event delegation to add “behaviors” to elements declaratively, with special attributes and classes.

The pattern has two parts:

  1. We add a custom attribute to an element that describes its behavior.
  2. A ^^document-wide^^ handler tracks events, and if an event happens on an attributed element – performs the action.

The “behavior” pattern can be an alternative to mini-fragments of JavaScript.

example: counter

Here the attribute data-counter adds a behavior: “increase value on click” to buttons:

Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // if the attribute exists...
      event.target.value++;
    }

  });
</script>

If we click a button – its value is increased. Not buttons, but the general approach is important here.
There can be as many attributes with data-counter as we want. We can add new ones to HTML at any moment. Using the event delegation we “extended” HTML, added an attribute that describes a new behavior.

!!! warning "For document-level handlers – always use #!js addEventListener."

When we assign an event handler to the document object, we should always use #!js addEventListener, not #!js document.on<event>, because the latter will cause conflicts: new handlers overwrite old ones.
For real projects it’s normal that there are many handlers on document set by different parts of the code.

example: toggler

A click on an element with the attribute data-toggle-id will show/hide the element with the given id:

<button data-toggle-id="subscribe-mail">
  Show the subscription form
</button>

<form id="subscribe-mail" hidden>
  Your mail: <input type="email">
</form>

<script>
  document.addEventListener('click', function(event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>

Now, to add toggling functionality to an element no need to write JavaScript for every such element. Just use the behavior, i.e. the attribute data-toggle-id. The ^^document-level^^ handler makes it work for any element of the page.
We can combine multiple behaviors on a single element as well.

Browser default actions

There are many default browser actions:

Preventing browser actions

All the default actions can be prevented if we want to handle the event exclusively by JavaScript.

To prevent a default action:

<!-- a click on a link doesn’t lead to navigation; the browser doesn’t do anything -->
<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>

!!! note "Follow-up events."

Certain events flow one into another. If we prevent the first event, there will be no second.

For instance, mousedown on an #!html <input> field leads to focusing in it, and the focus event. If we prevent the mousedown event, there’s no focus.

!!! warning "Stay semantic, don’t abuse. Сохраняйте семантику, не злоупотребляйте."

Technically, by preventing default actions and adding JavaScript we can customize the behavior of any elements. For instance, we can make a link #!html <a> work like a button, and a button #!html <button> behave as a link (redirect to another URL or so).

But we should generally keep the semantic meaning of HTML elements. For instance, #!html <a> should perform navigation, not a button.

Besides being “just a good thing”, that makes your HTML better in terms of accessibility.

Also if we consider the example with #!html <a>, then please note: a browser allows us to open such links in a new window (by right-clicking them and other means). And people like that. But if we make a button behave as a link using JavaScript and even look like a link using CSS, then #!html <a>-specific browser features still won’t work for it.

html <input value="Focus works" onfocus="this.value=''"> <input onmousedown="return false" onfocus="this.value=''" value="Click me">

If the default action was prevented, the value of #!js event.defaultPrevented becomes true, otherwise it’s false.

Sometimes we can use #!js event.defaultPrevented instead of using #!js event.stopPropagation(), to signal other event handlers that the event was handled.

Example: Preventing default actions of contextmenu for #!html <button> element and also for whole document. The problem is that when we click on elem, we get two menus: the button-level and (the event bubbles up) the document-level menu. Here is the solution:

<p>Right-click for the document menu (added a check for event.defaultPrevented)</p>
<button id="elem">Right-click for the button menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    // solution: check if the default action was prevented?
    // If it is so, then the event was handled, and we don’t need to react on it.
    if (event.defaultPrevented) return;

    event.preventDefault();
    alert("Document context menu");
  };
</script>

If we have nested elements, and each of them has a context menu of its own, that would also work. Just make sure to check for #!js event.defaultPrevented in each contextmenu handler.

Dispatching custom events(TODO)

Генерация пользовательских событий

TODO: https://javascript.info/dispatch-events

UI Events

Mouse events

!!! note "Such events may come not only from “mouse devices”."

But are also from other devices, such as phones and tablets, where they are emulated for compatibility.

Mouse event types:

  1. Simple events:

    • mousedown/mouseup - Mouse button is clicked/released over an element.

    • mouseover/mouseout - Mouse pointer comes over/out from an element.
      They trigger even when we go from the parent element to a child element.
      The browser assumes that the mouse can be only over one element at one time – the deepest one.
      See more on mouseover/out point.

    • mouseenter/mouseleave - Mouse pointer enters/leaves the element.
      They only trigger when the mouse comes in and out the element ^^as a whole^^.
      Also they do not bubble.
      See more on mouseenter/leave point.

    • mousemove - Every mouse move over an element triggers that event.

      !!! note "A fast mouse move may skip intermediate elements."

      mousemove doesn't trigger on every pixel. The browser checks the mouse position from time to time.
      That means that if the visitor is moving the mouse very fast then some DOM-elements may be skipped.

      That’s good for performance, because there may be many intermediate elements. We don’t really want to process in and out of each one.

    • contextmenu - Triggers when the right mouse button is pressed. There are other ways to open a context menu, e.g. using a special keyboard key, it triggers in that case also, so it’s not exactly the mouse event.

  2. Complex events(consist of several simple events):

    • click - Triggers after mousedown and then mouseup over the same element if the left mouse button was used.
    • dblclick - Triggers after two clicks on the same element within a short timeframe. Rarely used nowadays.

Mouse event properties

mouse button
modifiers: shift, alt, ctrl and meta

They are true if the corresponding key was pressed during the event.

For instance, the button below only works on ++alt+shift+++ click:

<button id="button">Alt+Shift+Click on me!</button>

<script>
  button.onclick = function(event) {
    if (event.altKey && event.shiftKey) {
      alert('Hooray!');
    }
  };
</script>

!!! tip "Attention: on Mac it’s usually ++cmd++ instead of ++ctrl++."

Even if we’d like to force Mac users to ++ctrl+++click – that’s kind of difficult. The problem is: a left-click with ++ctrl++ is interpreted as a right-click on MacOS, and it generates the contextmenu event, not click like Windows/Linux.

So if we want users of all operating systems to feel comfortable, then together with ctrlKey we should check metaKey.

For JS-code it means that we should check #!js if (event.ctrlKey || event.metaKey).

coordinates

All mouse events provide coordinates in two flavours:

  1. Window-relative: clientX and clientY.

    Are counted from the ^^current^^ window left-upper corner. When the page is scrolled, they change.

    For instance, if we have a window of the size 500x500, and the mouse is in the left-upper corner, then clientX and clientY are 0, no matter how the page is scrolled.
    And if the mouse is in the center, then clientX and clientY are 250, no matter what place in the document it is. They are similar to #!css position:fixed in that aspect.
    Move the mouse over the input field to see clientX/clientY (the example is in the iframe, so coordinates are relative to that iframe):

    <input onmousemove="this.value=event.clientX+':'+event.clientY" value="Mouse over me">
    
  2. Document-relative: pageX and pageY.

    Are counted from the left-upper corner ^^of the document^^, and do not change when the page is scrolled.

Preventing selection on mousedown

Selection cases:

  1. Left mouse holding pressing and moving: makes the selection, often unwanted.
    TODO: There are multiple ways to prevent the selection, that you can read in https://javascript.info/selection-range.

  2. Double mouse click: has a side effect that may be disturbing in some interfaces: it selects text.

    To prevent selection in this case is to prevent the browser action on mousedown. It prevent first selection case too.

    Before...
    <b ondblclick="alert('Click!')" onmousedown="return false">
      Double-click me
    </b>
    ...After
    

    Now the bold element is not selected on double clicks, and pressing the left button on it won’t start the selection.
    The text inside it is still selectable. However, the selection should start not on the text itself, but before or after it. Usually that’s fine for users.

!!! note "Preventing copying."

If we want to disable selection to protect our page content from copy-pasting, then we can use another event: oncopy.

html <div oncopy="alert('Copying forbidden!');return false"> Dear user, The copying is forbidden for you. If you know JS or HTML, then you can get everything from the page source though. </div>

Moving the mouse

mouseover/out

The mouseover event occurs when a mouse pointer comes over an element, and mouseout – when it leaves.

!!! note "If mouseover triggered, there must be mouseout."

In case of fast mouse movements, intermediate elements may be ignored, but one thing we know for sure: if the pointer “officially” entered an element (mouseover event generated), then upon leaving it we always get mouseout.

These event have special property #!js event.relatedTarget

For mouseover:

For mouseout the reverse:

!!! note "#!js event.relatedTarget can be null."

That’s normal and just means that the mouse came not from another element, but from out of the window. Or that it left the window.
If we access #!js event.relatedTarget.tagName, then there will be an error.

mouseout when leaving for a child:

An important feature of mouseout – it triggers, when the pointer moves from an element to its ^^descendant^^(just the same as if it was moving ^^out of^^ the parent element itself), e.g. if we’re on #parent and then move the pointer deeper into #child, we get mouseout on #parent in this HTML:

<div id="parent">
  <div id="child">...</div>
</div>

According to the browser logic: the mouse cursor may be only over a ^^single^^ element at any time – the most nested one and top by z-index. So if it goes to another element (even a descendant), then it leaves the previous one.

mouseover when leaving for a child:

The mouseover event on a descendant ^^bubbles up^^. So, if #parent has mouseover handler, it triggers.

In the example below moving the mouse from #parent to #child, generates two events on #parent:

  1. mouseout [target: parent] (left the parent), then
  2. mouseover [target: child] (came to the child, bubbled).
<!doctype html>
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>

  <style>
    #parent {
      background: #99C0C3;
      width: 160px;
      height: 120px;
      position: relative;
    }

    #child {
      background: #FFDE99;
      width: 50%;
      height: 50%;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
    }

    textarea {
      height: 140px;
      width: 300px;
      display: block;
    }
  </style>

  <div id="parent" onmouseover="mouselog(event)" onmouseout="mouselog(event)">parent
    <div id="child">child</div>
  </div>

  <textarea id="text"></textarea>
  <input type="button" onclick="text.value=''" value="Clear">

  <script>
    function mouselog(event) {
      let d = new Date();
      text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
      text.scrollTop = text.scrollHeight;
    }
  </script>

</body>

</html>

If there are some actions upon leaving the parent element, e.g. an animation runs in #!js parent.onmouseout, we usually don’t want it when the pointer just goes deeper into #parent.
To avoid it, we can check #!js event.relatedTarget in the handler and, if the mouse is still inside the element, then ignore such event.
Alternatively we can use other events: mouseenter and mouseleave, see next point.

mouseenter/leave

mouseenter/mouseleave like mouseover/mouseout trigger when the mouse pointer enters/leaves the element.

But there are two important differences:

  1. Transitions inside the element, ^^to/from descendants^^, are not counted.
  2. Events mouseenter/mouseleave do not bubble.

So if in the example form previous point above we'll change the events on top element #!html <div id='parent' ... > from mouseover/mouseout to mouseenter/mouseleave, we'll see that he only generated events are the ones related to moving the pointer in and out of the top element. Nothing happens when the pointer goes to the child and back. Transitions between descendants are ignored.

event delegation example

Highlighting TD elements as the mouse travels across them:

Beacuse of limitation of "not-bubbling" of the mouseenter/mouseleave events we use mouseover/mouseout events for "delegation" event handling pattern.

In our case we’d like to handle transitions between table cells #!html <td>: entering a cell and leaving it. Other transitions, such as inside the cell or outside of any cells, don’t interest us. Let’s filter them out.

Here’s what we can do:

<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
</head>

<body>

  <style>
    #text {
      display: block;
      height: 100px;
      width: 456px;
    }

    #table th {
      text-align: center;
      font-weight: bold;
    }

    #table td {
      width: 150px;
      white-space: nowrap;
      text-align: center;
      vertical-align: bottom;
      padding-top: 5px;
      padding-bottom: 12px;
      cursor: pointer;
    }

    #table .nw {
      background: #999;
    }

    #table .n {
      background: #03f;
      color: #fff;
    }

    #table .ne {
      background: #ff6;
    }

    #table .w {
      background: #ff0;
    }

    #table .c {
      background: #60c;
      color: #fff;
    }

    #table .e {
      background: #09f;
      color: #fff;
    }

    #table .sw {
      background: #963;
      color: #fff;
    }

    #table .s {
      background: #f60;
      color: #fff;
    }

    #table .se {
      background: #0c3;
      color: #fff;
    }

    #table .highlight {
      background: red;
    }
  </style>

  <table id="table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>

  </table>

  <textarea id="text"></textarea>

  <input type="button" onclick="text.value=''" value="Clear">

  <script>
    // <td> under the mouse right now (if any)
    let currentElem = null;

    table.onmouseover = function(event) {
      // before entering a new element, the mouse always leaves the previous one
      // if currentElem is set, we didn't leave the previous <td>,
      // that's a mouseover inside it, ignore the event
      if (currentElem) return;

      let target = event.target.closest('td');

      // we moved not into a <td> - ignore
      if (!target) return;

      // moved into <td>, but outside of our table (possible in case of nested tables)
      // ignore
      if (!table.contains(target)) return;

      // hooray! we entered a new <td>
      currentElem = target;
      onEnter(currentElem);
    };

    table.onmouseout = function(event) {
      // if we're outside of any <td> now, then ignore the event
      // that's probably a move inside the table, but out of <td>,
      // e.g. from <tr> to another <tr>
      if (!currentElem) return;

      // we're leaving the element – where to? Maybe to a descendant?
      let relatedTarget = event.relatedTarget;

      while (relatedTarget) {
        // go up the parent chain and check – if we're still inside currentElem
        // then that's an internal transition – ignore it
        if (relatedTarget == currentElem) return;

        relatedTarget = relatedTarget.parentNode;
      }

      // we left the <td>. really.
      onLeave(currentElem);
      currentElem = null;
    };

    // any functions to handle entering/leaving an element
    function onEnter(elem) {
      elem.style.background = 'pink';

      // show that in textarea
      text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
      text.scrollTop = 1e6;
    }

    function onLeave(elem) {
      elem.style.background = '';

      // show that in textarea
      text.value += `out <- ${elem.tagName}.${elem.className}\n`;
      text.scrollTop = 1e6;
    }
  </script>

</body>

</html>

Drag'n'Drop with mouse events(TODO)

TODO: https://javascript.info/mouse-drag-and-drop

Pointer events(TODO)

TODO: https://javascript.info/pointer-events

Keyboard events

Pressing a key always generates a keyboard events:

The only exception is ++fn++ key, because it’s often implemented on lower level than OS.

Main keyboard event properties:

!!! tip "#!js event.key vs. #!js event.code"

To handle ^^keyboard layout-dependant^^ keys → #!js event.key is the way to go.
Because same letters in different layouts may map to different physical keys, leading to different codes.
See the full list in the specification.

To get a hotkey to work even after a ^^language^^ switch → #!js event.code may be better.

Examples:

!!! warning "Not 100% reliable."

In the past, keyboard events were sometimes used to track user input in form fields. That’s not reliable, because the input can come from various sources(e.g. mobile keyboards formally known as IME(Input-Method Editor)). We have input and change events to handle any input (covered in TODO: Events: change, input, cut, copy, paste). They trigger after any kind of input, including copy-pasting or speech recognition.

!!! tip "We should use keyboard events when we really want keyboard."

For example, to react on hotkeys or special keys.

Scrolling

scroll event - allows reacting to a page or element scrolling

May be used for:

BUT: there is more interesting way to implement these(and many others) functionalitis by using IntersectionObserver which allows asynchronously watch for ^^intersection^^ of the element with his parent or visible document area.

// a small function to show the current scroll:
window.addEventListener('scroll', function() {
  document.getElementById('showScroll').innerHTML = window.pageYOffset + 'px';
});
/*
In action:
  - The 'scroll' event works both on the 'window' and on scrollable elements.
*/

Prevent scrolling

We can’t prevent scrolling by using #!js event.preventDefault() in onscroll listener, because it triggers after the scroll has already happened.
But we can prevent scrolling by #!js event.preventDefault() on an event that causes the scroll, for instance keydown event for ++page-up++ and ++page-down++.
If we add an event handler to these events and #!js event.preventDefault() in it, then the scroll won’t start.

BUT: There are many ways to initiate a scroll, so it’s ^^more reliable^^ to use CSS, #!css overflow: hidden; property.

Document and resource loading

Page: DOMContentLoaded, load, beforeunload, unload

Page load events:

document.readyState - current state of the document, has 3 following values:

We can check #!js document.readyState and setup a handler or execute the code immediately if it’s ready, like this:

function work() { /*...*/ }

if (document.readyState == 'loading') {
  // still loading, wait for the event
  document.addEventListener('DOMContentLoaded', work);
} else {
  // DOM is ready!
  work();
}
Connected Pages
On this page
Document Object Model
  • Preface: Browser environment, specs
    1. Browser environment
    2. Specs
  • DOM Essentials
  • DOM Tree
  • Walking the DOM
  • Searching the DOM
    1. Using getElement(s)*
    2. Using querySelector*
    3. DOM selectors summary table
    4. Additional useful methods
  • Changing the DOM
    1. Changing nodes content
    2. Creation, insertion, removal of nodes
      1. Creation
      2. Insertion and removal
      3. Insertion and removal(“old school” methods)
    3. Changing element properties: class, style
      1. class
      2. style
      3. styles: working tips
    4. HTML attributes vs. DOM properties
      1. DOM properties
      2. HTML attributes
      3. Property-attribute synchronization
      4. Non-standard attributes use cases
      5. #!js dataset DOM property
  • DOM Events
  • Event handlers
  • Event object
  • Bubbling and capturing
  • Event delegation Delegation examples basic example: highlight a cell on click
  • actions in markup
  • The “behavior” pattern
    1. example: counter
    2. example: toggler
  • Browser default actions
    1. Preventing browser actions
  • Dispatching custom events(TODO)
  • UI Events
    1. Mouse events
      1. Mouse event properties
        1. mouse button
        2. modifiers: shift, alt, ctrl and meta
        3. coordinates
      2. Preventing selection on mousedown
      3. Moving the mouse
        1. mouseover/out
        2. mouseenter/leave
        3. event delegation example
    2. Drag'n'Drop with mouse events(TODO)
    3. Pointer events(TODO)
    4. Keyboard events
    5. Scrolling
      1. Prevent scrolling
  • Document and resource loading
    1. Page: DOMContentLoaded, load, beforeunload, unload