Introduction to the DOM
Site: | Saylor Academy |
Course: | PRDV401: Introduction to JavaScript I |
Book: | Introduction to the DOM |
Printed by: | Guest user |
Date: | Monday, October 7, 2024, 11:03 PM |
Description
The power of JavaScript is its use in dynamically displaying and manipulating elements on a webpage written in HTML. We know that an HTML page consists of elements such as <head><body> <h1> tags or text fields and buttons. The DOM or Document Object Model represents the structure of these elements as a "tree" data structure after your web browser reads a page. The DOM Application Programming Interface (API) contains methods that provide JavaScript access to these elements. Start by reading this article to learn about the document structure of the DOM and how to find, create and change elements.
"Too bad! Same old story! Once you've finished building your house you notice you've accidentally learned something that you really should have known – before you started".
– Friedrich Nietzsche, Beyond Good and Evil
When you open a web page in your browser, the browser retrieves the page's HTML text and parses it. The browser builds up a model of the document's structure and uses this model to draw the page on the screen.
This representation of the document is one of the toys that a JavaScript program has available in its sandbox. It is a data structure that you can read or modify. It acts as a live data structure: when it's modified, the page on the screen is updated to reflect the changes.
Source: Marijn Haverbeke, https://eloquentjavascript.net/14_dom.html
This work is licensed under a Creative Commons Attribution-NonCommercial 3.0 License.
You can imagine an HTML document as a nested set of boxes. Tags such as <body>
and </body>
enclose other tags, which in turn contain other tags or text. Here's the
example document from the previous chapter:
<!doctype html> <html> <head> <title>My home page</title> </head> <body> <h1>My home page</h1> <p>Hello, I am Marijn and this is my home page.</p> <p>I also wrote a book! Read it <a href="http://eloquentjavascript.net">here</a>.</p> </body> </html>
This page has the following structure:
The data structure the browser uses to represent the document follows this shape. For each box, there is an object, which we can interact with to find out things such as what HTML tag it represents and which boxes and text it contains. This representation is called the Document Object Model, or DOM for short.
The global binding document
gives us access to these objects. Its documentElement
property refers to the object representing the <html>
tag.
Since every HTML document has a head and a body, it also has head and body properties, pointing at those elements.
Think back to the syntax trees. Their structures are strikingly similar to the structure of a browser's document. Each node may refer to other nodes, children, which in turn may have their own children. This shape is typical of nested structures where elements can contain subelements that are similar to themselves.
We call a data structure a tree when it has a branching structure, has no cycles (a node may not contain itself, directly or indirectly), and has a single, well-defined root. In the case of the DOM, document.documentElement
serves
as the root.
Trees come up a lot in computer science. In addition to representing recursive structures such as HTML documents or programs, they are often used to maintain sorted sets of data because elements can usually be found or inserted more efficiently in a tree than in a flat array.
A typical tree has different kinds of nodes. The syntax tree for the Egg language had identifiers, values, and application nodes. Application nodes may have children, whereas identifiers and values are leaves, or nodes without children.
The same goes for the DOM. Nodes for elements, which represent HTML tags, determine the structure of the document. These can have child nodes. An example of such a node is document.body
. Some of these children can be leaf nodes, such as pieces
of text or comment nodes.
Each DOM node object has a nodeType property
, which contains a code (number) that identifies the type of node. Elements have code 1, which is also defined as the constant property Node.ELEMENT_NODE
.
Text nodes, representing a section of text in the document, get code 3 (Node.TEXT_NODE
). Comments have code 8 (Node.COMMENT_NODE
).
Another way to visualize our document tree is as follows:
The leaves are text nodes, and the arrows indicate parent-child relationships between nodes.
Using cryptic numeric codes to represent node types is not a very JavaScript-like thing to do. Later in this chapter, we'll see that other parts of the DOM interface also feel cumbersome and alien. The reason for this is that the DOM wasn't designed for just JavaScript. Rather, it tries to be a language-neutral interface that can be used in other systems as well – not just for HTML but also for XML, which is a generic data format with an HTML-like syntax.
This is unfortunate. Standards are often useful. But in this case, the advantage (cross-language consistency) isn't all that compelling. Having an interface that is properly integrated with the language you are using will save you more time than having a familiar interface across languages.
As an example of this poor integration, consider the childNodes
property that element
nodes in the DOM have. This property holds an array-like object, with a length
property and properties labeled by numbers to access the child nodes. But it is an instance of the NodeList
type,
not a real array, so it does not have methods such as slice
and map
.
Then there are issues that are simply poor design. For example, there is no way to create a new node and immediately add children or attributes to it. Instead, you have to first create it and then add the children and attributes one by one, using side effects. Code that interacts heavily with the DOM tends to get long, repetitive, and ugly.
But these flaws aren't fatal. Since JavaScript allows us to create our own abstractions, it is possible to design improved ways to express the operations you are performing. Many libraries intended for browser programming come with such tools.
DOM nodes contain a wealth of links to other nearby nodes. The following diagram illustrates these:
Although the diagram shows only one link of each type, every node has a parentNode
property that points to the node it is part of, if any. Likewise, every element node (node type 1) has a childNodes
property
that points to an array-like object holding its children.
In theory, you could move anywhere in the tree using just these parent and child links. But JavaScript also gives you access to a
number of additional convenience links. The firstChild
and lastChild
properties point to the first and last child elements or have the value null
for nodes without children. Similarly, previousSibling
and nextSibling
point
to adjacent nodes, which are nodes with the same parent that appear immediately before or after the node itself. For a first child, previousSibling
will be null, and for a last child, nextSibling
will
be null.
There's also the children
property, which is like childNodes
but contains only element
(type 1) children, not other types of child nodes. This can be useful when you aren't interested in text nodes.
When dealing with a nested data structure like this one, recursive functions are often useful. The following function scans a document
for text nodes containing a given string and returns true
when it has found one:
function talksAbout(node, string) { if (node.nodeType == Node.ELEMENT_NODE) { for (let child of node.childNodes) { if (talksAbout(child, string)) { return true; } } return false; } else if (node.nodeType == Node.TEXT_NODE) { return node.nodeValue.indexOf(string) > -1; } } console.log(talksAbout(document.body, "book")); // → trueThe
nodeValue
property of a text node holds the string of text that it represents.Navigating these links among parents, children, and siblings is often useful. But if we want to find a specific node in the document, reaching it by starting at document.body
and following a fixed path of properties
is a bad idea. Doing so bakes assumptions into our program about the precise structure of the document – a structure you might want to change later. Another complicating factor is that text nodes are created even for the whitespace between nodes. The
example document's <body>
tag does not have just three children (<h1>
and two <p>
elements) but actually
has seven: those three, plus the spaces before, after, and between them.
So if we want to get the href
attribute of the link in that document, we don't want to
say something like "Get the second child of the sixth child of the document body". It'd be better if we could say "Get the first link in the document". And we can.
let link = document.body.getElementsByTagName("a")[0]; console.log(link.href);
All element nodes have a getElementsByTagName
method, which collects all elements with the given tag name that are descendants (direct or indirect children) of that node and returns them as an array-like object.
To find a specific single node, you can give it an id
attribute and use document.
instead.
<p>My ostrich Gertrude:</p> <p><img id="gertrude" src="img/ostrich.png"></p> <script> let ostrich = document.getElementById("gertrude"); console.log(ostrich.src); </script>
A third, similar method is getElementsByClassName
, which, like getElementsByTagName
, searches through the contents of an element node and retrieves all elements that have the
given string in their class
attribute.
Almost everything about the DOM data structure can be changed. The shape of the document tree can be modified by changing parent-child relationships. Nodes have a remove
method to remove them from their current parent node. To add
a child node to an element node, we can use appendChild
, which puts it at the end of the list of children, or insertBefore
, which inserts the node given as the first argument before the node given as the second
argument.
<p>One</p> <p>Two</p> <p>Three</p> <script> let paragraphs = document.body.getElementsByTagName("p"); document.body.insertBefore(paragraphs[2], paragraphs[0]); </script>
A node can exist in the document in only one place. Thus, inserting paragraph Three in front of paragraph One will first remove it from the end of the document and then insert it at the front, resulting in Three/One/Two.
All operations that insert a node somewhere will, as a side effect, cause it to be removed from its current position (if it has one).
The replaceChild
method is used to replace a child node with another one. It takes as arguments two nodes: a new node and the node to be replaced. The replaced node must be a child of the element the method is called on. Note that
both replaceChild
and insertBefore
expect the new node as their first argument.
Say we want to write a script that replaces all images (<img>
tags) in the document with the text held in their alt
attributes, which specifies an alternative textual
representation of the image.
This involves not only removing the images but adding a new text node to replace them. Text nodes are created with the
document.
method.
<p>The <img src="img/cat.png" alt="Cat"> in the <img src="img/hat.png" alt="Hat">.</p> <p><button onclick="replaceImages()">Replace</button></p> <script> function replaceImages() { let images = document.body.getElementsByTagName("img"); for (let i = images.length - 1; i >= 0; i--) { let image = images[i]; if (image.alt) { let text = document.createTextNode(image.alt); image.parentNode.replaceChild(text, image); } } } </script>
Given a string, createTextNode
gives us a text node that we can insert into the document to make it show up on the screen.
The loop that goes over the images starts at the end of the list. This is necessary because the node list returned by a method like
getElementsByTagName
(or a property like childNodes
) is live. That is, it is updated as the document changes. If we started from the front, removing the first image would cause the list
to lose its first element so that the second time the loop repeats, where i
is 1, it would stop because the length of the collection is now also 1.
If you want a solid collection of nodes, as opposed to a live one, you can convert the collection to a real array
by calling Array.from
.
let arrayish = {0: "one", 1: "two", length: 2}; let array = Array.from(arrayish); console.log(array.map(s => s.toUpperCase())); // → ["ONE", "TWO"]
To create element nodes, you can use the document.
method. This method takes a tag name and returns a new empty node of the given type.
The following example defines a utility elt
, which creates an element node and treats the rest
of its arguments as children to that node. This function is then used to add an attribution to a quote.
<blockquote id="quote"> No book can ever be finished. While working on it we learn just enough to find it immature the moment we turn away from it. </blockquote> <script> function elt(type, ...children) { let node = document.createElement(type); for (let child of children) { if (typeof child != "string") node.appendChild(child); else node.appendChild(document.createTextNode(child)); } return node; } document.getElementById("quote").appendChild( elt("footer", "—", elt("strong", "Karl Popper"), ", preface to the second edition of ", elt("em", "The Open Society and Its Enemies"), ", 1950")); </script>
Some element attributes, such as href
for links, can be accessed through a property of the same name on the element's DOM object. This is the case for most commonly used standard attributes.
But HTML allows you to set any attribute you want on nodes. This can be useful because it allows you to store extra information in
a document. If you make up your own attribute names, though, such attributes will not be present as properties on the element's node. Instead, you have to use the getAttribute
and setAttribute
methods
to work with them.
<p data-classified="secret">The launch code is 00000000.</p> <p data-classified="unclassified">I have two feet.</p> <script> let paras = document.body.getElementsByTagName("p"); for (let para of Array.from(paras)) { if (para.getAttribute("data-classified") == "secret") { para.remove(); } } </script>
It is recommended to prefix the names of such made-up attributes with data-
to ensure they do not conflict with any other attributes.
There is a commonly used attribute, class
, which is a keyword in the JavaScript language. For historical reasons – some
old JavaScript implementations could not handle property names that matched keywords – the property used to access this attribute is called className
. You can also access it under its real name, "class"
, by using
the getAttribute
and setAttribute
methods.
JavaScript programs may inspect and interfere with the document that the browser is displaying through a data structure called the DOM. This data structure represents the browser's model of the document, and a JavaScript program can modify it to change the visible document.
The DOM is organized like a tree, in which elements are arranged hierarchically according to the structure of the document. The objects
representing elements have properties such as parentNode
and childNodes
, which can be used to navigate through this tree.