DOM in JS
The power of pure JS to manipulate the DOM
With the rise of JS libraries (ReactJS, Vue, or Angular), we don't typically write code the way we used to in the past. At some point in my career, I want to escape from the current development of the web with the craziness of libraries and frameworks and dive deep into the foundation. In this blog, you and I will explore the DOMs from the bottom up and understand everything we need to know to master the DOM
1) Key DOM concepts
The window object
The window object is a very special object that represents the entire window
//Some of the common props of window
var width = window.innerWidth;
var height = window.innerHeight;
// location
var currentURL = window.location.href;
window.location.href = 'https://example.com';
// navigator
var userAgent = window.navigator.userAgent;
// local storage
window.localStorage.setItem('key', 'value');
var storedValue = window.localStorage.getItem('key');
//Some of the common methods of window
window.open('https://example.com', '_blank');
window.close();
//Timer
window.setTimeout(function() {
console.log('Delayed code execution');
}, 1000);
var intervalId = window.setInterval(function() {
console.log('Repeated code execution');
}, 2000);
//Scroll
window.scrollTo(0, 100);
window.scrollBy(0, 50);
DOM
DOM is a document object model; its job is to describe the structure of an HTML document and the relationship between different elements like tags, attributes, and texts on the page. In short
Document Object Model
Describe relationships in HTML (parent, child, sibling, etc)
The browser interprets and organizes HTML as a DOM API for CSS and JS to access
The Document Object
The document interface represents the entire HTML document, and it contains the factory methods needed to create all objects in the document. The object name of this class is simply a document. Conceptually, the document object is the root of this document tree, or actually the root of any document, and it provides primary access to everything you need to manipulate the DOM. So the document object has many properties, methods, and events, things like the document's location, its title, the children it has, and so on
/* Properties that usually uses */
const head = document.head;
const body = document.body;
const URL = document.URL;
const title = document.title;
const cookies = document.cookies;
const forms = document.forms;
/* Some of the query methods provided by the document */
document.querySelector(element);
document.querySelectorAll(allElements);
document.getElementById(id);
/* Some of the methods to create an node or element*/
document.createElement("div");
document.createTextElement("Hi");
Inheritance architect in DOM
From the most generic event (EventTarget prototype)
This is the most generic of the DOM types. All you can do with it is add event listeners, remove them and dispatch events
From N
ode (most of properties are related to relationship in DOM )
<!DOCTYPE html>
<html lang="en">
<body>
<a href="#">Hi</a>
<script>
//is <a> a ELEMENT_NODE?
console.log(document.querySelector('a').nodeType === 1); //logs true, <a> is an Element node
//or use Node.ELEMENT_NODE which is a property containg the numerice value of 1
console.log(document.querySelector('a').nodeType === Node.ELEMENT_NODE); //logs true, <a> is an Element node
</script>
</body>
</html>
But as we get down the tree to more specific DOM like Element we have (getAttributeNames,getBoundingClientRect,querySelector,querySelectorAll,etc)
Element interface
Basically, an interface contains a bunch of methods that other objects can inherit from it.
Properties
Methods
Node
interface. TheElement
interface adds features that are specific to working with HTML or XML elements.Most of the methods that we usually work with come from the element interfacedocument.body instanceof EventTarget; //true
document.body instanceof Node; //true (sub-class) more specific
document.body instanceof Element; //true (sub-class) more specific
document.body instanceof HTMLElement; //true (sub-class) more specific
document.body instanceof HTMLBodyElement; //true(sub-class) more specific
Typescript and DOM
Query for an HTML element with TS
document.getElementsByTagName('p')[0]; // HTMLParagraphElement
document.createElementButton("div"); // HTMLButtonElement
document.querySelector("div"); // HTMLDivElement
// with className it is hard for TS to guess
document.getElementsByClassName('idontknow');
Helping TS out
document.getElementsByClassName('my-div') as HTMLDivElement;
Add Element types to React
Interface has been existed for a long-time. But strangely enough, in land of JS we don't have the concept of an interface. In TS, we can utilitize this
Knowing these interface APIs is extremely useful when we're working with the Mordern FE library, like in React, where we need to type the component. For example
Usually, we would like to write some logic on top of the basic HTML interface to allow more dynamic features, for example, coloring the button or aligning the button based on the props the users has passed into the component
2) DOM + Finding elements
How many ways are there to query an element from the DOM?
I'll put these query selectors into three groups
One HTML element
HTML Collection
NodeList
Get a reference to one DOM element
// An ID is a unique item in a web page
let element = document.getElementById("one-item");
//querySelector just like JQuery that let's you query based on HTML
//relationship
element = document.querySelector("section>header a");
if( element !== null ) {
...
}
Get a reference to multiple DOM elements
HTML Collections | NodeList |
document.getElementsByTagName | document.getElementsByName |
document.getElementsByClassName | document.querySelectorsAll |
HTML collections vs NodeList
Similarity
- Array-like object ( has a length property and key is a number )
Differences
- with HTML collections, we can't use array methods but NodeList has all the array modern interfaces we know
const elements = document.queryElementsByTagName("p");
//HTML collections
if(elements.length > 0 ) {
elements.forEach((element)=>{
console.log(element)
/* elements.forEach is not a function */
})
}
/* Work-around is to transfer Array like object to array */
const elements = Array.from(document.queryElementsByTagName("p"));
elements.filter((e)=>e.tagName === "p");
const elements = document.querySelectorsAll("a"); // NodeList
elements.forEach(element => {
console.log(element);
});
// OK
- The second difference is how we access the element from the HTML collection and NodeList
// HTML collection
const elements = document.getElementsByTagName("a");
console.log(elements[0]); // index
console.log(elements.namedItem("id"); // id
console.log(elements.namedItem("name"); // name
// NodeList
const elements = document.querySelectorsAll("a");
console.log(elements[0]); //index
- The third difference is how we update the parent element
const liveElements = document.getElementsByTagName("a");
const staticElements = document.querySelectorAll("a");
const parentElement = document.queryElementById("content");
// add 1 element into parent elements
content.appendChild(document.createElement("a"));
liveElements.length // 3
staticElements.length // 2
Named form element
DOM provides document.forms object
Form elements can have name attributes
Named elements can also be selected
<form id="register" name="register">
<label for="myname">My Name <label>
<input id="myname" name="myname" type="text">
</form>
<script>
// Access through forms
const allForms = document.forms;
const firstFormInTheDOM = allForms[0];
// Access through form name
const form = document.register;
const input = document.register.myname;
input.value = "VinceNguyen"
// Access through form element name
const myName = document.getElementsByName('myname')[0];
</script>
Children vs childNodes
ChildNodes (Node.prototype) (NodeList) makes sense because Node can be everything when Children is more specific ( should be more of HTML behaviors)
Children (Element.prototype) (HTML collection)
From stack-overflow
Understand that .children
is a property of an Element. 1 Only Elements have .children
, and these children are all of type Element. 2
However, .child
Nodes
is aproperty of Node. .childNod
es
can contain any node. 3
A concrete example would be:
let el = document.createElement("div");
el.textContent = "foo";
el.childNodes.length === 1; // Contains a Text node child.
el.children.length === 0; // No Element children.
Most of the time, you want to use.children
because generally you don't want to loop over Text or Comment nodes in your DOM manipulation.
If you do want to manipulate Text nodes, you probably want .
textContent
or .nodeValue
instead
It's important to understand the distinction between the 2,before deciding which one to use: The .textContent
property represents the text content of thenode andits descendants whereas the nodeValue
property represents the value of the current node
<div id="content">
This is my blog
<!-- Comments -->
<p>Javascript basic</p>
<p>JS fundamental </p>
</div>
const content = document.getElementById("content");
console.log(content);
As we see, we have child nodes with the NodeList interface that contain everything (text, comment, tag element, meta tag). On the other hand, children have HTML collections that include HTML elements (Element interface)
/* One thing to keep in mind because children is a HTML collection
It will not have access to array methods interface */
const ingredients = document.getElementById("ingredients");
ingredients.children.forEach((tr)=>{
console.log(tr); /* children.forEach is not a fn */
})
/* Solution for children */
Array.from(ingredients.children).forEach((tr)=>{
console.log(tr); /* children.forEach is not a fn */
p p
});
If we want to target the pure element without text, comments, etc. we have a list of methods available for exactly this use case
InnerHTML vs innerText and textContent
innerHTML
Element.prototype
element.innerHTML = "<b>Hello, World!</b>";
// This will create a bold "Hello, World!"
console.log(element.innerHTML); // Outputs: <b>Hello, World!</b>
Contains characters as well as HTML tags that the browser can interpret to format the text.
Example:
"<b>Hello, World!</b>"
is HTML content where<b>
tags instruct the browser to display "Hello, World!" in bold.
innerText
HTMLElement.prototype
<div id="example"><span style="display:none">Hidden</span>Visible</div>
const element = document.getElementById('example');
// Retrieving innerText
console.log(element.innerText); // Outputs: Visible
// Setting innerText
element.innerText = "<p>New content</p>";
// The element's content is replaced with plain text: <p>New content</p>
textContent
Node.prototype
<div id="example"><span style="display:none">Hidden</span>Visible</div>
const element = document.getElementById('example');
// Retrieving textContent
console.log(element.textContent); // Outputs: HiddenVisible
// Setting textContent
element.textContent = "<p>New content</p>";
// The element's content is replaced with plain text: <p>New content</p>
Contains only the raw characters.
No special rendering or interpretation.
Example:
"Hello, World!"
is plain text that will just be displayed as is.
3) Modifying the DOM
After we have access to the DOM element through JS, we can then do some interesting things with that element
Read and change attributes of a DOM element (not recommended)
// Most HTML attributes have the same name as JS properties
// but exceptions apply , such as className (class) and htmlFor (for)
element.hidden = false;
element.src = "logo.png";
element.className = "myClass"
Working with restricted attributes
The quickest way to work with element attributes in HTML is to use the dot notation. That's not convenient because sometimes we work with restricted attributes in JS (className,htmlFor). JS provides a few functions to deal with it
/* CRUD operators */
node.getAttribute(attributeName);
node.setAttribute(attributeName,value)
node.hasAttribute(attributeName);
node.removeAttribute(attributeName);
/* Example */
node.getAttribute('class');
node.getAttribute('for');
Special HTML attributes tag
A special HTML attribute that we should treat with care
- style
- src
<img src="images/logo.jpg" >
<a href="foo.html"> Click here </a>
<script>
const elementImg = document.querySelector("img");
img.src; = mydomain.com/images/logo.jpg
elementImg.getAttribute("src"); = images/logo.jpg
const linkElement = document.querySelector("a");
linkElement.src; = mydomain.com/foo.html;
linkElement.getAttribute("href"); = foo.html;
</script>
Non-standard HTML attributes
There may be a possible problem with custom attributes. What if we use a non-standard attribute for our purposes, and later the standard introduces it and makes it do something? The HTML language is alive; it grows, and more attributes appear to suit the needs of developers. There may be unexpected effects in such cases.
To avoid conflicts, there exist data-*attributes.
All attributes starting with “data-” are reserved for programmers’ use. They are available in thedataset
property.
<!-- HTML attribute seletor -->
<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"; // (*)
</script>
Controlling classes
node.classList.add(class); // add a class
node.classList.remove(class); // remove a class
node.classList.toggle(class); // add a class if its not exist otherwise remove it
node.classList.length // how many
node.classList.contains // classname
Read and change the style of a DOM
Read the inline styles of an element, i.e., styles that are directly set on the HTML element using the style
attribute.
//Note that whatever property we access in CSS through JS is STRING
// If we need to do some math with it we need to parse it
Style object vs attributeStyleMap object
const heading = document.querySelector("h1");
//READ THE ATTRIBUTE STYLE
const fontSizeString = heading.style.fontSize;
const fontSizeNumber = heading.attributeStyleMap.get("font-size").value;
const fontSizeUnit = heading.attributeStyleMap.get("font-size").unit;
console.log({ fontSizeString, fontSizeNumber, fontSizeUnit });
// "4rem"
// 4
// rem
Change
// You can use dot syntax to access a style object
// That object will have a map to every CSS property you can inline
// in HTML
element.style.color = "blue";
element.style.fontSize = "1.2em";
element.style.borderRightColor = "#FCFCFC";
//Or prefer way it to use styleMapAttribute
element.attributeStyleMap.set("color","red");
Read the computed style of an element
If you want to dive deep into modifying CSS with JS, you can check one of my articles here
Accessing and editing the contents of the elements
Working with text content
const element = document.querySelector("#message");
// we read the current element's content as string
const content = element.textContent;
// we change the contents of the element with a new string
element.textContent = "The text has been changed"
Working with innerHTML
const element = document.querySelector("#message");
// we read the current element's HTML content as string
const content = element.innerHTML;
// we change the contents of the element with a new HTML string
element.innerHTML =
" <h1>Hello</h1>
<p> Welcome to my blog </p>
"
Working with outerHTML
<div id="d">
<p>Content</p>
<p>Further Elaborated</p>
</div>
const d = document.getElementById("d");
console.log(d.outerHTML);
// The string '<div id="d"><p>Content</p><p>Further Elaborated</p></div>'
// is written to the console window
4) Modifying the document
Creating and appending nodes
appendChild()
moves it from its current position to the new positionControlling node insertions with insertBefore
const pNode = document.createElement('p');
const myText = document.createTextNode('Hello there');
pNode.appendChild(myText);
const newNode = document.querySelector('#thevenue');
newNode.insertBefore(pNode,newNode.childNodes[5]);
Cloning and removing nodes
const myNode = document.querySelector('.artist');
const cloneNode = myNode.cloneNode(true); // true -> clone the node and all of its children
//Remove node
const parent = document.getElementById("parent");
const child = document.getElementById("child");
const throwawayNode = parent.removeChild(child);
//Remove a specify child without query its parent
const node = document.getElementById("child");
if (node.parentNode) {
node.parentNode.removeChild(node);
}
//Remove all of its children
const element = document.getElementById("idOfParent");
while (element.firstChild) {
element.removeChild(element.firstChild);
}
Replacing existing nodes
// Given:
// <div>
// <span id="childSpan">foo bar</span>
// </div>
// Create an empty element node
// without an ID, any attributes, or any content
const sp1 = document.createElement("span");
// Give it an id attribute called 'newSpan'
sp1.id = "newSpan";
// Create some content for the new element.
const sp1_content = document.createTextNode("new replacement span element.");
// Apply that content to the new element
sp1.appendChild(sp1_content);
// Build a reference to the existing node to be replaced
const sp2 = document.getElementById("childSpan");
const parentDiv = sp2.parentNode;
// Replace existing node sp2 with the new span element sp1
parentDiv.replaceChild(sp1, sp2);
// Result:
// <div>
// <span id="newSpan">new replacement span element.</span>
// </div>
5) The DOM in actions
In this blog, I've walked through everything we need to know when we work with JS and the DOM from querying a single DOM to multiple DOM, how we manipulate the DOM from attribute and style, add event listeners, changing the content of the DOM (inner HTML, textContent, etc ), how do we appendChild to the DOM, how do we remove the existing child from the DOM. In the end, I also include a practical example for us to see how we combine everything we study together to solidify our knowledge