DOM in JS

Photo by Max Chen on Unsplash

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

💡
Think of it as a big tree of elements

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");
💡
Everything related to DOM actions will be controlled by the document (create an element, query an element in the DOM, styleSheet, etc)

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 )

💡
My trick to remember it is to think of the node element as a CSS selector. We have firstChild,lastChild,previousSibling,nextSibling,parentNode,children
<!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)

💡
But we should know that these properties are very common and are not very specific to DOM elements. All of its properties is related to the relationship between its and others

💡
An element node corresponding to <a> has link-related properties, and the one corresponding to <input> has input-related properties, and so on. But they all share common properties and methods from the elemental single hierarchy

Element interface

Basically, an interface contains a bunch of methods that other objects can inherit from it.

Properties

Methods

💡
The Element interface inherits properties and methods from the more general 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 interface
document.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

💡
Design is the hard part of mordern UI. We always have different ideas of how certain things should be displayed in certain ways

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 ) {
  ...
}
💡
If we console.log(element), you'll get the XML view of the node, if we want to see all the properties of the object, we will use dir(element);

Get a reference to multiple DOM elements

HTML CollectionsNodeList
document.getElementsByTagNamedocument.getElementsByName
document.getElementsByClassNamedocument.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
💡
And once we get our hands on the DOM element, we can do whatever we want with that element

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>

More on HTML form

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 .childrenis a property of an Element. 1 Only Elements have .children, and these children are all of type Element. 2

However, .childNodesis aproperty of Node. .childNodes 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 .textContentproperty represents the text content of thenode andits descendants whereas the nodeValueproperty 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);

💡
Everything in a DOM is a node (including text, comments, etc )

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

// 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
💡
A style attribute is a special attribute of an HTML element that we cannot set like a normal attribute. We have to do it this way node.style.color="red" or node.attributeStyleMap.set("color","red");
  • 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>
💡
If you think about it, every attribute in an HTML element is the state of that element, but the standard attribute does not cover enough of the wide range of scenarios (remind me of ReactJS) where we have so many states for a component (toggle, fetching, selection, filter, etc)

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
💡
Classes are really hard to describe STATE. Classes are meant for describing style but for some reason, we still use state to transfer it to classes

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

💡
document.appendChild with an existing node. If the given child is a reference to an existing node in the document, appendChild() moves it from its current position to the new position

Controlling 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