DOM Manipulation #
DOM Manipulation #
An exploration into the HTMLElement type
#
In the 20+ years since its standardization, JavaScript has come a very long way. While in 2020, JavaScript can be used on servers, in data science, and even on IoT devices, it is important to remember its most popular use case: web browsers.
Websites are made up of HTML and/or XML documents. These documents are static, they do not change. The Document Object Model (DOM) is a programming interface implemented by browsers to make static websites functional. The DOM API can be used to change the document structure, style, and content. The API is so powerful that countless frontend frameworks (jQuery, React, Angular, etc.) have been developed around it to make dynamic websites even easier to develop.
TypeScript is a typed superset of JavaScript, and it ships type
definitions for the DOM API. These definitions are readily available in
any default TypeScript project. Of the 20,000+ lines of definitions in
lib.dom.d.ts, one stands out among the rest: HTMLElement. This type
is the backbone for DOM manipulation with TypeScript.
You can explore the source code for the DOM type definitions
Basic Example #
Given a simplified index.html file:
<!DOCTYPE html>
<html lang="en">
<head><title>TypeScript Dom Manipulation</title></head>
<body>
<div id="app"></div>
<!-- Assume index.js is the compiled output of index.ts -->
<script src="index.js"></script>
</body>
</html>
Let’s explore a TypeScript script that adds a <p>Hello, World!</p>
element to the #app element.
// 1. Select the div element using the id property
const app = document.getElementById("app");
// 2. Create a new <p></p> element programmatically
const p = document.createElement("p");
// 3. Add the text content
p.textContent = "Hello, World!";
// 4. Append the p element to the div element
app?.appendChild(p);
After compiling and running the index.html page, the resulting HTML will be:
<div id="app">
<p>Hello, World!</p>
</div>
The Document Interface
#
The first line of the TypeScript code uses a global variable document.
Inspecting the variable shows it is defined by the Document interface
from the lib.dom.d.ts file. The code snippet contains calls to two
methods, getElementById and createElement.
Document.getElementById
#
The definition for this method is as follows:
getElementById(elementId: string): HTMLElement | null;
Pass it an element id string and it will return either HTMLElement or
null. This method introduces one of the most important types,
HTMLElement. It serves as the base interface for every other element
interface. For example, the p variable in the code example is of type
HTMLParagraphElement. Also, take note that this method can return
null. This is because the method can’t be certain pre-runtime if it
will be able to actually find the specified element or not. In the last
line of the code snippet, the new optional chaining operator is used
to call appendChild.
Document.createElement
#
The definition for this method is (I have omitted the deprecated definition):
createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
createElement(tagName: string, options?: ElementCreationOptions): HTMLElement;
This is an overloaded function definition. The second overload is
simplest and works a lot like the getElementById method does. Pass it
any string and it will return a standard HTMLElement. This definition
is what enables developers to create unique HTML element tags.
For example document.createElement('xyz') returns a <xyz></xyz>
element, clearly not an element that is specified by the HTML
specification.
For those interested, you can interact with custom tag elements using the
document.getElementsByTagName
For the first definition of createElement, it is using some advanced
generic patterns. It is best understood broken down into chunks,
starting with the generic expression:
<K extends keyof HTMLElementTagNameMap>. This expression defines a
generic parameter K that is constrained to the keys of the interface
HTMLElementTagNameMap. The map interface contains every specified HTML
tag name and its corresponding type interface. For example here are the
first 5 mapped values:
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
"area": HTMLAreaElement;
...
}
Some elements do not exhibit unique properties and so they just return
HTMLElement, but other types do have unique properties and methods so
they return their specific interface (which will extend from or
implement HTMLElement).
Now, for the remainder of the createElement definition:
(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K].
The first argument tagName is defined as the generic parameter K.
The TypeScript interpreter is smart enough to infer the generic
parameter from this argument. This means that the developer does not
have to specify the generic parameter when using the method; whatever
value is passed to the tagName argument will be inferred as K and
thus can be used throughout the remainder of the definition. This is
exactly what happens; the return value HTMLElementTagNameMap[K] takes
the tagName argument and uses it to return the corresponding type.
This definition is how the p variable from the code snippet gets a
type of HTMLParagraphElement. And if the code was
document.createElement('a'), then it would be an element of type
HTMLAnchorElement.
The Node interface
#
The document.getElementById function returns an HTMLElement.
HTMLElement interface extends the Element interface which extends
the Node interface. This prototypal extension allows for all
HTMLElements to utilize a subset of standard methods. In the code
snippet, we use a property defined on the Node interface to append the
new p element to the website.
Node.appendChild
#
The last line of the code snippet is app?.appendChild(p). The
previous, document.getElementById, section detailed that the optional
chaining operator is used here because app can potentially be null at
runtime. The appendChild method is defined by:
appendChild<T extends Node>(newChild: T): T;
This method works similarly to the createElement method as the generic
parameter T is inferred from the newChild argument. T is
constrained to another base interface Node.
Difference between children and childNodes
#
Previously, this document details the HTMLElement interface extends
from Element which extends from Node. In the DOM API there is a
concept of children elements. For example in the following HTML, the
p tags are children of the div element
<div>
<p>Hello, World</p>
<p>TypeScript!</p>
</div>;
const div = document.getElementsByTagName("div")[0];
div.children;
// HTMLCollection(2) [p, p]
div.childNodes;
// NodeList(2) [p, p]
After capturing the div element, the children prop will return an
HTMLCollection list containing the HTMLParagraphElements. The
childNodes property will return a similar NodeList list of nodes.
Each p tag will still be of type HTMLParagraphElements, but the
NodeList can contain additional HTML nodes that the HTMLCollection
list cannot.
Modify the HTML by removing one of the p tags, but keep the text.
<div>
<p>Hello, World</p>
TypeScript!
</div>;
const div = document.getElementsByTagName("div")[0];
div.children;
// HTMLCollection(1) [p]
div.childNodes;
// NodeList(2) [p, text]
See how both lists change. children now only contains the
<p>Hello, World</p> element, and the childNodes contains a text
node rather than two p nodes. The text part of the NodeList is the
literal Node containing the text TypeScript!. The children list
does not contain this Node because it is not considered an
HTMLElement.
The querySelector and querySelectorAll methods
#
Both of these methods are great tools for getting lists of dom elements that fit a more unique set of constraints. They are defined in lib.dom.d.ts as:
/**
* Returns the first element that is a descendant of node that matches selectors.
*/
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;
/**
* Returns all element descendants of node that match selectors.
*/
querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;
The querySelectorAll definition is similar to getElementsByTagName,
except it returns a new type: NodeListOf. This return type is
essentially a custom implementation of the standard JavaScript list
element. Arguably, replacing NodeListOf<E> with E[] would result in
a very similar user experience. NodeListOf only implements the
following properties and methods: length, item(index),
forEach((value, key, parent) => void), and numeric indexing.
Additionally, this method returns a list of elements, not nodes,
which is what NodeList was returning from the .childNodes method.
While this may appear as a discrepancy, take note that interface
Element extends from Node.
To see these methods in action modify the existing code to:
<ul>
<li>First :)</li>
<li>Second!</li>
<li>Third times a charm.</li>
</ul>;
const first = document.querySelector("li"); // returns the first li element
const all = document.querySelectorAll("li"); // returns the list of all li elements
Interested in learning more? #
The best part about the lib.dom.d.ts type definitions is that they are
reflective of the types annotated in the Mozilla Developer Network (MDN)
documentation site. For example, the HTMLElement interface is
documented by this
HTMLElement
page on MDN.
These pages list all available properties, methods, and sometimes even
examples. Another great aspect of the pages is that they provide links
to the corresponding standard documents. Here is the link to the
W3C
Recommendation for
HTMLElement.
Sources:
::: _attribution
© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/dom-manipulation.html{._attribution-link}
:::