HTML DOM
# HTML DOM
When a web page is loaded, the browser creates a Document Object Model (DOM) of the page.
# What is DOM
DOM is the programming interface for web documents. It represents the page so that a program (usually in JavaScript) can change the document structure, style, and content.
Document
- Entire web page (HTML or XML)
Object
- Every part of web page is converted to an object
Model
- Tree structure used to represent relationships between these objects
The DOM itself is not part of JavaScript language; it is a Web API that can be used to access and modified web pages.
# DOM data types
# Node types
Element Nodes: These represent HTML or XML tags in the document. For example,
<div>
,<span>
,<a>
etc.Text Nodes: These contain textual data and are always leaf nodes, meaning they do not have children.
Attribute Nodes: These represent attributes of elements, like class, id, href etc.
Document Node: This is the root node that represents the entire document.
Comment Nodes: These represent comments in the HTML/XML document.
Document Fragment Nodes: These are essentially lightweight Document nodes that hold a part of the Document tree. They are useful for offscreen manipulations.
# Node properties
Each node in the DOM tree can be identified by a node type. JavaScript uses integer number to determine the node types.
For example, 9
is the value of document node and 1
is for element node.
We can use nodeType
property on any node to get the type of node.
node.nodeType; // 1
Other important node properties include nodeName
and nodeValue
.
The values of these properties depend on the node type. For example, when the node type is the element node, the nodeName
is the element's tag name and nodeValue
is null
.
if (node.nodeType === Node.ELEMENT_NODE) {
console.log(node.nodeName); // "button" for a <button> element
console.log(node.nodeValue); // null
}
# Node vs. Element
It's easy to confuse these two.
Node is the generic name of any object in the DOM tree.
Element is a specific type of Node with nodeType
of 1
.
# Selecting elements
# Select element by id
Looking for the first element with the specific attribute id
.
If element found, returns an Element, else return null
.
const element = document.getElementById(id);
For example
<html>
<head>
...
</head>
<body>
<p id="message">A paragraph</p>
</body>
</html>
The document contains a <p>
element that has an id
attribute with value message
.
const p = document.getElementById("message");
console.log(p); // <p id="message">A paragraph</p>
# Select elements by name
Every element on an HTML document may have a name
attribute:
<input type="radio" name="language" value="JavaScript" />
<input type="radio" name="language" value="TypeScript" />
Unlike id
, name
can be non-unique. Multiple HTML elements can have the same name
value.
To find all elements with a specific name, we can use
const elements = document.getElementsByName(name);
This will return a NodeList
of elements.
# Select elements by tag name
The getElementsByTagName()
method accepts a tag name and returns a live HTMLCollection
of elements with the matching tag name in the order which they appear in the document.
<p>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h2>Heading 2.2</h2>
</p>
const elements = document.getElementsByTagName("h2");
# Select elements by class names
The getElementsByClassName()
method takes a class name and returns an array-like live HTMLCollection
of elements with matching class="name"
.
<div class="container">content...</div>
<div class="container">content...</div>
const elements = document.getElementsByClassName("container");
# Select elements by CSS selectors
We are going to cover querySelector()
and querySelectorAll()
method here as well as the ways to construct the CSS selectors.
const element = parentNode.querySelector(selector); // return single Element
const elements = parentNode.querySelectorAll(selector); // return NodeList of Elements
To convert a NodeList
to an array, we can sue Array.from()
method.
const nodeList = document.querySelectorAll(selector);
const elements = Array.from(nodeList);
# CSS Selectors
- Universal selector
*
: match all - Type selector
tagName
: matching html tag name - Class selector
.className
: matching class name - ID selector
#id
: matching id (should be unique) - Attribute selector
[attribute]
[attribute=value]
exact match[attribute^=pre]
prefix match[attribute$=post]
suffix match[attribute~=substring]
substring match
# Grouping selectors
selector1, selector2, selector 3
- Use comma to separate, the selector list will match any element with at least one of the selectors in the group.
# Selector combinator
Descendent combinator
selector selector
- Use space, i.e.
p a
will match all<a>
elements inside<p>
element. let links = document.querySelector("p a");
- Use space, i.e.
Child combinator
selector > selector
- Finds all elements that are direct children of the first element.
General sibling combinator
selector ~ selector
- Finds siblings that share the same parent.
// this will match all <a> elements follows <p>, immediately or not let links = document.querySelectorAll("p ~ a");
Adjacent sibling combinator
selector + selector
- Matches elements directly follows the preceding element
- For example,
h1 + a
matches all<a>
elements that directly follow anh1
. let links = document.querySelectorAll("h1 + a");
# Pseudo
- Pseudo-classes
The
:
pseudo matches elements based on their states:
element:state
For example, the li:nth-child(2)
selects the second <li>
element in a list:
let listItem = document.querySelectorAll("li:nth-child(2)");
Other commonly used pseudo-selectors
:nth-last-child(2)
count from the last child backwards.:first-child
and:last_child
first and last child of a parent element.:not(selector)
excludes elements matching specified selector.:hover
,:active
,:focus
match based on user interaction.:empty
match elements that have no children.:root
highest-level parent element, usually<html>
.
- Pseudo-elements
The
::
represent entities that are not included in the document.
For example, p::first-line
matches the first line of all p
elements:
let links = document.querySelector("p::first-line");
# Traversing elements
# Find parent element
const parent = node.parentNode;
The parentNode
is read-only.
The document
and DocumentFragment
do not have a parent node. (null
)
# Finding sibling elements
To find the next sibling of an element, use nextElementSibling
attribute.
To find the previous sibling of an element, use previousElementSibling
attribute.
const nextSibling = currentNode.nextElementSibling;
const prevSibling = currentNode.previousElementSibling;
# Finding child elements
To find the first child element of a specified element, use firstChild
property.
To find the last child element, use lastChild
property.
To find all the children, use childNodes
property which returns a NodeList
of all child elements with any node type.
To find all the child element nodes, use children
property which returns a live HTMLCollection
of all child nodes with Element type.
const firstChild = currentNode.firstChild;
const lastChild = currentNode.lastChild;
const childrenNodes = currentNode.childNodes;
const childrenElements = currentNode.children;
# Manipulating elements
# Create an element
To create an HTML element, we use document.createElement
method.
const element = document.createElement(htmlTag);
The document.createElement()
accepts an HTML tag name and returns a new Node
with Element
type.
For example, let's create a new <div>
element.
let div = document.createElement("div");
div.innerHTML = "<p>Hello World</p>";
div.id = "content";
div.className = "greeting";
// after creating the div, we need to attach it to an existing element
document.body.appendChild(div);
- Use
appendChild()
method to add a node to the end of the list of child nodes of a specified parent node. - The
appendChild()
can be used to move an existing child node to the new position within the document.
# Modify innerHTML
The innerHTML
is a property of the Element
that allows us to get or set the HTML markup contained within the element.
const content = element.innerHTML;
If we get the innerHTML
of an element, the browser will serialize the HTML fragment of that element's descendants. The content reflects the latest HTML source, including all changes that have been made since the page was loaded.
For example, Suppose that you have the following markup:
<ul id="menu">
<li>Home</li>
<li>Services</li>
</ul>
The following example uses the innerHTML property to get the content of the <ul>
element:
let menu = document.getElementById('menu');
console.log(menu.innerHTML);
// output
<li>Home</li>
<li>Services</li>
To set the value of innerHTML
property,
element.innerHTML = newHTML;
// clear the body
document.body.innerHTML = "";
!!! Security risks: HTML5 specifies that a <script>
tag inserted with innerHTML should not execute. However, cross-site scripting (XSS) can still occur if we do not have control over what we insert with innerHTML. For example,
const main = document.getElementById("main");
const externalHTML = `<img src='1' onerror='alert("Error loading image")'>`;
// shows the alert
main.innerHTML = externalHTML;
Here we try to insert an <img>
tag but it will error out since the src
is invalid. Then the onerror
function will be run. Instead of a simple alert
, hackers could include malicious code there and initiate an attack.
Therefore, we should not set any content that we have no control to the innerHTML
. If we want to insert plain text into document, we can use textContent
property instead which will be parsed as raw text instead of html.
# Reading and setting text
We can use either textContent
or innerText
property.
textContent
returns the concatenation of the textContent
of every child node.
innerText
takes the CSS style into account (something with display:none
will not return) and returns only human-readable text (comment not returned).
const text = node.textContent;
const text = node.innerText;
Setting textContent
will overwrite all the node's children and replace with a single text node.
node.textContent = newText;
# Inserting nodes
- Insert one or more nodes before the element, use
element.before(node1, node2, ...)
. - Insert one or more nodes after the element, use
element.after(node1, node2, ...)
. - Insert one or more nodes after the last child of a parent node, use
parentNode.append(...nodes)
. - Insert one or more nodes before the first child of a parent node, use
parentNode.prepend(...nodes)
. - Insert a node before another node as a child node of a parent node, use
parentNode.insertBefore(newNode, existingNode)
. - Insert a node after another node as a child node of a parent node, use
parentNode.insertAfter(newNode, existingNode)
. UsingexistingNode.after(newNode)
accomplishes the same thing.
# DocumentFragment interface
The lightweight version of the Document
that is not part of the active DOM tree. This means, any changes made to the document fragment is not reflected in the current document and won't affect performance.
The typical use case is to compose DOM nodes first using DocumentFragment
and insert it into the active DOM tree to get better performance.
const fragment = new DocumentFragment();
// or
const fragment = document.createDocumentFragment();
// then construct DOM nodes
let li = document.createElement("li");
li.innerText = "Hello";
fragment.appendChild(li);
document.body.appendChild(fragment);
# Managing attributes
When the web browser loads a page, it generates the corresponding DOM objects based on the DOM nodes of the document.
For example, if a page contains an input
element:
<input type="text" id="username" />
The web browser will generate an HTMLInputElement
object which has the corresponding properties type
and id
.
The browser will only convert standard attributes to object properties.
Attribute method
element.getAttribute(name)
element.setAttribute(name, value)
element.hasAttribute(name)
element.removeAttribute(name)
Data attribute
If we were to add a custom attribute for an element, we should try to prefix it with data-
, e.g. data-testid
, data-status
. All attributes that start with data-
are reserved for developer use cases.
To access data-*
attributes, we can use the dataset
property.
For example, we have the following div
element with custom attributes:
<div id="main" data-progress="pending" data-value="10%"></div>
Then we can access the attribute via the dataset
property.
const bar = document.querySelector('#main');
console.log(bar.dataset);
// output
[object DOMStringMap] {
progress: "pending",
value: "10%",
}
# Working with styling
# Setting inline styles
To get the inline style of an element, we can use the style
property.
element.style;
// we can modify the inline style
element.style.color = "orange";
The style
property returns a read-only CSSStyleDeclaration
object that contains a list of CSS properties.
To completely override the existing inline style, we can set the cssText
property of the style
object.
element.style.cssText = "color:red;background-color:yellow";
// or
element.setAttribute("style", "color:red;background-color:yellow");
// or adding new styles
element.style.cssText += "color:red;background-color:yellow";
# Getting computed styles
CSS properties may change upon user's interaction with the element. For example, when user hover overs an element, we attach pseudo element :hover
on the element so that the element's style may change for the hover state. We can get the modified style with getComputedStyle(element, pseudoElement)
method of the window
object.
const element = document.querySelector("button");
const style = window.getComputedStyle(element, ":hover");
The method would return a live style object which is an instance of the CSSStyleDeclaration
object.
# Working with CSS classes
className
returns a space-separated list of classes of an element as a string.
<div id="hi" class="hello world" />;
const element = document.querySelector("#hi");
console.log(element.className); // hello world
// we can add to this className by
element.className += newClassName; // prefix newClassName with space
The classList
is a read-only property of an element that returns a live collection of CSS classes. We can modified the CSS classes of an element with this interface.
const classes = element.classList;
classList
methods
add(className)
andremove(className)
replace(oldClass, newClass)
contains(className)
toggle(className)
: If class list contains the className, remove it; otherwise, add it