Please note that all the code examples for this chapter are available as a part of this chapter's code download on the book's website at www.wrox.com/go/projavascript4e
on the Download Code tab.
Even though the DOM is a fairly well-defined API, it is also frequently augmented with both standards-based and proprietary extensions to provide additional functionality. Prior to 2008, almost all of the DOM extensions found in browsers were proprietary. After that point, the W3C went to work to codify some of the proprietary extensions that had become de facto standards into formal specifications.
The two primary standards specifying DOM extensions are the Selectors API and HTML5. These both arose out of needs in the development community and a desire to standardize certain approaches and APIs. There is also a smaller Element Traversal specification with additional DOM properties. Proprietary extensions still exist, even though these two specifications, especially HTML5, cover a large number of DOM extensions. The proprietary extensions are also covered in this chapter.
All content in this chapter is supported by all major browsers meaning all vendor releases that have meaningful web traffic—unless otherwise stated.
One of the most popular capabilities of JavaScript libraries is the ability to retrieve a number of DOM elements matching a pattern specified using CSS selectors. Indeed, the library jQuery (www.jquery.com
) is built completely around the CSS selector queries of a DOM document in order to retrieve references to elements instead of using getElementById()
and getElementsByTagName()
.
The Selectors API (www.w3.org/TR/selectors-api
) was started by the W3C to specify native support for CSS queries in browsers. All JavaScript libraries implementing this feature had to do so by writing a rudimentary CSS parser and then using existing DOM methods to navigate the document and identify matching nodes. Although library developers worked tirelessly to speed up the performance of such processing, there was only so much that could be done while the code ran in JavaScript. By making this a native API, the parsing and tree navigating can be done at the browser level in a compiled language and thus tremendously increase the performance of such functionality.
At the core of Selectors API Level 1 are two methods: querySelector()
and querySelectorAll()
. On a conforming browser, these methods are available on the Document
type and on the Element
type.
The Selectors API Level 2 specification (https://www.w3.org/TR/selectors-api2/
) introduces more methods: matches()
, find()
, and findAll()
on the Element
type, although no browsers currently have or have stated an intention to support find()
, or findAll()
.
The querySelector()
method accepts a CSS query and returns the first descendant element that matches the pattern or null
if there is no matching element. Here is an example:
// Get the body element
let body = document.querySelector("body");
// Get the element with the ID "myDiv"
let myDiv = document.querySelector("#myDiv");
// Get first element with a class of "selected"
let selected = document.querySelector(".selected");
// Get first image with class of "button"
let img = document.body.querySelector("img.button");
When the querySelector()
method is used on the Document
type, it starts trying to match the pattern from the document element; when used on an Element
type, the query attempts to make a match from the descendants of the element only.
The CSS query may be as complex or as simple as necessary. If there's a syntax error or an unsupported selector in the query, then querySelector()
throws an error.
The querySelectorAll()
method accepts the same single argument as querySelector()
—the CSS query—but returns all matching nodes instead of just one. This method returns a static instance of NodeList
.
To clarify, the return value is actually a NodeList
with all of the expected properties and methods, but its underlying implementation acts as a snapshot of elements rather than a dynamic query that is constantly reexecuted against a document. This implementation eliminates most of the performance overhead associated with the use of NodeList
objects.
Any call to querySelectorAll()
with a valid CSS query will return a NodeList
object regardless of the number of matching elements; if there are no matches, the NodeList
is empty.
As with querySelector()
, the querySelectorAll()
method is available on the Document
, DocumentFragment
, and Element
types. Here are some examples:
// Get all <em> elements in a <div> (similar to getElementsByTagName("em"))
let ems = document.getElementById("myDiv").querySelectorAll("em");
// Get all elements wthat have "selected" as a class
let selecteds = document.querySelectorAll(".selected");
// Get all <strong> elements inside of <p> elements
let strongs = document.querySelectorAll("p strong");
The resulting NodeList
object may be iterated over using iteration hooks, item()
, or bracket notation to retrieve individual elements. Here's an example:
let strongElements = document.querySelectorAll("p strong");
// All three of the following loops will have the same effect:
for (let strong of strongElements) {
strong.className = "important";
}
for (let i = 0; i < strongElements.length; ++i) {
strongElements.item(i).className = "important";
}
for (let i = 0; i < strongElements.length; ++i) {
strongElements [i].className = "important";
}
As with querySelector()
, querySelectorAll()
throws an error when the CSS selector is not supported by the browser or if there's a syntax error in the selector.
matches()
was formerly referred to as matchesSelector()
in the specification draft. This method accepts a single argument, a CSS selector, and returns true
if the given element matches the selector or false
if not. For example:
if (document.body.matches ("body.page1")){
// true
}
This method allows you to easily check if an element would be returned by querySelector()
or querySelectorAll()
when you already have the element reference.
All major browsers support some form of matches()
. Edge, Chrome, Firefox, Safari, and Opera fully support it; IE 9–11 and minor mobile browsers support it with vendor prefixes.
Prior to version 9, Internet Explorer did not return text nodes for white space in between elements while all of the other browsers did. This led to differences in behavior when using properties such as childNodes
and firstChild
. In an effort to equalize the differences while still remaining true to the DOM specification, a new group of properties was defined in the Element Traversal (www.w3.org/TR/ElementTraversal/
).
The Element Traversal API adds five new properties to DOM elements:
childElementCount
—Returns the number of child elements (excludes text nodes and comments).firstElementChild
—Points to the first child that is an element. Element-only version offirstChild
.lastElementChild
—Points to the last child that is an element. Element-only version oflastChild
.previousElementSibling
—Points to the previous sibling that is an element. Element-only version ofpreviousSibling
.nextElementSibling
—Points to the next sibling that is an element. Element-only version of nextSibling
.Supporting browsers add these properties to all DOM elements to allow for easier traversal of DOM elements without the need to worry about white space text nodes.
For example, iterating over all child elements of a particular element in a traditional cross-browser way looks like this:
let parentElement = document.getElementById('parent');
let currentChildNode = parentElement.firstChild;
// For zero children, firstChild returns null and the loop is skipped
while (currentChildNode) {
if (currentChildNode.nodeType === 1) {
// If this is an ELEMENT_NODE, do whatever work is needed in here
processChild(currentChildNode);
}
if (currentChildNode === parentElement.lastChild) {
break;
}
currentChildNode = currentChildNode.nextSibling;
}
Using the Element Traversal properties allows a simplification of the code:
let parentElement = document.getElementById('parent');
let currentChildElement = parentElement.firstElementChild;
// For zero children, firstElementChild returns null and the loop is skipped
while (currentChildElement) {
// You already know this is an ELEMENT_NODE, do whatever work is needed here
processChild(currentChildElement);
if (currentChildElement === parentElement.lastElementChild) {
break;
}
currentChildElement = currentChildElement.nextElementSibling;
}
Element Traversal is implemented in Internet Explorer 9+ and all modern browsers.
HTML5 represents a radical departure from the tradition of HTML. In all previous HTML specifications, the descriptions stopped short of describing any JavaScript interfaces, instead focusing purely on the markup of the language and deferring JavaScript bindings to the DOM specification.
The HTML5 specification, on the other hand, contains a large amount of JavaScript APIs designed for use with the markup additions. Part of these APIs overlap with the DOM and define DOM extensions that browsers should provide.
One of the major changes in web development since the time HTML4 was adopted is the increased usage of the class
attribute to indicate both stylistic and semantic information about elements. This caused a lot of JavaScript interaction with CSS classes, including the dynamic changing of classes and querying the document to find elements with a given class or set of classes. To adapt to developers and their newfound appreciation of the class
attribute, HTML5 introduces a number of changes to make CSS class usage easier.
One of HTML5's most popular additions is getElementsByClassName()
, which is available on the document
object and on all HTML elements. This method evolved out of JavaScript libraries that implemented it using existing DOM features and is provided as a native implementation for performance reasons.
The getElementsByClassName()
method accepts a single argument, which is a string containing one or more class names, and returns a NodeList
containing all elements that have all of the specified classes applied. If multiple class names are specified, then the order is considered unimportant. Here are some examples:
// Get all elements with a class containing "username" and "current"
// It does not matter if one is declared before the other
let allCurrentUsernames = document.getElementsByClassName("username current");
// Get all elements with a class of "selected" that exist in myDiv's subtree
let selected = document.getElementById("myDiv").getElementsByClassName("selected");
When this method is called, it will return only elements in the subtree of the root from which it was called. Calling getElementsByClassName()
on document
always returns all elements with matching class names, whereas calling it on an element will return only descendant elements.
This method is useful for attaching events to classes of elements rather than using IDs or tag names. Keep in mind that since the returned value is a NodeList
, there are the same performance issues as when you're using getElementsByTagName()
and other DOM methods that return NodeList
objects.
The getElementsByClassName()
method is implemented in Internet Explorer 9+ and all modern browsers.
In class name manipulation, the className
property is used to add, remove, and replace class names. Because className
contains a single string, it's necessary to set its value every time a change needs to take place, even if there are parts of the string that should be unaffected. For example, consider the following HTML code:
<div class="bd user disabled">…</div>
This <div>
element has three classes assigned. To remove one of these classes, you need to split the class
attribute into individual classes, remove the unwanted class, and then create a string containing the remaining classes. Here is an example:
// Remove the "user" class
let targetClass = "user";
// First, get list of class names
let classNames = div.className.split(/s+/);
// Find the class name to remove
let idx = classNames.indexOf(targetClass);
// Remove the class name if found
if (idx> -1) {
classNames.splice(i,1);
}
// Set back the class name
div.className = classNames.join(" ");
All of this code is necessary to remove the "user"
class from the <div>
element's class
attribute. A similar algorithm must be used for replacing class names and detecting if a class name is applied to an element. Adding class names can be done by using string concatenation, but checks must be done to ensure that you're not applying the same class more than one time. Many JavaScript libraries implement methods to aid in these behaviors.
HTML5 introduces a way to manipulate class names in a much simpler and safer manner through the addition of the classList
property for all elements. The classList
property is an instance of a new type of collection named DOMTokenList
. As with other DOM collections, DOMTokenList
has a length
property to indicate how many items it contains, and individual items may be retrieved via the item()
method or using bracket notation. It also has the following additional methods:
add(
value
)
—Adds the given string value to the list. If the value already exists, it will not be added.contains(
value
)
—Indicates if the given value exists in the list (true
if so; false
if not).remove(
value
)
—Removes the given string value from the list.toggle(value)
—If the value already exists in the list, it is removed. If the value doesn't exist, then it's added.The entire block of code in the previous example can quite simply be replaced with the following:
div.classList.remove("user");
Using this code ensures that the rest of the class names will be unaffected by the change. The other methods also greatly reduce the complexity of the basic operations, as shown in these examples:
// Remove the "disabled" class
div.classList.remove("disabled");
// Add the "current" class
div.classList.add("current");
// Toggle the "user" class
div.classList.toggle("user");
// Figure out what's on the element now
if (div.classList.contains("bd") && !div.classList.contains("disabled")){
// Do stuff
)
// Iterate over the class names
for (let class of div.classList){
doStuff(class);
}
The addition of the classList
property makes it unnecessary to access the className
property unless you intend to completely remove or completely overwrite the element's class
attribute. The classList
property is implemented partially in Internet Explorer 10+ and fully in all other major browsers.
HTML5 adds functionality to aid with focus management in the DOM. The first is document.activeElement
, which always contains a pointer to the DOM element that currently has focus. An element can receive focus automatically as the page is loading, via user input (typically using the Tab key), or programmatically using the focus()
method. For example:
let button = document.getElementById("myButton");
button.focus();
console.log(document.activeElement === button); // true
By default, document.activeElement
is set to document.body
when the document is first loaded. Before the document is fully loaded, document.activeElement
is null
.
The second addition is document.hasFocus()
, which returns a Boolean value indicating if the document has focus:
let button = document.getElementById("myButton");
button.focus();
console.log(document.hasFocus()); // true
Determining if the document has focus allows you to determine if the user is interacting with the page.
This combination of being able to query the document to determine which element has focus and being able to ask the document if it has focus is of the utmost importance for web application accessibility. One of the key components of accessible web applications is proper focus management, and being able to determine which elements currently have focus is a major improvement over the guesswork of the past.
HTML5 extends the HTMLDocument
type to include more functionality. As with other DOM extensions specified in HTML5, the changes are based on proprietary extensions that are well-supported across browsers. As such, even though the standardization of the extensions is relatively new, some browsers have supported the functionality for a while.
Internet Explorer 4 was the first to introduce a readyState
property on the document
object, and it is supported by all modern browsers. Other browsers then followed suit and this property was eventually formalized in HTML5. The readyState
property for document
has two possible values:
loading
—The document is loading.complete
—The document is completely loaded.The best way to use the document.readyState
property is as an indicator that the document has loaded. Before this property was widely available, you would need to add an onload
event handler to set a flag indicating that the document was loaded. Basic usage:
if (document.readyState == "complete"){
// Do stuff
}
With the introduction of Internet Explorer 6 and the ability to render a document in either standards or quirks mode, it became necessary to determine in which mode the browser was rendering the page. Internet Explorer added a property on the document
named compatMode
whose sole job is to indicate what rendering mode the browser is in. As shown in the following example, when in standards mode, document.compatMode
is equal to "CSS1Compat"
; when in quirks mode, document.compatMode
is "BackCompat"
:
if (document.compatMode == "CSS1Compat"){
console.log("Standards mode");
} else {
console.log("Quirks mode");
}
The compatMode
property was added to HTML5 to formalize its implementation.
HTML5 introduces document.head
to point to the <head>
element of a document to complement document.body
, which points to the <body>
element of the document. You can retrieve a reference to the <head>
element using this property:
let head = document.head;
HTML5 describes several new properties dealing with the character set of the document. The characterSet
property indicates the actual character set being used by the document and can also be used to specify a new character set. By default, this value is "UTF-16"
, although it may be changed by using <meta>
elements or response headers or through setting the characterSet
property directly. Here's an example:
console.log(document.characterSet); // "UTF-16"
document.characterSet = "UTF-8";
HTML5 allows elements to be specified with nonstandard attributes prefixed with data-
in order to provide information that isn't necessary to the rendering or semantic value of the element. These attributes can be added as desired and named anything, provided that the name begins with data-
. Here is an example:
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
When a custom data attribute is defined, it can be accessed via the dataset
property of the element. The dataset
property contains an instance of DOMStringMap
that is a mapping of name-value pairs. Each attribute of the format data-name
is represented by a property with a name equivalent to the attribute but without the data-
prefix (for example, attribute data-myname
is represented by a property called myname
). The following is an example of how to use custom data attributes:
// Methods used in this example are for illustrative purposes only
let div = document.getElementById("myDiv");
// Get the values
let appId = div.dataset.appId;
let myName = div.dataset.myname;
// Set the value
div.dataset.appId = 23456;
div.dataset.myname = "Michael";
// Is there a "myname" value?
if (div.dataset.myname){
console.log('Hello, ${div.dataset.myname}');
}
Custom data attributes are useful when nonvisual data needs to be tied to an element for some other form of processing. This is a common technique to use for link tracking and mashups in order to better identify parts of a page. It is also extensively utilized in numerous single-page application frameworks.
Although the DOM provides fine-grained control over nodes in a document, it can be cumbersome when attempting to inject a large amount of new HTML into the document. Instead of creating a series of DOM nodes and connecting them in the correct order, it's much easier (and faster) to use one of the markup insertion capabilities to inject a string of HTML. The following DOM extensions have been standardized in HTML5 for this purpose.
When used in read mode, innerHTML
returns the HTML representing all of the child nodes, including elements, comments, and text nodes. When used in write mode, innerHTML
completely replaces all of the child nodes in the element with a new DOM subtree based on the specified value. Consider the following HTML code:
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
For the <div>
element in this example, the innerHTML
property returns the following string:
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
The exact text returned from innerHTML
differs from browser to browser. Internet Explorer and Opera tend to convert all tags to uppercase, whereas Safari, Chrome, and Firefox return HTML in the way it is specified in the document, including white space and indentation. You cannot depend on the returned value of innerHTML
being exactly the same from browser to browser.
When used in write mode, innerHTML
parses the given string into a DOM subtree and replaces all of the existing child nodes with it. Because the string is considered to be HTML, all tags are converted into elements in the standard way that the browser handles HTML (again, this differs from browser to browser). Setting simple text without any HTML tags, as shown here, sets the plain text:
div.innerHTML = "Hello world!";
Setting innerHTML
to a string containing HTML behaves quite differently as innerHTML
parses them. Consider the following example:
div.innerHTML = "Hello & welcome, <b>"reader"!</b>";
The result of this operation is as follows:
<div id="content">Hello & welcome, <b>"reader"!</b></div>
After setting innerHTML
, you can access the newly created nodes as you would any other nodes in the document.
<script>
elements cannot be executed when inserted via innerHTML
in all modern browsers. Internet Explorer 8 and earlier, is the only browser that allows this but only as long as the defer
attribute is specified and the <script>
element is preceded by what Microsoft calls a scoped element. The <script>
element is considered a NoScope element, which effectively means that it has no visual representation on the page, like a <style>
element or a comment. Internet Explorer strips out all NoScope elements from the beginning of strings inserted via innerHTML
, which means the following won't work:
// Won't work
div.innerHTML = "<script defer>console.log('hi');</script>";
In this case, the innerHTML
string begins with a NoScope element, so the entire string becomes empty. To allow this script to work appropriately, you must precede it with a scoped element, such as a text node or an element without a closing tag such as <input>
. The following lines will all work:
// All these will work
div.innerHTML = "_<script defer>console.log('hi');</script>";
div.innerHTML = "<div> </div><script defer>console.log('hi');</script>";
div.innerHTML = "<input type="hidden"><script defer>console.log('hi');</script>";
The first line results in a text node being inserted immediately before the <script>
element. You may need to remove this after the fact so as not to disrupt the flow of the page. The second line has a similar approach, using a <div>
element with a nonbreaking space. An empty <div>
alone won't do the trick; it must contain some content that will force a text node to be created. Once again, the first node may need to be removed to avoid layout issues. The third line uses a hidden <input>
field to accomplish the same thing. Because it doesn't affect the layout of the page, this may be the optimal case for most situations.
In most browsers, the <style>
element causes similar problems with innerHTML
. Most browsers support the insertion of <style>
elements using innerHTML
in the exact way you'd expect, as shown here:
div.innerHTML = "<style type="text/css">body {background-color: red; }</style>";
In Internet Explorer 8 and earlier, <style>
is yet another NoScope element, so it must be preceded by a scoped element such as this:
div.innerHTML = "_<style type="text/css">body {background-color: red; }</style>";
div.removeChild(div.firstChild);
When outerHTML
is called in read mode, it returns the HTML of the element on which it is called, as well as its child nodes. When called in write mode, outerHTML
replaces the node on which it is called with the DOM subtree created from parsing the given HTML string. Consider the following HTML code:
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
When outerHTML
is called on the <div>
in this example, the same code is returned, including the code for the <div>
. Note that there may be differences based on how the browser parses and interprets the HTML code. (These are the same types of differences you'll notice when using innerHTML
.)
Use outerHTML
to set a value in the following manner:
div.outerHTML = "<p>This is a paragraph.</p>";
This code performs the same operation as the following DOM code:
let p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);
The new <p>
element replaces the original <div>
element in the DOM tree.
The last addition for markup insertion is the insertAdjacentHTML()
and insertAdjacentText()
methods. These methods also originated in Internet Explorer and accept two arguments: the position in which to insert and the HTML or text to insert. The first argument must be one of the following values:
"beforebegin"
—Insert just before the element as a previous sibling."afterbegin"
—Insert just inside of the element as a new child or series of children before the first child."beforeend"
—Insert just inside of the element as a new child or series of children after the last child."afterend"
—Insert just after the element as a next sibling.Note that each of these values is case insensitive. The second argument is parsed as an HTML string (the same as with innerHTML
/outerHTML
) or as a raw string (the same as with innerText
/outerText
) and in the case of HTML, will throw an error if the value cannot be properly parsed. Basic usage is as follows:
// Insert as previous sibling
element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>");
element.insertAdjacentText("beforebegin", "Hello world!");
// Insert as first child
element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>");
element.insertAdjacentText("afterbegin", "Hello world!");
// Insert as last child
element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>");
element.insertAdjacentText("beforeend", "Hello world!");
// Insert as next sibling
element.insertAdjacentHTML("afterend", "<p>Hello world!</p>"); element.insertAdjacentText("afterend", "Hello world!");
Replacing child nodes using the methods in this section may cause memory problems in browsers, especially Internet Explorer. The problem occurs when event handlers or other JavaScript objects are assigned to subtree elements that are removed. If an element has an event handler (or a JavaScript object as a property), and one of these properties is used in such a way that the element is removed from the document tree, the binding between the element and the event handler remains in memory. If this is repeated frequently, memory usage increases for the page. When using innerHTML
, outerHTML
, and insertAdjacentHTML()
, it's best to manually remove all event handlers and JavaScript object properties on elements that are going to be removed.
Using these properties does have an upside, especially when using innerHTML
. Generally speaking, inserting a large amount of new HTML is more efficient through innerHTML
than through multiple DOM operations to create nodes and assign relationships between them. This is because an HTML parser is created whenever a value is set to innerHTML
(or outerHTML
). This parser runs in browser-level code (often written in C++), which is must faster than JavaScript. That being said, the creation and destruction of the HTML parser does have some overhead, so it's best to limit the number of times you set innerHTML
or outerHTML
. For example, the following creates a number of list items using innerHTML
:
for (let value of values){
ul.innerHTML += '<li>${value}</li>'; // avoid!!
}
This code is inefficient because it sets innerHTML
once each time through the loop. Furthermore, this code is reading innerHTML
each time through the loop, meaning that innerHTML
is being accessed twice each time through the loop. It's best to build up the string separately and assign it using innerHTML
just once at the end, like this:
let itemsHtml = "";
for (let value of values){
itemsHtml += '<li>${value}</li>';
}
ul.innerHTML = itemsHtml;
This example is more efficient, limiting the use of innerHTML
to one assignment. Of course, if you wanted to condense it to a single line:
ul.innerHTML = values.map(value => '<li>${value}</li>').join('');
Although innerHTML
does not execute script tags that it creates, it still provides an extremely broad attack surface for malicious actors looking to compromise a web page because it so readily creates elements and executable attributes such as onclick
.
Anywhere you are interpolating user-provided information into the page, it is nearly always inadvisable to do so using innerHTML
. The headaches of preventing XSS vulnerabilities far outweigh any convenience benefits gained from using innerHTML
. Compartmentalize interpolated data, and don't hesitate to use libraries that escape interpolated data before inserting them into the page.
One of the issues not addressed by the DOM specification is how to scroll areas of a page. To fill this gap, browsers implemented several methods that control scrolling in different ways. Of the various proprietary methods, only scrollIntoView()
was selected for inclusion in HTML5.
The scrollIntoView()
method exists on all HTML elements and scrolls the browser window or container element so the element is visible in the viewport.
true
is supplied, it specifies alignToTop
: The window scrolls so that the top of the element is at the top of the viewport.false
is supplied, it specifies alignToTop
: The window scrolls so that the bottom of the element is at the top of the viewport.behavior
property, which specifies how the scroll should occur: auto, instant, or smooth (limited support outside of Firefox), and the block
property is the same as alignToTop
.// Ensures this element is visible
document.forms[0].scrollIntoView();
// These behave identically
document.forms[0].scrollIntoView(true);
document.forms[0].scrollIntoView({block: true});
// This attempts to scroll the element smoothly into view:
document.forms[0].scrollIntoView({behavior: 'smooth', block: true});
This method is most useful for getting the user's attention when something has happened on the page. Note that setting focus to an element also causes the browser to scroll the element into view so that the focus can properly be displayed.
Although all browser vendors understand the importance of adherence to standards, they all have a history of adding proprietary extensions to the DOM in order to fill perceived gaps in functionality. Though this may seem like a bad thing on the surface, proprietary extensions have given the web development community many important features that were later codified into standards such as HTML5.
There are still a large amount of DOM extensions that are proprietary in nature and haven't been incorporated into standards. This doesn't mean that they won't later be adopted as standards—just that at the time of this writing, they remain proprietary and adopted by only a subset of browsers.
The differences in how Internet Explorer prior to version 9 and other browsers interpret white space in text nodes led to the creation of the children
property. The children
property is an HTMLCollection
that contains only an element's child nodes that are also elements. Otherwise, the children
property is the same as childNodes
and may contain the same items when an element has only elements as children. The children
property is accessed as follows:
let childCount = element.children.length;
let firstChild = element.children[0];
It's often necessary to determine if a given node is a descendant of another. Internet Explorer first introduced the contains()
method as a way of providing this information without necessitating a walk up the DOM document tree. The contains()
method is called on the ancestor node from which the search should begin and accepts a single argument, which is the suspected descendant node. If the node exists as a descendant of the root node, the method returns true
; otherwise it returns false
. Here is an example:
console.log(document.documentElement.contains(document.body)); // true
This example tests to see if the <body>
element is a descendant of the <html>
element, which returns true
in all well-formed HTML pages.
There is another way of determining node relationships by using the DOM Level 3 compareDocumentPosition()
method. This method determines the relationship between two nodes and returns a bitmask indicating the relationship. The values for the bitmask are as shown in the following table.
MASK | RELATIONSHIP BETWEEN NODES |
0x1 | Disconnected (The passed-in node is not in the document.) |
0x2 | Precedes (The passed-in node appears in the DOM tree prior to the reference node.) |
0x4 | Follows (The passed-in node appears in the DOM tree after the reference node.) |
0x8 | Contains (The passed-in node is an ancestor of the reference node.) |
0x10 | Is contained by (The passed-in node is a descendant of the reference node.) |
To mimic the contains()
method, you will be interested in the 16 mask. The result of compareDocumentPosition()
can be bitwise ANDed to determine if the reference node contains the given node. Here is an example:
let result = document.documentElement.compareDocumentPosition(document.body);
console.log(!!(result & 0x10));
When this code is executed, result
becomes 20, or 0x14 (0x4 for “follows” plus 0x10 for “is contained by”). Applying a bitwise mask of 0x10 to the result returns a nonzero number, and the two NOT bang operators convert that value into a Boolean.
IE9+ and all modern browsers support both contains
and compareDocumentPosition.
While the innerHTML
and outerHTML
markup insertion properties were adopted by HTML5 from Internet Explorer, there are two others that were not. The two remaining properties that are left out of HTML5 are innerText
and outerText
.
The innerText
property works with all text content contained within an element, regardless of how deep in the subtree the text exists. When used to read the value, innerText
concatenates the values of all text nodes in the subtree in depth-first order. When used to write the value, innerText
removes all children of the element and inserts a text node containing the given value. Consider the following HTML code:
<div id="content">
<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
For the <div>
element in this example, the innerText
property returns the following string:
This is a paragraph with a list following it.
Item 1
Item 2
Item 3
Note that different browsers treat white space in different ways, so the formatting may or may not include the indentation in the original HTML code.
Using the innerText
property to set the contents of the <div>
element is as simple as this single line of code:
div.innerText = "Hello world!";
After executing this line of code, the HTML of the page is effectively changed to the following:
<div id="content">Hello world!</div>
Setting innerText
removes all of the child nodes that existed before, completely changing the DOM subtree. Additionally, setting innerText
encodes all HTML syntax characters (less-than, greater-than, quotation marks, and ampersands) that may appear in the text. Here is an example:
div.innerText = "Hello & welcome, <b>"reader"!</b>";
The result of this operation is as follows:
<div id="content">Hello & welcome, <b>"reader"!</b></div>
Setting innerText
can never result in anything other than a single text node as the child of the container, so the HTML-encoding of the text must take place in order to keep to that single text node. The innerText
property is also useful for stripping out HTML tags. By setting the innerText
equal to the innerText
, as shown here, all HTML tags are removed:
div.innerText = div.innerText;
Executing this code replaces the contents of the container with just the text that exists already.
The outerText
property works in the same way as innerText
except that it includes the node on which it's called. For reading text values, outerText
and innerText
essentially behave in the exact same way. In writing mode, however, outerText
behaves very differently. Instead of replacing just the child nodes of the element on which it's used, outerText
actually replaces the entire element, including its child nodes. Consider the following:
div.outerText = "Hello world!";
This single line of code is equivalent to the following two lines:
let text = document.createTextNode("Hello world!");
div.parentNode.replaceChild(text, div);
Essentially, the new text node completely replaces the element on which outerText
was set. After that point in time, the element is no longer in the document and cannot be accessed.
The outerText
property is nonstandard, and not on a standards track. It is not recommended that you rely on it for important behavior. It is supported in all modern browsers except for Firefox.
As mentioned previously, scrolling is one area where specifications didn't exist prior to HTML5. While scrollIntoView()
was standardized in HTML5, there are still several additional proprietary methods available in various browsers. scrollIntoViewIfNeeded
exists as an extension to the HTMLElement
type and therefore each is available on all elements. scrollIntoViewIfNeeded
(alignCenter
) scrolls the browser window or container element so that the element is visible in the viewport only if it's not already visible; if the element is already visible in the viewport, this method does nothing. The optional alignCenter
argument will attempt to place the element in the center of the viewport if set to true
. This is implemented in Safari, Chrome, and Opera.
Following is an example of how this may be used:
// Make sure this element is visible only if it's not already
document.images[0].scrollIntoViewIfNeeded();
Because scrollIntoView()
is the only method supported in all browsers, this is typically the only one used.
While the DOM specifies the core API for interacting with XML and HTML documents, there are several specifications that provide extensions to the standard DOM. Many of the extensions are based on proprietary extensions that later became de facto standards as other browsers began to mimic their functionality. The three specifications covered in this chapter are:
querySelector()
, querySelectorAll(),
and matches()
.innerHTML
, as well as additional functionality for dealing with focus management, character sets, scrolling, and more.The number of DOM extensions is currently small, but it's almost a certainty that the number will continue to grow as web technology continues to evolve. Browsers still experiment with proprietary extensions that, if successful, may end up as pseudo-standards or be incorporated into future versions' specifications.