The Document Object Model
- When you open a web page in your browser, the browser retrieves the page’s HTML text and parses it, much like the way our parser from Chapter 12 parsed programs. 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.
Document structure
- You can imagine an HTML document as a nested set of boxes.
-
<!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. ItsdocumentElement
property refers to the object representing the<html>
tag. Since every HTML document has a head and a body, it also hashead
andbody
properties, pointing at those elements.
Trees
- Think back to the syntax trees from Chapter 12 for a moment. 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.
- In the case of the DOM,
document.documentElement
serves as the root. - 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 propertyNode.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 standard
- Using cryptic numeric codes to represent node types is not a very JavaScript-like thing to do. 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.
- 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 alength
property and properties labeled by numbers to access the child nodes. But it is an instance of theNodeList
type, not a real array, so it does not have methods such asslice
andmap
. - 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.
Moving through the tree
-
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 achildNodes
property that points to an array-like object holding its children. -
... The
firstChild
andlastChild
properties point to the first and last child elements or have the valuenull
for nodes without children. ... For a first child,previousSibling
will benull
, and for a last child,nextSibling
will benull
. -
There’s also the
children
property, which is likechildNodes
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 (int i = 0; i < node.childNodes.length; i++) { if (talksAbout(node.childNodes[i], string)) { return true; } } } else if (node.nodeType == Node.TEXT_NODE) { return node.nodeValue.indexOf(string) > -1; } } console.log(talksAbout(document.body, "book"));
-
Because
childNodes
is not a real array, we cannot loop over it withfor/of
and have to run over the index range using a regularfor
loop or useArray.from
. -
The
nodeValue
property of a text node holds the string of text that it represents.
Finding elements
-
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 list = document.body.getElementsByTagName("a")[0]; console.log(link.href); // http://eloquentjavascript.net/
-
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 usedocument.getElementById
instead. -
<p>My ostrich Gertrude:</p> <p><img id="gertrude" src="img/ostrich.png"></p> <script> let ostrich = document.getElementById("gertrude"); console.log(octrich.src); </script>
-
A third, similar method is
getElementsByClassName
, which, likegetElementsByTagName
, searches through the contents of an element node and retrieves all elements that have the given string in theirclass
attribute.
Changing the document
-
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 useappendChild
, which puts it at the end of the list of children, orinsertBefore
, 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 bothreplaceChild
andinsertBefore
expect the new node as their first argument.
Creating nodes
-
Say we want to write a script that replaces all images (
<img>
tags) in the document with the text held in theiralt
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.createTextNode
method. -
<p>The <img src="img/cat.png" alt="Cat"> in the <img src="img/hat.png" alt="Hat">.</p> <button onclick="replaceImages()">Replace</button> <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 likechildNodes
) is live. That is, it is updated as the document changes. -
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.createElement
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>
Attributes
-
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
andsetAttribute
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 calledclassName
. You can also access it under its real name,"class"
, by using thegetAttribute
andsetAttribute
methods.
Layout
-
You may have noticed that different types of elements are laid out differently. Some, such as paragraphs (
<p>
) or headings (<h1>
), take up the whole width of the document and are rendered on separate lines. These are called block elements. Others, such as links (<a>
) or the<strong>
element, are rendered on the same line with their surrounding text. Such elements are called inline elements. -
For any given document, browsers are able to compute a layout, which gives each element a size and position based on its type and content. This layout is then used to actually draw the document.
-
The size and position of an element can be accessed from JavaScript. The
offsetWidth
andoffsetHeight
properties give you the space the element takes up in pixels. A pixel is the basic unit of measurement in the browser. It traditionally corresponds to the smallest dot that the screen can draw, but on modern displays, which can draw very small dots, that may no longer be the case, and a browser pixel may span multiple display dots. -
Similarly,
clientWidth
andclientHeight
give you the size of the space inside the element, ignoring border width. -
<p style="border: 3px solid red"> I'm boxed in </p> <script> let para = document.body.getElementsByTagName("p")[0]; console.log("clientHeight:", para.clientHeight); console.log("offsetHeight:", para.offsetHeight); </script>
-
The most effective way to find the precise position of an element on the screen is the
getBoundingClientRect
method. It returns an object withtop
,bottom
,left
, andright
properties, indicating the pixel positions of the sides of the element relative to the top left of the screen. If you want them relative to the whole document, you must add the current scroll position, which you can find in thepageXOffset
andpageYOffset
bindings. -
Laying out a document can be quite a lot of work. In the interest of speed, browser engines do not immediately re-layout a document every time you change it but wait as long as they can. When a JavaScript program that changed the document finishes running, the browser will have to compute a new layout to draw the changed document to the screen. When a program asks for the position or size of something by reading properties such as
offsetHeight
or callinggetBoundingClientRect
, providing correct information also requires computing a layout. -
A program that repeatedly alternates between reading DOM layout information and changing the DOM forces a lot of layout computations to happen and will consequently run very slowly.
-
<p><span id="one"></span></p> <p><span id="two"></span></p> <script> function time(name, action) { let start = Date.now(); action(); console.log(name, "took", Date.now() - start, "ms"); } time("naive", () => { let target = document.getElementById("one"); while (target.offsetWidth < 2000) { target.appendChild(document.createTextNode("X")); } }); time("clever", function() { let target = document.getElementById("two"); target.appendChild(document.createTextNode("XXXXX")); let total = Math.ceil(2000 / (target.offsetWidth / 5)); target.firstChild.nodeValue = "X".repeat(total); }) </script>
Styling
-
The way an
<img>
tag shows an image or an<a>
tag causes a link to be followed when it is clicked is strongly tied to the element type. But we can change the styling associated with an element, such as the text color or underline. Here is an example that uses thestyle
property: -
<p><a href=".">Normal link</a></p> <p><a href="." style="color: green">Green link</a></p>
-
A style attribute may contain one or more declarations, which are a property (such as
color
) followed by a colon and a value (such asgreen
). When there is more than one declaration, they must be separated by semicolons, as in"color: red; border: none"
. -
A lot of aspects of the document can be influenced by styling. For example, the
display
property controls whether an element is displayed as a block or an inline element. -
This text is displayed <strong>inline</strong>, <strong style="display: block">as a block</strong>, and <strong style="display: none">not at all</strong>.
-
The
block
tag will end up on its own line since block elements are not displayed inline with the text around them. The last tag is not displayed at all—display: none
prevents an element from showing up on the screen. This is a way to hide elements. It is often preferable to removing them from the document entirely because it makes it easy to reveal them again later. -
JavaScript code can directly manipulate the style of an element through the element’s
style
property. This property holds an object that has properties for all possible style properties. The values of these properties are strings, which we can write to in order to change a particular aspect of the element’s style. -
<p id="para" style="color: purple"> Nice text </p> <script> let para = document.getElementById("para"); console.log(para.style.color); para.style.color = "magenta"; </script>
-
Some style property names contain hyphens, such as
font-family
. Because such property names are awkward to work with in JavaScript (you’d have to saystyle["font-family"]
), the property names in thestyle
object for such properties have their hyphens removed and the letters after them capitalized (style.fontFamily
).
Cascading styles
- The styling system for HTML is called CSS, for Cascading Style Sheets. A style sheet is a set of rules for how to style elements in a document. It can be given inside a
<style>
tag. -
<style> stront { font-style: italic; color: gray; } </style> <p>Now <strong>strong text</strong> is italic and gray.</p>
- The cascading in the name refers to the fact that multiple such rules are combined to produce the final style for an element. In the example, the default styling for
<strong>
tags, which gives themfont-weight: bold
, is overlaid by the rule in the<style>
tag, which addsfont-style
andcolor
. - When multiple rules define a value for the same property, the most recently read rule gets a higher precedence and wins. So if the rule in the
<style>
tag includedfont-weight: normal
, contradicting the defaultfont-weight
rule, the text would be normal, not bold. Styles in astyle
attribute applied directly to the node have the highest precedence and always win. - It is possible to target things other than tag names in CSS rules. A rule for
.abc
applies to all elements with"abc"
in theirclass
attribute. A rule for#xyz
applies to the element with anid
attribute of"xyz"
(which should be unique within the document). -
<style> .subtle { color: gray; font-size: 80%; } #header { background: blue; color: white; } /* p elements with id main and with classes a and b */ p#main.a.b { margin-bottom: 20px; } </style>
- The precedence rule favoring the most recently defined rule applies only when the rules have the same specificity. A rule’s specificity is a measure of how precisely it describes matching elements, determined by the number and kind (tag, class, or ID) of element aspects it requires. For example, a rule that targets
p.a
is more specific than rules that targetp
or just.a
and would thus take precedence over them. - The notation
p > a {…}
applies the given styles to all<a>
tags that are direct children of<p>
tags. Similarly,p a {…}
applies to all<a>
tags inside<p>
tags, whether they are direct or indirect children.
Query selectors
-
... Understanding style sheets is helpful when programming in the browser, but they are complicated enough to warrant a separate book.
-
The main reason I introduced selector syntax—the notation used in style sheets to determine which elements a set of styles apply to—is that we can use this same mini-language as an effective way to find DOM elements.
-
The
querySelectorAll
method, which is defined both on thedocument
object and on element nodes, takes a selector string and returns aNodeList
containing all the elements that it matches. -
<p>And if you go chasing <span class="animal">rabbits</span></p> <p>And you know you're going to fall</p> <p>Tell 'em a <span class="character">hookah smoking <span class="animal">caterpillar</span></span></p> <p>Has given you the call</p> <script> function count(selector) { return document.querySelectorAll(selector).length; } console.log(count("p")); // 4 console.log(count(".animal")); // 2 console.log(count("p .animal")); // 2 console.log(count("p > .animal")); // 1 </script>
-
Unlike methods such as
getElementsByTagName
, the object returned byquerySelectorAll
is not live. It won’t change when you change the document. It is still not a real array, though, so you still need to callArray.from
if you want to treat it like one. -
The
querySelector
method (without theAll
part) works in a similar way. This one is useful if you want a specific, single element. It will return only the first matching element or null when no element matches.
Positioning and animating
- The
position
style property influences layout in a powerful way. By default it has a value ofstatic
, meaning the element sits in its normal place in the document. When it is set torelative
, the element still takes up space in the document, but now thetop
andleft
style properties can be used to move it relative to that normal place. Whenposition
is set toabsolute
, the element is removed from the normal document flow—that is, it no longer takes up space and may overlap with other elements. Also, itstop
andleft
properties can be used to absolutely position it relative to the top-left corner of the nearest enclosing element whoseposition
property isn’tstatic
, or relative to the document if no such enclosing element exists. - We can use this to create an animation.
-
<p style="text-align: center"> <img src="img/cat.png" style="position: relative"> </p> <script> let cat = document.querySelector("img"); let angle = Math.PI / 2; function animate(time, lastTime) { if (lastTime != null) { angle += (time - lastTime) * 0.001; } cat.style.top = (Math.sin(angle) * 20) + "px"; cat.style.left = (Math.cos(angle) * 200) + "px"; requestAnimationFrame(newTime => animate(newTime, time)); } requestAnimationFrame(animate); </script>
- Our picture is centered on the page and given a
position
ofrelative
. We’ll repeatedly update that picture’stop
andleft
styles to move it. - The script uses
requestAnimationFrame
to schedule theanimate
function to run whenever the browser is ready to repaint the screen. The animate function itself again callsrequestAnimationFrame
to schedule the next update. When the browser window (or tab) is active, this will cause updates to happen at a rate of about 60 per second, which tends to produce a good-looking animation. - If we just updated the DOM in a loop, the page would freeze, and nothing would show up on the screen. Browsers do not update their display while a JavaScript program is running, nor do they allow any interaction with the page. This is why we need
requestAnimationFrame
—it lets the browser know that we are done for now, and it can go ahead and do the things that browsers do, such as updating the screen and responding to user actions. - The animation function is passed the current time as an argument. To ensure that the motion of the cat per millisecond is stable, it bases the speed at which the angle changes on the difference between the current time and the last time the function ran. If it just moved the angle by a fixed amount per step, the motion would stutter if, for example, another heavy task running on the same computer were to prevent the function from running for a fraction of a second.
-
y축이 뒤집힌 것으로 추측
- Note that styles usually need units. In this case, we have to append
"px"
to the number to tell the browser that we are counting in pixels (as opposed to centimeters, “ems”, or other units). This is easy to forget. Using numbers without units will result in your style being ignored—unless the number is 0, which always means the same thing, regardless of its unit.
Exercises
Build a table
-
<table> <tr> <th>name</th> <th>height</th> <th>place</th> </tr> <tr> <td>Kilimanjaro</td> <td>5895</td> <td>Tanzania</td> </tr> </table>
-
For each row, the
<table>
tag contains a<tr>
tag. Inside of these<tr>
tags, we can put cell elements: either heading cells (<th>
) or regular cells (<td>
). -
function createTable(objects) { function appendRow(type, values) { let row = document.createElement("tr"); for (let value of values) { let tableData = document.createElement(type); tableData.appendChild(document.createTextNode(value)); row.appendChild(tableData); if (!Number.isNaN(Number(value))) { tableData.style.textAlign = right; } } table.appendChild(row); } let table = document.createElement("table"); let headers = Object.keys(objects[0]); appendRow("th", headers); for (let object of objects) { let values = []; for (let header of headers) { values.push(object[header]); } appendRow("td", values); } return table; } document.getElementById("mountains") .appendChild(createTable(MOUNTAINS));
-
// solution function buildTable(data) { let table = document.createElement("table"); let fields = Object.keys(data[0]); let headRow = document.createElement("tr"); fields.forEach(function(field) { let headCell = document.createElement("th"); headCell.appendChild(document.createTextNode(field)); headRow.appendChild(headCell); }); table.appendChild(headRow); data.forEach(function(object) { let row = document.createElement("tr"); fields.forEach(function(field) { let cell = document.createElement("td"); cell.appendChild(document.createTextNode(object[field])); if (typeof object[field] == "number") { cell.style.textAlign = "right"; } row.appendChild(cell); }); table.appendChild(row); }); return table; } document.querySelector("#mountains") .appendChild(buildTable(MOUNTAINS));
Elements by tag name
-
To find the tag name of an element, use its
nodeName
property. But note that this will return the tag name in all uppercase. -
function byTagName(node, tagName) { let elements = []; if (node.nodeType !== Node.ELEMENT_NODE) { return elements; } for (let child of Array.from(node.childNodes)) { if (child.nodeName === tagName.toUpperCase()) { elements.push(child); } elements = elements.concat(byTagName(child, tagName)); } return elements; }
-
// solution function byTagName(node, tagName) { let found = []; tagName = tagName.toUpperCase(); function explore(node) { for (let i = 0; i < node.childNodes.length; i++) { let child = node.childNodes[i]; if (child.nodeType == document.ELEMENT_NODE) { if (child.nodeName == tagName) found.push(child); explore(child); } } } explore(node); return found; }
'Programming > JavaScript' 카테고리의 다른 글
[EloquentJS] Ch16. Project: A Platform Game (0) | 2020.05.21 |
---|---|
[EloquentJS] Ch15. Handling Events (0) | 2020.04.26 |
[EloquentJS] Ch13. JavaScript and the Browser (0) | 2020.04.22 |
[EloquentJS] Ch12. Project: A Programming Language (0) | 2020.04.21 |
[EloquentJS] Ch11. Asynchronous Programming (0) | 2020.04.20 |