Frontend

Basic concepts

For historical reasons (document.write() method), web browsers block rendering HTML webpages when they encounter a <script> tag. To speed up the loading of the HTML document, you can put your <script> tags at the end of the document and/or specify either defer or async boolean attributes with the <script> tag. Deferred scripts run in the order they appear in the document. Async scripts run as soon as possible, but they do not block document parsing. The module scripts run after all of their dependencies and the HTML document have been loaded.

<script defer src="deferred.js"></script>
<script async src="async.js"></script>

You can load a script module on demand with import(), but if you are not using modules, you should add the <script> tag to the document when you want the script to load.

function importScript(url) {
  // Returns a promise that resolves when the script is loaded
  return new Promise((resolve, reject) => {
    let s = document.createElement("script");
    s.onload = () => {
      resolve();
    };
    s.onerror = (e) => {
      reject(e);
    };
    s.src = url;
    document.head.append(s);
  });
}

All scripts or modules of a document share a single global object per browser window or tab. The global object is where JavaScript standard library is defined. You can use window or globalThis to refer to this global object.

In modules, top-level declarations are scoped to the module and can be explicitly exported. However, in non-module scripts, top-level declarations are scoped to the containing document, and the declarations are shared by all scripts in the document. The embedded frame (created by using the <iframe> or <frame> element) has a different global object and the document object than its containing document.

JavaScript programs are single-threaded and begin in a script-execution phase and then transition to an event-handling phase. The transition is marked by firing a "DOMContentLoaded" event on the document object. At this point there may still be async scripts not executed yet. When the document is completely parsed with all additional content such as images loaded, and when async scripts are loaded and executed, the document.readyState property changes to "complete" and the web browser fires a "load" event on the window object.

You can define window.onerror() or window.onunhandledrejection() functions to report client-side unhandled errors to the server for telemetry. Alternatively, you can add event listener window.addEventListener() to register a handler for "unhandledrejection" events.

HTML documents contain HTML elements nested within one another, forming a tree, which is referred to as Document Object Model (DOM).

Event categories

In client-side JavaScript events can occur on any element within an HTML document. Event type is a string that specifies what kind of event occurred. Events are grouped into the following categories:

  • Device-dependent input events: "click", "mousedown", "mousemove", "mouseup", "touchstart", "touchmove", "touchend", "keydown", "keyup", etc.
  • Device-independent input events: "pointerdown", "pointermove", "pointerup", etc.
  • User interface events: "focus", "change", "submit".
  • State-change events: "load", "DOMContentLoaded", "online", "offline", "popstate".
  • API specific events: "waiting", "playing", "seeking", "volumechange".

Event target is the object on which the event occurred or with which the event is associated. Event handler or event listener is a function that handles or responds to an event. Programs register their event handler functions with the event type and the event target. Event object is the object associated with a particular event. And event propagation is the process by which the browser decides which objects to trigger event handlers on.

Registering events

The simplest way to register an event handler is by setting a corresponding property of the event target to the desired event handler function. Because property event handlers are invoked as methods of the object on which they are defined, the this keyword inside event handler refers to the object on which the event handler was registered (but not for arrow functions).

window.onload = function () {
  let form = document.querySelector("form");
  form.onsubmit = function (event) {
    event.preventDefault();
    console.log("Form sent:", event.target[0].value);
  };
};

The event handler properties of document elements can also be defined directly in the HTML file as attributes on the corresponding HTML tag. However, this technique assumes that event targets will have at most one handler for each type of event. Also, JavaScript code executed in HTML document is never in strict mode, which can lead to bugs. Event handlers should not return anything.

<button onclick="console.log('Thank you.');">Click me</button>

A better way is to register event handlers using addEventListener() method on target objects, because such technique does not overwrite previously registered handlers. Each object that can be an event target defines a method named addEventListener() that you can register an event handler on. You can register more than one handler function for the same event type on the same object. The first argument is the event type, and the second argument is the function that should be invoked when the specified even type occurs. The this keyword inside event handler also refers to the target object on which the event handler was registered (but not for arrow functions).

let b = document.querySelector("#mybutton");
b.onclick = function () {
  console.log("Thanks for clicking me!");
};
b.addEventListener("click", () => console.log("Thanks again!"));

The method addEventListener() is coupled with removeEventListener() method that expects the same two arguments but instead removes an event handler function from the object.

function handleMouseUp() {
  document.removeEventListener("mousemove", handleMouseMove);
  document.removeEventListener("mouseup", handleMouseUp);
}

document.addEventListener("mousedown", (e) => {
  console.log(e.x, e.y);
  document.addEventListener("mousemove", handleMouseMove);
  document.addEventListener("mouseup", handleMouseUp);
});

You can create your own event object with CustomEvent() constructor and pass it to dispatchEvent() method of the target object. The first argument to CustomEvent() is a string with the type of event object, and the second argument is an object that specifies the properties of the event object.

// Register a handler for "busy" custom events
document.addEventListener("busy", (e) => {
  if (e.detail) {
    showSpinner();
  } else {
    hideSpinner();
  }
});

// Dispatch event signally busy before performing operation
document.dispatchEvent(new CustomEvent("busy", { detail: true }));

getResource()
  .then((res) => console.log("Resource obtained"))
  .catch((err) => console.error("Error getting the resource"))
  .finally(() => {
    // When operation is done, dispatch event signalling not busy
    document.dispatchEvent(new CustomEvent("busy", { detail: false }));
  });

Event propagation

The methods addEventListener() and removeEventListener() support an optional third argument that could be a boolean value with true meaning the handler function is registered as a capturing event. However, the third optional argument can also be an options object with three properties specifying the options for the event listener. Only capture property is relevant to removeEventListener().

document.addEventListener("click", handleClick, {
  capture: true,
  once: true,
  passive: true,
});

Event bubbling provides an alternative to registering handlers on many individual elements: instead you can register a single event handler on a common ancestor element. All events except "focus", "blur", and "scroll" bubble up the DOM tree.

The first phase of handling events is event capturing that occurs before bubbling (third phase) and invocation of event handlers (second phase). The capturing phase is akin to bubbling in reverse. If a capturing event is registered, the capturing handlers of the window object are invoked first, then document object, and so on down the DOM tree until the capturing handlers of the parent of the event target are invoked. Capturing event handlers registered on the event target itself are not invoked.

Many of the events that happen in the browser have their default actions. You can cancel those default action by invoking the preventDefault() method of the event object. You can also cancel the propagation of events by calling the stopPropagation() method of the event object. The stopImmediatePropagation() works like stopPropagation() method but also prevents the invocation of any subsequent event handlers registered on the same object.

Selecting elements

The method querySelector() takes a CSS selector string as its argument and returns the first matching element that it finds or null if it can't find any. The querySelectorAll() method works similar but returns all matching elements in the document. The return value of querySelectorAll() is an array-like object NodeList. To convert NodeList to an array, pass it to Array.from() method.

// Select a <p> element with class caption that immediately follows <div>
let pCaption = document.querySelector("div + p.caption");
// Select all <p> elements that are sibling of <h2> and come after it
let h2p = document.querySelectorAll("h2 ~ p");

The querySelector() and querySelectorAll() methods can be invoked on the document object and any element object. When invoked on an element, these methods will return only descendants of that element.

// Select any <span> in French with class "warning" that is descendant of pCaption
let spanWarning = pCaption.querySelector('span[lang="fr"].warning');

The closest() method works like querySelector() but looks for a match above the element in the tree, returning element's ancestors.

function insideList(el) {
  return el.closest("ul,ol,dl") !== null;
}

The matches() method tests whether an element is matched by a CSS selector.

function isHeading(el) {
  return el.matches("h1,h2,h3,h4,h5,h6");
}

Document traversal

You traverse the document tree by using properties of element objects:

  • parentNode refers to the parent of the element,
  • children contains the children of the element,
  • firstElementChild and lastElementChild refer to the first and last children of the element, and
  • nextElementSibling and previousElementSibling refer to the siblings immediately before and after the element.
function traverse(e, f) {
  f(e);
  for (let child of e.children) {
    traverse(child, f);
  }
}

Another way to traverse a document is to work with nodes. A node is a superclass of the element class, and it includes all types of items in the DOM tree, including elements, text, comments, and the document itself. All node objects define the following properties:

  • parentNode is the parent node,
  • childNodes is read-only NodeList of all children of the node,
  • firstChild and lastChild are the first and last child nodes of the node,
  • nextSibling and previousSibling are the next and previous sibling nodes of the node,
  • nodeType is a number that specifies node type: the document is type 9, an element is type 1, text is type 3, and comment is type 8.
  • nodeValue is the text content of a text node or a comment node,
  • nodeName is the HTML tag name of an element node in uppercase.
document.childNodes[1].lastChild.nodeType; // => 1
document.childNodes[1].lastChild.nodeName; // => 'BODY'

Element attributes

The element object defines getAttribute(), setAttribute(), hasAttribute() and removeAttribute() methods for querying, setting, testing, and removing the attributes of an element. However, the same functionality can be achieved by working with properties of element objects. Note that the property name may not match exactly the attribute name.

let button = document.querySelector("#mybutton");
button.id === "mybutton"; // => true
let input = document.querySelector("input[type='text']");
input.defaultValue = "Hello";

The class attribute of an element corresponds to the className property of the element object. It is a space-separated list of CSS classes that apply to the element. The element object also defines a classList property that is an array-like object that behaves like a set of classes with defined methods add(), remove(), contains(), and toggle().

let p = document.querySelector("#p1");
p.className; // => 'caption first'
p.classList; // => ['caption', 'first']
p.classList.add("second");
p.classList.contains("second"); // => true
p.classList.toggle("second"); // => false
p.classList.contains("second"); // => false
p.classList.remove("first");

The dataset attributes of HTML elements are lower-case strings that start with the prefix data-. They are used to add supplementary information to the elements.

<p id="p2" data-paragraph-name="second">Paragraph 2</p>

The elements define a dataset property that is an object with properties that correspond to the data- attributes but in camelCase and without the prefix.

let p2 = document.querySelector("#p2");
p2.dataset.paragraphName; // => 'second'

Element content

The innerHTML property of an element is a string of HTML markup. When set to a new value, the browser parses the string and then replaces the old value with the new value. Appending text to innerHTML is not efficient. The outerHTML property of an element is like innerHTML except that its value includes the element itself.

The insertAdjacentHTML() method allows you to insert a string of HTML markup adjacent to the element itself.

let p3 = document.querySelector("#p3");
p3.insertAdjacentHTML("beforebegin", "<i>A</i>");
// => <i>A</i><p id="p3">Paragraph 3</p>
p3.insertAdjacentHTML("afterbegin", "<i>B</i>");
// => <p id="p3"><i>B</i>Paragraph 3</p>
p3.insertAdjacentHTML("beforeend", "<i>C</i>");
// => <p id="p3">Paragraph 3<i>C</i></p>
p3.insertAdjacentHTML("afterend", "<i>D</i>");
// => <p id="p3">Paragraph 3</p><i>D</i>

The textContent property of an element object queries and sets content of an element as plain text. The textContent property is also defined in text node objects.

p3.textContent; // => 'BParagraph 3C'
p3.textContent = "Hello <span>World</span>";
p3.innerHTML; // => 'Hello &lt;span&gt;World&lt;/span&gt;'

Inline <script> elements with custom type attribute are not displayed by the browser. They have a text property that you can use to embed arbitrary textual data that can be used by your application.

<script type="text/x-custom-data" id="app-config">
  {
    "theme": "dark",
    "language": "en",
    "features": {
        "comments": true,
        "sharing": false
    }
  }
</script>

Storing data within the HTML itself can reduce the need for additional HTTP requests. This can lead to faster initial page load times and can be useful for preloading configuration data, such as initial state.

document.addEventListener("DOMContentLoaded", () => {
  const configData = document.getElementById("app-config").text;
  const config = JSON.parse(configData);
  console.log(config.theme);
});

Node operations

The document class defines the createElement() method to create a new element, and the element class defines the append() and prepend() methods to append strings of text or other elements to its child list.

let p4 = document.createElement("p"); // Create <p> element
let em = document.createElement("em"); // Create <em> element
em.append("there"); // Add text to <em>
p4.append("Hi ", em, "!"); // Add text and <em> to <p>
p4.prepend("Hey! "); // Add text to the start of <p>
p4.innerHTML; // => 'Hey! Hi <em>there</em>!'

The before() and after() methods insert the new content before and after the sibling element. The before() and after() work both on element and text node objects.

let p5 = document.querySelector("#p5");
p5.before(p4); // Move p4 before p5
p5.after(document.createElement("hr")); // Create <hr> and move after p5

One specific element can only occupy one place in the document. If you want to make a copy of an element, use the cloneNode() method with a boolean true in its argument meaning to copy all of its content.

p5.after(p4, p3); // Move p4 and p3 after p5
p5.before(p4.cloneNode(true)); // Create a copy of p4 and move before p5

You can remove an element or text node by invoking its remove() method, or replace it with any number of other elements by calling its replaceWith() method.

p4.replaceWith(p5); // Replace p4 with p5 by moving p5 to p4 position
p5.remove();

Scripting CSS

The simplest way to manipulate CSS in JavaScript is to add or remove CSS class names from the class attribute of HTML elements.

.hidden {
  display: none;
}
let p6 = document.querySelector("#p6");
p6.classList.add("hidden");
p6.classList.remove("hidden");

The DOM defines a style property on all element objects that corresponds to an inline style attribute represented by a CSSStyleDeclaration object. The CSS property names of CSSStyleDeclaration object are defined in camel case format.

p6.style.display = "block";
p6.style.border = "2px solid red";
p6.style.marginLeft = "50px";

You can query the inline style of an element as a single string by the getAttribute() method and copy it to another element with the setAttribute() method.

let p7 = document.querySelector("#p7");
p7.setAttribute("style", p6.getAttribute("style"));

The same can be achieved with the cssText property of the CSSStyleDeclaration object.

let p8 = document.querySelector("#p8");
p8.style.cssText = p6.style.cssText;

The computed style for an element is the set of read-only property values that the browser computes from the element's inline style plus all style rules in all stylesheets. It is the set of properties actually used to display the element. The style properties are absolute with values measured in pixels and colors defined in rgb() or rgba() format. The shortcut properties are not computed, use the fundamental properties instead. Call the getComputedStyle() method of the window object with the first argument specifying the element, and the second optional argument specifying the CSS pseudo-element, such as ::before or ::after.

let styles = window.getComputedStyle(p8);
styles.borderWidth; // => '1.6px'
styles.borderColor; // => 'rgb(255, 0, 0)'

You disable entire CSS stylesheet by setting the disabled property of the <style> and <link> elements to true.

function toggleTheme() {
  let lightTheme = document.querySelector("#light-theme");
  let darkTheme = document.querySelector("#dark-theme");

  if (darkTheme.disable) {
    lightTheme.disabled = true;
    darkTheme.disabled = false;
  } else {
    lightTheme.disabled = false;
    darkTheme.disabled = true;
  }
}

You can also insert new stylesheet into the document using DOM manipulation methods.

function setTheme(name) {
  let link = document.createElement("link");
  link.id = "theme";
  link.rel = "stylesheet";
  link.href = `themes/${name}.css`;

  let currentTheme = document.querySelector("#theme");
  if (currentTheme) {
    currentTheme.replaceWith(link);
  } else {
    document.head.append(link);
  }
}

Or you can insert a <style> tag directly into the HTML document. There are more methods that allow you to manipulate stylesheets with CSSStyleSheet interface and CSS Object Model.

Document geometry

The devicePixelRatio property of the window object specifies how many device pixels are used for each software pixel.

let p9 = document.querySelector("#p9");
let dpr = window.devicePixelRatio;
p9.textContent = `Device pixel ratio: ${dpr}`;

To determine the current position of an element in the viewport, call the getBoundingClientRect() method for block elements and getClientRects() method for inline elements.

let coords = p9.getBoundingClientRect();
coords.top; // => 390
coords.left; // => 8
coords.bottom; // => 408.3999996185303
coords.right; // => 273.6000061035156

To determine which element is at a given location in the viewport, use the elementFromPoint() method of the document object. Call it with x and y viewport coordinates of a point.

document.elementFromPoint(100, 400); // => p9

The scrollTo() method of the window object takes the x and y coordinates of a point and sets theses as the scrollbar offsets. The scrollBy() method is similar to scrollTo(), but its arguments are relative and are added to the current scroll position. To scroll smoothly, pass an object argument with behavior property set to "smooth".

window.scrollTo(5, 10);
window.scrollBy({
  left: 0,
  right: 100,
  behavior: "smooth",
});

The scrollIntoView() method scrolls to the desired HTML element.

let p10 = document.querySelector("#p10");
p10.scrollIntoView({ behavior: "smooth" });

For browser windows, the viewport size is given by window.innerWidth and window.innerHeight properties. To obtain the size of the entire document, use the offsetWidth and offsetHeight properties of the document.documentElement object. The scroll offsets of the document within the viewport are available as window.scrollX and window.scrollY.

window.innerWidth; // => 282
window.innerHeight; // => 682
document.documentElement.offsetWidth; // => 282
document.documentElement.offsetHeight; // => 459
window.scrollX; // => 0
window.scrollY; // => 0

Each element object has the following properties:

  • offsetWidth and offsetHeight return on-screen size in CSS pixels.
  • offsetLeft and offsetTop return the x and y coordinates of the element.
  • offsetParent specifies which element the properties are relative to.
  • clientWidth and clientHeight are like offsetWidth and offsetHeight except that they do not include the border size.
  • scrollWidth and scrollHeight return the size of an element's content area plus its padding plus any overflowing content.
  • scrollLeft and scrollTop give the scroll offset of the element content within the element's viewport.