Document Object Model
Preface: Browser environment, specs
Read more from source.
The JavaScript language was initially created for web browsers. Since then, it has evolved into a language with many uses and platforms.
A platform may be a browser, or a web-server or another host, or even a “smart” coffee machine if it can run JavaScript. Each of these provides platform-specific functionality. The JavaScript specification calls that a host environment.
A host environment provides its own objects and functions in addition to the language core. Web browsers give a means to control web pages. Node.js provides server-side features, and so on.
Browser environment
Here’s a bird’s-eye view of what we have when JavaScript runs in a web browser:
- window:
- DOM: document, ...
- BOM: navigator, screen, location, frames, history, XMLHttpRequest
- JavaScript: Object, Array, Function, ...
There’s a “root” object called window. It has two roles:
- First, it is a global object for JavaScript code.
- Second, it represents the “browser window” and provides methods to control it.
Specs
- DOM specification - Describes the document structure, manipulations, and events.
- CSSOM specification - Describes stylesheets and style rules, manipulations with them, and their binding to documents. The CSSOM is used together with the DOM when we modify style rules for the document. In practice though, the CSSOM is rarely required, because we rarely need to modify CSS rules from JavaScript (usually we just add/remove CSS classes, not modify their CSS rules), but that’s also possible.
- HTML specification - Describes the HTML language (e.g. tags) and also the BOM (browser object model) – various browser functions:
setTimeout,alert,locationand so on. It takes the DOM specification and extends it with many additional properties and methods. - Additionally, some classes are described separately at https://spec.whatwg.org/.
DOM Essentials
Document Object Model, or DOM for short, represents all page content as objects that can be modified.
Note
DOM is not only for browsers For instance, server-side scripts that download HTML pages and process them can also use the DOM. They may support only a part of the specification though.
DOM Tree
There are 12 node types. In practice we usually work with 4 of them:
documentobject - the main “entry point” to the page(= into DOM), represents the whole document.- element nodes – HTML-tags, the tree building blocks.
- text nodes – contain text(spaces and newlines too!).
- comment nodes – sometimes we can put information there, it won’t be shown, but JS can read it from the DOM.
node.nodeType - “old-fashioned” way to get the “type” of a DOM node, read only:
node.nodeType == 9for the 'document' objectnode.nodeType == 8for comment nodesnode.nodeType == 3for text nodesnode.nodeType == 1for element nodes
<body><!-- this is a comment -->
<script>
let elem = document.body;
// let's examine: what type of node is in elem?
console.log(elem.nodeType); // 1 => element
// its first child is...
console.log(elem.firstChild.nodeType); // 8 => comment
// and its second child is...
console.log(elem.childNodes[1].nodeType) // 3 => text(newline)
// for the document object, the type is 9
console.log(document.nodeType); // 9
</script>
</body>
In modern scripts, we can use instanceof and other class-based tests to see the node type.
node.nodeName - returns:
- for element nodes:
<tag>name(same aselement.tagNameproperty do) - for other node types (text, comment, etc.): string with the node type
<body><!-- comment -->
<script>
// for <body> element
console.log(document.body.nodeName); // BODY
// for comment
console.log(document.body.firstChild.nodeName); // #comment
// for document
console.log(document.nodeName); // #document
</script>
</body>
Walking the DOM
-
The topmost tree nodes are available directly as
documentproperties:<html>...</html>=document.documentElement<head>...</head>=document.head<body>...</body>=document.body
-
Given an any DOM node, we can go to its immediate neighbors using navigation properties.
There are two main sets of them:
- For all nodes:
parentNode,childNodes,firstChild(===childNodes[0]),lastChild(===childNodes[elem.childNodes.length - 1]),previousSibling,nextSibling. -
For element nodes only:
parentElement,children,firstElementChild,lastElementChild,previousElementSibling,nextElementSibling.Examples:
document.querySelectorAll.("li")[1].parentElement; //e.g. get 'ul' element document.querySelectorAll.("li")[1].parentElement.parentElement; //e.g. gets 'body' element document.querySelectorAll.("li")[1].parentElement.children; //e.g. get all the children elements of the 'body'
DOM Collections
childNodesandchildrenare collections. Collection – a special array-like iterable object and has two important consequences:-
we can use
for..ofto iterate over it:for (let node of document.body.childNodes) { alert(node); // shows all nodes from the collection }That’s because it’s iterable (provides the
Symbol.iteratorproperty, as required).Don’t use
for..into loop over collectionsThe
for..inloop iterates over all enumerable properties. And collections have some “extra” rarely used properties that we usually do not want to get:<body> <script> // shows 0, 1, length, item, values and more. for (let prop in document.body.childNodes) alert(prop); </script> </body> -
array methods won’t work, because it’s not an array:
alert(document.body.childNodes.filter); // undefined (there's no filter method!)BUT: We can create a “real” array from the collection, if we want array methods, i.e. to make a copy using
Array.fromto iterate over if adding, moving, or removing nodes.alert( Array.from(document.body.childNodes).filter ); // function
DOM collections are live
Almost all DOM collections with minor exceptions are live, i.e. they reflect the current state of DOM. If we keep a reference to
element.childNodes, and add/remove nodes into DOM, then they appear in the collection automatically.DOM collections and navigation properties are read-only
We can’t replace a child by something else by assigning
childNodes[i] = .... Changing DOM needs other methods, see below. - For all nodes:
-
Some types of DOM elements, provide additional navigation properties and collections to access their content, e.g.:
-
<table>...</table>table.rows– the collection of<tr>elements of the table.table.caption/tHead/tFoot– references to elements<caption>,<thead>,<tfoot>.table.tBodies– the collection of<tbody>elements (can be many according to the standard, but there will always be at least one – even if it is not in the source HTML, the browser will put it in the DOM).
<thead>,<tfoot>,<tbody>elements provide the rows property:tbody.rows– the collection of<tr>inside.
<tr>:tr.cells– the collection of<td>and<th>cells inside the given<tr>.tr.sectionRowIndex– the position (index) of the given<tr>inside the enclosing<thead>/<tbody>/<tfoot>.tr.rowIndex– the number of the<tr>in the table as a whole (including all table rows).
<td>and<th>:td.cellIndex– the number of the cell inside the enclosing<tr>.
An example of usage:
<table id="table"> <tr> <td>one</td><td>two</td> </tr> <tr> <td>three</td><td>four</td> </tr> </table> <script> // get td with "two" (first row, second column) let td = table.rows[0].cells[1]; td.style.backgroundColor = "red"; // highlight it </script> -
<form>...</form>
-
Searching the DOM
Using getElement(s)*
This methods used for older code bases.
-
document.getElementById
document.getElementById("first");
All methods getElementsBy* below return a live collection.
Such collections always reflect the current state of the document and “auto-update” when it changes.
-
element.getElementsByTagName
looks for elements with the given tag and returns the live collection of them the tag parameter can also be a star
"*"for “any tags”// outputs live collection of all h1 elements: document.getElementsByTagName("h1"); // outputs specific element: document.getElementsByTagName("h1")[0];<!-- Let’s find all input tags inside the table: --> <table id="table"> <tr> <td>Your age:</td> <td> <label> <input type="radio" name="age" value="young" checked> less than 18 </label> <label> <input type="radio" name="age" value="mature"> from 18 to 50 </label> <label> <input type="radio" name="age" value="senior"> more than 60 </label> </td> </tr> </table> <script> let inputs = table.getElementsByTagName('input'); for (let input of inputs) { alert( input.value + ': ' + input.checked ); } </script> -
element.getElementsByClassName
looks for elements with the given class and returns the live collection of them
// outputs live collection of all elements with class 'second': document.getElementsByClassName("second"); // outputs specific element: document.getElementsByClassName("second")[0]; -
document.getElementsByName
looks for elements with the given name and returns the live collection of them
Using querySelector*
This methods can select anything inside quotes exactly like selecting in CSS. They are more powerfull than the first three above.
-
element.querySelectorAll
returns all elements in the document that matches a specified CSS selector(s), as a static NodeList object(static collection), i.e. it doesn't reflect the current state of the document and doesn't “auto-update” when it changes can be iterated with
for...ofloop or withforEacharray methodCan use pseudo-classes as well
Pseudo-classes in the CSS selector like
:hoverand:activeare also supported. For instance,document.querySelectorAll(':hover')will return the collection with elements that the pointer is over now (in nesting order: from the outermost<html>to the most nested one).<!-- Here we look for all <li> elements that are last children: --> <ul> <li>The</li> <li>test</li> </ul> <ul> <li>has</li> <li>passed</li> </ul> <script> let elements = document.querySelectorAll('ul > li:last-child'); for (let elem of elements) { alert(elem.innerHTML); // "test", "passed" } </script>element.querySelectorAll “unexpected“ result
By default this method checks the last element without considering the context, e.g.:
<ul class="list"> <li class="item-list"> <ul class="sub-list"> <li class="item-sub-list"></li> <li class="item-sub-list"></li> </ul> </li> </ul> <script> const sublist = document.querySelectorAll('.sub-list') const sublistitems = sublist[0].querySelectorAll('.list .item-sub-list') console.log(sublistitems) // NodeList(2) [li.item-sub-list, li.item-sub-list] </script> <!-- we expect here to get nothing, but instead we've got collection of 2 nodes --> -
element.querySelector
returns the first element that matches a specified CSS selector(s) in the document, i.e. the result(only!) is the same as
element.querySelectorAll(css)[0], but the latter is looking for all elements and picking one, whileelement.querySelectorjust looks for one, so it’s faster and also shorter to writedocument.querySelector("li");
DOM selectors summary table
| Method | Searches by... | Retruns | Can call on an element? | Live collection? |
|---|---|---|---|---|
querySelector |
CSS-selector | One obect | ||
querySelectorAll |
CSS-selector | Collection of objects | ||
getElementById |
id |
One obect | - Searches the whole document by calling on document object |
|
getElementsByName |
name |
Collection of objects | - Searches the whole document by calling on document object |
|
getElementsByTagName |
tag or '*' |
Collection of objects | ||
getElementsByClassName |
class | Collection of objects |
It is important to CACHE selectors in variables.
This is in order to reduce memory usage by js engine(by going to DOM each time when we use selector), e.g: var h1 = document.querySelector("h1");
Additional useful methods
-
element.matches(css)
checks if element matches the given CSS-selector and returns
trueorfalse<a href="http://example.com/file.zip">...</a> <a href="http://ya.ru">...</a> <script> // can be any collection instead of document.body.children for (let elem of document.body.children) { if (elem.matches('a[href$="zip"]')) { alert("The archive reference: " + elem.href ); } } </script> -
element.closest(css)
looks for the nearest ancestor that matches the CSS-selector; returns
nullif finds nothing; the element itself is also included in the search<h1>Contents</h1> <div class="contents"> <ul class="book"> <li class="chapter">Chapter 1</li> <li class="chapter">Chapter 2</li> </ul> </div> <script> let chapter = document.querySelector('.chapter'); // LI alert(chapter.closest('.book')); // UL alert(chapter.closest('.contents')); // DIV alert(chapter.closest('h1')); // null (because h1 is not an ancestor) </script> -
elemA.contains(elemB)
checks for the child-parent relationship; returns true if
elemBis insideelemA(a descendant ofelemA) or whenelemA==elemB -
element.tagName
returns
<tag>nametagNameis only supported by element nodes (as it originates fromElementclass)<body><!-- comment --> <script> // for <body> element console.log(document.body.tagName); // BODY // for comment console.log(document.body.firstChild.tagName); // undefined (not an element node) // for document console.log(document.tagName); // undefined (not an element node) </script> </body>The tag name is always uppercase except in XML mode.
The browser has two modes of processing documents: HTML and XML. Usually the HTML-mode is used for webpages. XML-mode is enabled when the browser receives an XML-document with the header:
Content-Type: application/xml+xhtml.In HTML mode
tagName(andnodeName) is always uppercased: it’sBODYeither for<body>or<BoDy>.
Changing the DOM
Changing nodes content
-
element.innerHTML
// returns the HTML content(inner HTML) of an element as a string document.querySelector("h1").innerHTML;// sets the HTML content(inner HTML) of an element // DANGEROUS - it removes everything within the element(also other elements) document.querySelector("h1").innerHTML = "<strong>!!!!!!</strong>";element.innerHTML = "...";typing errors automatically fixed by browserFor example when we forgot to close the tag.
element.innerHTML = "...";don’t execute scriptsWhen inserting a
<script>tag into the document – it becomes a part of HTML, but doesn’t execute.//appends HTML to an element elem.innerHTML += "..."; // is a shorter way to write: elem.innerHTML = elem.innerHTML + "..." // In other words, 'innerHTML+=' does this: // 1. The old contents is removed. // 2. The new 'innerHTML' is written instead (a concatenation of the old and the new one). // DANGEROUS - becsause the old content is “zeroed-out” causing following side effects: // - all images and other resources will be reloaded // - if the existing text was selected with the mouse, then most browsers will remove the selection upon rewriting 'innerHTML' // - if there was an <input> with a text entered by the visitor, then the text will be removed // - and so on ... -
element.outerHTML
<!-- returns the full HTML of the element as a string, like 'innerHTML' plus the element itself --> <div id="elem">Hello <b>World</b></div> <script> alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div> </script><!-- seting the full HTML of the element --> <div>Hello, world!</div> <script> let elem = document.querySelector('div'); // replace elem.outerHTML with <p>...</p> elem.outerHTML = '<p>A new element</p>'; // (*) // Wow! 'elem' is still the same! alert(elem.outerHTML); // <div>Hello, world!</div> (**) </script>Regarding the codeblock above:Unlike
innerHTML, writing toouterHTMLdoes not change the element. Instead, it replaces it in the DOM.In the line
(*)we replacedelemwith<p>A new element</p>. In the outer document (the DOM and therefore the page content) we can see the new content instead of the<div>Hello, world!</div>. But, as we can see in line(**), the value of the oldelemvariable hasn’t changed!The
outerHTMLassignment does not modify the DOM element (the object referenced by, in this case, the variableelem), but removes it from the DOM and inserts the new HTML in its place.So what happened in
elem.outerHTML=...is:elemwas removed from the document- another piece of HTML
<p>A new element</p>was inserted in its place elemstill has its old value and the new HTML(<p>A new element</p>) wasn’t saved to any variable
Tip
It’s so easy to make an error here: modify
elem.outerHTMLand then continue to work withelemas if it had the new content in it. But it doesn’t.elem.outerHTML = '...'puts the new HTML in its place instead. We can get references to the new elements by querying the DOM. -
textNode/commentNode.data, textNode/commentNode.nodeValue
dataandnodeValueproperties are almost the same for practical use.There are only minor specification differences. So we’ll use
data, because it’s shorter.returns/modifies the content of a non-element node(text, comment):
<body> Hello <!-- Comment --> <script> let text = document.body.firstChild; console.log(text.data); // Hello text.data = 'Hello World!' console.log(text.data); // Hello World! let comment = text.nextSibling; console.log(comment.data); // Comment comment.data = 'New Comment'; console.log(comment.data); // New Comment </script> </body>Practical usgae of commentNode.data
Sometimes developers embed information or template instructions into HTML in them, like this:
<!-- if isAdmin --> <div>Welcome, Admin!</div> <!-- /if -->…Then JavaScript can read it from
dataproperty and process embedded instructions. -
element.textContent
provides access to the text inside the element: only text, minus all
<tags><!-- returning text content --> <div id="news"> <h1>Headline!</h1> <p>Martians attack people!</p> </div> <script> let news = document.querySelector('#news'); // Headline! Martians attack people! alert(news.textContent); </script><!-- modifying/writing to text content --> <!-- this div gets the name “as HTML”: all tags become tags, so we see the bold name --> <div id="elem1"></div> <!-- this div gets the name “as text”, so we literally see <b>Winnie-the-Pooh!</b> --> <div id="elem2"></div> <script> let name = prompt("What's your name?", "<b>Winnie-the-Pooh!</b>"); let elem1 = document.querySelector('#elem1') let elem2 = document.querySelector('#elem2') elem1.innerHTML = name; elem2.textContent = name; </script>Getting an input from a user.
In most cases, we expect the text from a user, and want to treat it as text. We don’t want unexpected HTML in our site. An assignment to
textContentdoes exactly that. -
element.hidden,
<tag hidden>...</tag>The “hidden” attribute and the DOM property specifies whether the element is visible or not. Technically, hidden works the same as
style="display:none". But it’s shorter to write.Example of blinking element:
<div id="elem">A blinking element</div> <script> let elem = document.querySelector('#elem'); setInterval(() => elem.hidden = !elem.hidden, 1000); </script>
Creation, insertion, removal of nodes
Creation
- document.createElement('tag') – creates an element with the given tag
- document.createTextNode('string value') – creates a text node(rarely used)
- element.cloneNode(deep) – clones the element without descendants when
deep=='', ifdeep==truethen with all descendants
Insertion and removal
- node.append(...nodes or 'strings') - insert into node, at the end
- node.prepend(...nodes or 'strings') - insert into node, at the beginning
- node.before(...nodes or 'strings') - insert right before node
- node.after(...nodes or 'strings') - insert right after node
- node.replaceWith(...nodes or 'strings') - replace node
- node.remove() - remove the node
The above insertion methods can only be used to insert DOM nodes or text pieces.
To insert an HTML string “as html”, with all tags and stuff working, in the same manner as element.innerHTML does it use following method:
-
Given some HTML in
html, element.insertAdjacentHTML("where", html) inserts it depending on the value of"where":- "beforebegin" – insert html right before elem
- "afterbegin" – insert html into elem, at the beginning
- "beforeend" – insert html into elem, at the end
- "afterend" – insert html right after elem
Also there are similar methods, element.insertAdjacentText("where", 'text') and element.insertAdjacentElement("where", element), that insert text strings and elements, but they are rarely used because there are mentioned above
append,prepend,beforeandaftermethods for this needs.
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
// 1. Create <div> element
let div = document.createElement('div');
// 2. Set its class to "alert"
div.className = "alert";
// 3. Fill it with the content
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";
document.body.append(div);
</script>
<!-- alternative variant using insertAdjacentHTML -->
<script>
document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
<strong>Hi there!</strong> You've read an important message.
</div>`);
</script>
<!-- inserting multiple nodes and text pieces in a single call -->
<div id="div"></div>
<script>
div.before('<p>Hello</p>', document.createElement('hr'));
</script>
All insertion methods automatically remove the node from the old place.
For instance, let’s swap elements:
<div id="first">First</div>
<div id="second">Second</div>
<script>
// no need to call remove
second.after(first); // take #second and after it insert #first
</script>
-
DocumentFragment - a special DOM node that serves as a wrapper to pass around lists of nodes. It is rarely used explicitly.
DocumentFragmentmentioned here mainly because there are some concepts on top of it, like<template></template>element. -
document.write(html) - append HTML to the page before it has finished loading
<p>Somewhere in the page...</p> <script> document.write('<b>Hello from JS</b>'); </script> <p>The end</p>The call to
document.writeonly works while the page is loading. So it’s kind of unusable at “after loaded” stage, unlike other DOM methods.Technically, when
document.writeis called while the browser is reading (“parsing”) incoming HTML, and it writes something, the browser consumes it just as if it were initially there, in the HTML text. So it works blazingly fast, because there’s no DOM modification involved. It writes directly into the page text, while the DOM is not yet built. So if we need to add a lot of text into HTML dynamically, and we’re at page loading phase, and the speed matters, it may help. But in practice these requirements rarely come together. And usually we can see this method in scripts just because they are old.After the page is loaded such a call erases the document.
Insertion and removal(“old school” methods)
- parent.appendChild(node) - appends
nodeas the last child ofparentElem -
parent.insertBefore(node, nextSibling) - inserts
nodebeforenextSiblingintoparentElem<ol id="list"> <li>0</li> <li>1</li> <li>2</li> </ol> <!-- insert a new list item before the second <li> --> <script> let newLi = document.createElement('li'); newLi.innerHTML = 'Hello, world!'; list.insertBefore(newLi, list.children[1]); </script> <!-- insert 'newLi' as the first element --> <script> list.insertBefore(newLi, list.firstChild); </script> -
parent.removeChild(node) - removes
nodefromparentElem(assumingnodeis its child) - parent.replaceChild(newElement, node) - replaces
oldChildwithnodeamong children ofparentElem
All these methods return the inserted/removed node.
But usually the returned value is not used, we just run the method.
Changing element properties: class, style
class
-
element.className – corresponds to the
classattribute; the string value, good to manage the whole set of classes// returns the class name(s) of an element as string document.querySelector("h1").className;// sets the class name of an element (i.e. removes existing class names if any and then adds the new one) document.querySelector("h1").className = "coolTitle"; -
element.classList– the object with methods
add/remove/toggle/contains, good for individual classes// return a list of classes of the element as iterable, document.querySelector("h1").classList; // so we can list all classes with 'for..of' loop for (let name of document.querySelector("h1").classList) { console.log(name); }// sets the class name of an element (i.e. removes existing class names if any and then adds the new one) document.querySelector("h1").classList = "coolTitle";Methods of classList:
- element.classList.add/remove("class") – adds/removes the class
- element.classList.toggle("class") – adds the class if it doesn’t exist, otherwise removes it
- element.classList.contains("class") – checks for the given class, returns
true/false
document.querySelector("h1").classList.add("done"); document.querySelector("h1").classList.remove("done"); document.querySelector("h1").classList.toggle("done");
style
-
element.style.[css-property] - corresponds to what’s written in the
styleattributeAll elements on the web page have a 'style' attribute
// return the whole bunch of CSS properties of the element(a long list): document.querySelector("h1").style// set background to yellow: document.querySelector("h1").style.backgroud = "yellow";<!-- the above is the exact thing as: --> <h1 style="background: yellow"></h1>// two ways to reset style property, e.g. given: document.body.style.background = 'red'; // 1. set the property to an empty string document.body.style.background = ""; // 2. element.style.removeProperty('style property') document.body.style.removeProperty('background');<!-- Don’t forget to add CSS units to values --> <body> <script> // doesn't work! document.body.style.margin = 20; alert(document.body.style.margin); // '' (empty string, the assignment is ignored) // now add the CSS unit (px) - and it works document.body.style.margin = '20px'; alert(document.body.style.margin); // 20px alert(document.body.style.marginTop); // 20px alert(document.body.style.marginLeft); // 20px </script> </body> <!-- Please note: the browser “unpacks” the property 'style.margin' in the last lines and infers 'style.marginLeft' and 'style.marginTop' from it. -->For multi-word property the camelCase is used (a dash
-means upper case):CSS property → styleobject propertybackground-color→ elem.style.backgroundColor z-index→ elem.style.zIndex border-left-width→ elem.style.borderLeftWidth -moz-border-radius→ element.style.MozBorderRadius -webkit-border-radius→ element.style.WebkitBorderRadius and so on...
How to apply other staff.
To see how to apply
importantand other rare stuff – there’s a list of methods at MDN.The
element.styleproperty operates only on the value of the"style"attribute, without any CSS cascade.To read anything that comes from CSS classes use
getComputedStyle(element, [pseudo]), see below.element.style is an object, and it’s read-only, so we can’t set the full style like
element.style="color: red; width: 100px". Insteadelement.style.cssTextbelow can be used. -
element.style.cssText - corresponds to the whole "style" attribute, the full string of styles
element.style.cssText = `css-property1: value1; css-property2: value2; ...` - full style rewrite
<div id="div">Button</div> <script> // we can set special style flags like "important" here div.style.cssText=`color: red !important; background-color: yellow; width: 100px; text-align: center; `; alert(div.style.cssText); </script>This property is rarely used.
Because such assignment removes all existing styles: it does not add, but replaces them. May occasionally delete something needed. But we can safely use it for new elements, when we know we won’t delete an existing style. The code above can be accomplished by setting an attribute:
div.setAttribute('style', 'color: red...') -
getComputedStyle(element, [pseudo]) - reads the resolved(= resolved value of the property, usually in
pxfor geometry) styles(with respect to all classes, after all CSS is applied and final values are calculated)[pseudo] - a pseudo-element if required, for instance
::before. An empty string or no argument means the element itself.Returns the
element.style-like object. Read-only.<head> <style> body { color: red; margin: 5px } </style> </head> <body> <script> let computedStyle = getComputedStyle(document.body); // now we can read the margin and the color from it alert( computedStyle.marginTop ); // 5px alert( computedStyle.color ); // rgb(255, 0, 0) </script> </body>getComputedStylerequires the full property nameWe should always ask for the exact property that we want, like
paddingLeftormarginToporborderTopWidth. Otherwise the correct result is not guaranteed.Styles applied to
:visitedlinks are hidden!Visited links may be colored using
:visitedCSS pseudoclass.But
getComputedStyledoes not give access to that color, because otherwise an arbitrary page could find out whether the user visited a link by creating it on the page and checking the styles.JavaScript may not see the styles applied by
:visited. And also, there’s a limitation in CSS that forbids applying geometry-changing styles in:visited. That’s to guarantee that there’s no side way for an evil page to test if a link was visited and hence to break the privacy.
styles: working tips
-
We should always prefer CSS classes to
element.style, because the latter breaks the separation of control concept by addingstyleattribute to selected element. Theelement.styleshould only be used if classes “can’t handle it”.For example,
element.styleis acceptable if we calculate coordinates of an element dynamically and want to set them from JavaScript, like this:let top = /* complex calculations */; let left = /*complex calculations*/; elem.style.left = left; // e.g '123px', calculated at run-time elem.style.top = top; // e.g '456px'For other cases, like making the text red, adding a background icon – describe that in CSS and then add the class (JavaScript can do that). That’s more flexible and easier to support.
-
Converting string property value to number using
parseInt(), in order to do math with it later.// get element const elem = document.querySelector('h1'); // element style const elemStyle = getComputedStyle(elem); console.log(elemStyle.paddingLeft); // e.g. 20px // get number const paddingLeft = parseInt(elemStyle.paddingLeft); console.log(paddingLeft); // e.g. 20
HTML attributes vs. DOM properties
For most situations using DOM properties is preferable.
We should refer to attributes only when DOM properties do not suit us, when we need exactly attributes, for instance:
- We need a non-standard non-“data-*” attribute. See Non-standard attributes use cases
- We want to read the value “as written” in HTML. For instanse: see note about
hrefattribute later on this page.
DOM properties
DOM nodes are regular JavaScript objects.
-
We can alter them.
// create a new property in document.body document.body.myData = { name: 'Caesar', title: 'Imperator' }; alert(document.body.myData.title); // Imperator // add a method document.body.sayTagName = function() { alert(this.tagName); }; document.body.sayTagName(); // BODY (the value of "this" in the method is document.body) // modify built-in prototypes like Element.prototype and add new methods to all elements Element.prototype.sayHi = function() { alert(`Hello, I'm ${this.tagName}`); }; document.documentElement.sayHi(); // Hello, I'm HTML document.body.sayHi(); // Hello, I'm BODY -
They can have any value and they are not always strings, i.e. they are typed(типизированные)
<!-- For instance, the element.input.checked property (for checkboxes) is a boolean --> <input id="input" type="checkbox" checked> checkbox <script> let input = document.querySelecor("#input"); alert(input.getAttribute('checked')); // the attribute value is: empty string alert(input.checked); // the property value is: true </script><!-- The "style" attribute is a string, but the 'style' property is an object. --> <div id="div" style="color:red;font-size:120%">Hello</div> <script> let div = document.querySelecor("#div"); // string alert(div.getAttribute('style')); // color:red;font-size:120% // object alert(div.style); // [object CSSStyleDeclaration] alert(div.style.color); // red </script>Most properties are strings.
Quite rarely, even if a DOM property type is a string, its valur may differ from the attribute's value. See note about
hrefattribute later on this page. -
They are case-sensitive (write
element.nodeType, notelement.NoDeTyPe).
HTML attributes
When the browser parses the HTML to create DOM objects for tags, it recognizes standard attributes and creates the corresponding DOM properties from them. But that doesn’t happen if the attribute is non-standard.
Most standard HTML attributes have the corresponding DOM properties. They described in the specification for the corresponding element class(see WHATWG: HTML Living Standard).
For instance, HTMLInputElement class is documented at https://html.spec.whatwg.org/#htmlinputelement.
Alternative way to get DOM properties
If we’d like to get them fast or are interested in a concrete browser specification – we can always output the element using console.dir(element) and read the properties.
Or explore “DOM properties” in the Elements tab of the browser developer tools.
HTML attributes have the following features:
- Their values are always strings.
- Their name is case-insensitive (
idis same asID), but usually attributes are lowercased.
<body id="test" something="non-standard">
<script>
alert(document.body.id); // test
// non-standard attribute does not yield a property
alert(document.body.something); // undefined
</script>
</body>
A standard attribute for one element can be unknown for another one. For instance, "type" is standard for <input>(HTMLInputElement specification class), but not for <body> (HTMLBodyElement specification class).
<body id="body" type="...">
<input id="input" type="text">
<script>
alert(input.type); // text
alert(body.type); // undefined: DOM property not created, because it's non-standard
</script>
</body>
Examples of standard attributes and their corresponing DOM nodes properties(depending on their specification class):
id– the value of “id” attribute, for all elements (HTMLElementclass).value– the value for<input>,<select>and<textarea>(classes:HTMLInputElement,HTMLSelectElement…).-
href– the “href” for<a href="...">(HTMLAnchorElementclass).hrefDOM property is always a full URL.Even if the attribute contains a relative URL or just a
#hash.<a id="a" href="#hello">link</a> <script> let a = document.querySelecor("#a"); // attribute alert(a.getAttribute('href')); // #hello // property alert(a.href ); // full URL in the form http://site.com/page#hello </script> -
…and much more…
All attributes are accessible by using the following methods:
-
element.attributes - read all attributes and return an iterable collection(can be iterated with
for...ofloop) of objects that belong to a built-in Attr class, withnameandvalueproperties -
element.hasAttribute("name") – checks for existence
-
element.getAttribute("name") – gets the value as string exactly as written in the HTML
document.querySelector("img").getAttribute("width");<body something="non-standard"> <script> alert(document.body.getAttribute('Something')); // non-standard; the first letter is uppercase here, // and in HTML it’s all lowercase. But that doesn’t matter: attribute names are case-insensitive. </script> </body> -
element.setAttribute("name", "value") – sets the value as string
document.querySelector("img").setAttribute("width", "5px");Can be used to change styles by changing value of 'class' atribute.
But this is the "old school" way as we have more advanced method to manipulate the style. See above.
-
element.removeAttribute("name") – removes the attribute
Property-attribute synchronization
When a standard attribute changes, the corresponding property is auto-updated, and (with some exceptions) vice versa.
In the example below id is modified as an attribute, and we can see the property changed too. And then the same backwards:
<input>
<script>
let input = document.querySelector('input');
// attribute => property
input.setAttribute('id', 'id');
alert(input.id); // id (updated)
// property => attribute
input.id = 'newId';
alert(input.getAttribute('id')); // newId (updated)
</script>
But there are exclusions, for instance:
-
element.input.valuesynchronizes only from attribute → property, but not back<input> <script> let input = document.querySelector('input'); // attribute => property input.setAttribute('value', 'text'); alert(input.value); // text // NOT property => attribute input.value = 'newValue'; alert(input.getAttribute('value')); // text (not updated!) </script>That “feature” may actually come in handy.
Because the user actions may lead to
valuechanges, and then after them, if we want to recover the “original” value from HTML, it’s in the attribute.
Non-standard attributes use cases
- To pass custom data from HTML to JavaScript.
-
To “mark” HTML-elements for JavaScript.
<!-- mark the div to show "name" here --> <div show-info="name"></div> <!-- and age here --> <div show-info="age"></div> <script> // the code finds an element with the mark and shows what's requested let user = { name: "Pete", age: 25 }; for(let div of document.querySelectorAll('[show-info]')) { // insert the corresponding info into the field let field = div.getAttribute('show-info'); div.innerHTML = user[field]; // first Pete into "name", then 25 into "age" } </script> -
To style an element.
<!-- For instance, here for the order state the attribute "order-state" is used:--> <style> /* styles rely on the custom attribute "order-state" */ .order[order-state="new"] { color: green; } .order[order-state="pending"] { color: blue; } .order[order-state="canceled"] { color: red; } </style> <div class="order" order-state="new"> A new order. </div> <div class="order" order-state="pending"> A pending order. </div> <div class="order" order-state="canceled"> A canceled order. </div>Why would using an attribute be preferable to having classes like
.order-state-new,.order-state-pending,.order-state-canceled? Because an attribute is more convenient to manage. The state can be changed as easy as:// a bit simpler than removing old/adding a new class div.setAttribute('order-state', 'canceled');
dataset DOM property
Possible problem with custom(non-standard) attributes: they can appear in standard specifications in the future and therefore become unavailable for our use. To avoid conflicts, there exist "data-*" attributes. They are actually a safe way to pass custom data.
All attributes starting with “data-” are reserved for programmers’ use. They are available in the element.dataset.[“data-*“ attribute(with ommited “data-” part) in camelCase] property.
<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"; // we can not only read, but also modify data-attributes
</script>
DOM Events
An event is a signal that something has happened(user actions, document events, CSS events etc.). All DOM nodes generate such signals(but events are not limited to DOM).
Event handlers
Handler - is a function that assigned to an event and runs when event happens.
There are 3 ways to assign event handlers:
-
HTML attribute:
on<event>="..."(...- JavaScript code).The browser reads it, creates a new function from the attribute content and writes it to the DOM property.
<!-- inside onclick we use single quotes, because the attribute itself is in double quotes --> <input value="Click me" onclick="alert('Click!')" type="button"> <!-- An HTML-attribute is not a convenient place to write a lot of code, so we’d better create a JavaScript function and call it there. --> <script> function countRabbits() { for(let i=1; i<=3; i++) { alert("Rabbit number " + i); } } </script> <input type="button" onclick="countRabbits()" value="Count rabbits!">Accessing the element using
this<!-- The value of 'this' inside a handler is the element. The one which has the handler on it. --> <button onclick="alert(this.innerHTML)">Click me</button> <!-- Click me -->HTML attributes are used sparingly.
Because JavaScript in the middle of an HTML tag looks a little bit odd and alien. Also can’t write lots of code in there.
-
DOM property: element.on<event> = function.
<!-- we can’t assign more than one handler of the particular event --> <input type="button" id="elem" onclick="alert('Before')" value="Click me"> <script> let elem = document.querySelecor("#elem"); elem.onclick = function() { // overwrites the existing handler alert('After'); // only this will be shown }; </script>Set an existing function as a handler.
function sayThanks() { alert('Thanks!'); } // function should be assigned without parentheses elem.onclick = sayThanks;<!-- On the other hand, in the markup we do need the parentheses --> <input type="button" id="button" onclick="sayThanks()">/* When the browser reads the attribute, it creates a handler function with body from the attribute content. So the markup generates this property: */ button.onclick = function() { sayThanks(); // <-- the attribute content goes here };To remove a handler – assign element.on<event> = null
-
Methods: element.addEventListener(event, handler[, options]) to add handler, element.removeEventListener(event, handler[, options]) to remove handler.
event- Event name, e.g."click".handler- The handler function.-
options- An additional optional object with properties:once: iftrue, then the listener is automatically removed after it triggers.capture: the phase where to handle the event. See in Bubbling and capturing point. For historical reasons,optionscan also befalse/true, that’s the same as{capture: false/true}.-
passive: iftrue, then the handler will not callpreventDefault()(trying to do this will throw an error). That’s useful for some mobile events, liketouchstartandtouchmove, to tell the browser that it should not wait for all handlers to finish before scrolling.For some browsers (Firefox, Chrome),
passiveistrueby default fortouchstartandtouchmoveevents.See more about
preventDefault()in Preventing browser actions point.
To remove a handler:
-
we should pass exactly the same function as was assigned
// The handler won’t be removed, because 'removeEventListener' gets another function // – with the same code, but that doesn’t matter, as it’s a different function object. elem.addEventListener("click" , () => alert('Thanks!')); // .... elem.removeEventListener("click", () => alert('Thanks!')); // Here’s the right way: function handler() { alert('Thanks!'); } input.addEventListener("click", handler); // .... input.removeEventListener("click", handler); // Please note – if we don’t store the function in a variable, then we can’t remove it. // There’s no way to “read back” handlers assigned by 'addEventListener'. -
also the phase should be the same
If we
addEventListener(..., true), then we should mention the same phase inremoveEventListener(..., true)to correctly remove the handler.
element.addEventListener(event, handler[, options]) allows to assign multiple handlers to one event.
<input id="elem" type="button" value="Click me"/> <script> function handler1() { alert('Thanks!'); }; function handler2() { alert('Thanks again!'); } let elem = documnet.querySelecor("#elem"); elem.onclick = () => alert("Hello"); elem.addEventListener("click", handler1); // Thanks! elem.addEventListener("click", handler2); // Thanks again! </script> <!-- We can set handlers both using a DOM-property and 'addEventListener'. But generally we use only one of these ways. -->For some events, handlers only work with element.addEventListener
-
DOMContentLoadedevent - triggers when the document is loaded and DOM is built// will never run document.onDOMContentLoaded = function() { alert("DOM built"); }; // this way it works document.addEventListener("DOMContentLoaded", function() { alert("DOM built"); }); -
transitionendevent
Also element.addEventListener supports objects as event handlers. In that case the method
handleEventis called in case of the event.<button id="elem">Click me</button> <script> let obj = { handleEvent(event) { alert(event.type + " at " + event.currentTarget); } }; let elem = document.querySelecor("#elem"); elem.addEventListener('click', obj); </script> <!-- As we can see, when 'addEventListener' receives an object as the handler, it calls 'obj.handleEvent(event)' in case of an event. -->We could also use a class for that:
<button id="elem">Click me</button> <script> class Menu { handleEvent(event) { switch(event.type) { case 'mousedown': elem.innerHTML = "Mouse button pressed"; break; case 'mouseup': elem.innerHTML += "...and released."; break; } } } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script>Here the same object handles both events. Please note that we need to explicitly setup the events to listen using
addEventListener. Themenuobject only getsmousedownandmouseuphere, not any other types of events.The method
handleEventdoes not have to do all the job by itself. It can call other event-specific methods instead, like this:<button id="elem">Click me</button> <script> class Menu { handleEvent(event) { // mousedown -> onMousedown OR mouseup -> onMouseup, etc. let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1); // call one of the methods that defined below(onMousedown() OR onMouseup(), etc.) this[method](); } onMousedown() { elem.innerHTML = "Mouse button pressed"; } onMouseup() { elem.innerHTML += "...and released."; } // Now event handlers are clearly separated, that may be easier to support. } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script>
Event object
No matter how you assign the handler – it gets an event object as the first argument. That object contains the details about what’s happened.
Here’s an example of getting pointer coordinates from the event object:
<input type="button" value="Click me" id="elem">
<script>
elem.onclick = function(event) {
// show event type, element and coordinates of the click
alert(event.type + " at " + event.currentTarget);
alert("Coordinates: " + event.clientX + ":" + event.clientY);
};
// 'event.type' - Event type, here it’s "click".
/* 'event.currentTarget' - Element that handled the event. That’s exactly the same as 'this',
unless the handler is an arrow function, or its 'this' is bound(using 'bind') to something else,
then we can get the element from 'event.currentTarget'. */
// 'event.clientX / event.clientY' - Window-relative coordinates of the cursor, for pointer events.
</script>
The event object is also available in HTML handlers.
<input type="button" onclick="alert(event.type)" value="Event type">
That’s possible because when the browser reads the attribute, it creates a handler like this: function(event) { alert(event.type) }. That is: its first argument is called event, and the body is taken from the attribute.
Bubbling and capturing
Всплытие и погружение
When an event happens – the most nested element where it happens gets labeled as the “target element” (event.target). Then:
-
Phase 1 - Capturing: the event moves down from the
documentroot toevent.target, calling handlers assigned withaddEventListener(..., true)on the way (trueis a shorthand for{capture: true}).Capture phase is invisible for handlers and they only run on the 2nd and 3rd phases when:
they added using
on<event>-property or using HTML attributes or using two-argumentaddEventListener(event, handler)The capturing phase is used very rarely.
Usually we handle events on bubbling.
-
Phase 2 - Target: handlers(on both capturing and bubbling phases, i.e. this phase is not handled separately) are called on the target element itself.
-
Phase 3 - Bubbling: the event bubbles up from
event.targetto the root (till thedocumentobject, and some events even reachwindow), calling handlers assigned usingon<event>, HTML attributes andaddEventListenerwithout the 3rd argument or with the 3rd argumentfalse/{capture:false}.Almost all events bubble. Here the list of those that don't:
focusevent
Example of both capturing and bubbling in action:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
}
</script>
The code sets click handlers on every element in the document to see which ones are working.
If you click on <p>, then the sequence is:
HTML→BODY→FORM→DIV→P(capturing phase, the first listener):P→DIV→FORM→BODY→HTML(bubbling phase, the second listener).
Please note, the P shows up twice, because we’ve set two listeners: capturing and bubbling. The target triggers at the end of the first and at the beginning of the second phase.
Listeners on same element and same phase run in their set order.
If we have multiple event handlers on the same phase, assigned to the same element with addEventListener, they run in the same order as they are created:
elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));
Each handler can access event object properties:
event.target– the deepest element that originated the event.event.currentTarget(=this) – the current element that handles the event(the one that has the handler on it)event.eventPhase– the current phase (capturing=1, target=2, bubbling=3). It’s rarely used, because we usually know it in the handler.
Any event handler can stop the event capturing/bubbling by calling:
-
event.stopPropagation()- for a single handler of that eventThat is if an element has multiple event handlers on a single event, then even if one of them stops the capturing/bubbling, the other ones still execute. In other words,
event.stopPropagation()stops the move downwards/upwards, but on the current element all other handlers will run. -
event.stopImmediatePropagation()- for a multiple handlers of that eventThis method stops the capturing/bubbling and prevents handlers on the current element from running. After it no other handlers execute.
<!-- here 'body.onclick' doesn’t work if you click on <button> -->
<body onclick="alert(`the bubbling doesn't reach here`)">
<button onclick="event.stopPropagation()">Click me</button>
</body>
The event.stop[Immediate]Propagation() during the capturing also prevents the bubbling.
In other words, normally the event goes first down (“capturing”) and then up (“bubbling”). But if event.stop[Immediate]Propagation() is called during the capturing phase, then the event travel stops, no bubbling will occur.
Don’t stop bubbling without a need!
Bubbling is convenient. Don’t stop it without a real need: obvious and architecturally well thought out. Всплытие – это удобно. Не прекращайте его без явной нужды, очевидной и архитектурно прозрачной.
Sometimes event.stopPropagation() creates hidden pitfalls that later may become problems.
For instance:
- We create a nested menu. Each submenu handles clicks on its elements and calls
stopPropagationso that the outer menu won’t trigger. - Later we decide to catch clicks on the whole window, to track users’ behavior (where people click). Some analytic systems do that. Usually the code uses
document.addEventListener('click'…)to catch all clicks. - Our analytic won’t work over the area where clicks are stopped by
stopPropagation. Sadly, we’ve got a “dead zone”.
There’s usually no real need to prevent the bubbling. A task that seemingly requires that may be solved by other means. One of them is to use custom events, we’ll cover them later. Also we can write our data into the event object in one handler and read it in another one, so we can pass to handlers on parents information about the processing below.
Bubbling and capturing lay the foundation for “event delegation” – an extremely powerful event handling pattern.
Event delegation
Event delegation is of the most powerful event handling patterns.
It’s often used to add the same handling for many similar elements, but not only for that.
same handling for many similar elements - if we have a lot of elements handled in a similar way, then instead of assigning a handler to each of them – we put a single handler on their common ancestor.
The algorithm:
- Put a single handler on the container.
- In the handler – check the source element
event.target. - If the event happened inside an element that interests us, then handle the event.
Benefits:
- Simplifies initialization and saves memory: no need to add many handlers.
- Less code: when adding or removing elements, no need to add/remove handlers.
- DOM modifications: we can mass add/remove elements with
innerHTMLand the like.
Limitations:
- The event must be bubbling. Some events do not bubble.
- Low-level handlers should not use
event.stopPropagation(). - The delegation may add CPU load, because the container-level handler reacts on events in any place of the container, no matter whether they interest us or not. But usually the load is negligible, so we don’t take it into account.
Delegation examples
basic example: highlight a cell <td> on click
<!DOCTYPE HTML>
<html>
<body>
<style>
#bagua-table th {
text-align: center;
font-weight: bold;
}
#bagua-table td {
width: 150px;
white-space: nowrap;
text-align: center;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 12px;
}
#bagua-table .nw {
background: #999;
}
#bagua-table .n {
background: #03f;
color: #fff;
}
#bagua-table .ne {
background: #ff6;
}
#bagua-table .w {
background: #ff0;
}
#bagua-table .c {
background: #60c;
color: #fff;
}
#bagua-table .e {
background: #09f;
color: #fff;
}
#bagua-table .sw {
background: #963;
color: #fff;
}
#bagua-table .s {
background: #f60;
color: #fff;
}
#bagua-table .se {
background: #0c3;
color: #fff;
}
#bagua-table .highlight {
background: red;
}
</style>
<table id="bagua-table">
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong>
<br>Metal
<br>Silver
<br>Elders
</td>
<td class="n"><strong>North</strong>
<br>Water
<br>Blue
<br>Change
</td>
<td class="ne"><strong>Northeast</strong>
<br>Earth
<br>Yellow
<br>Direction
</td>
</tr>
<tr>
<td class="w"><strong>West</strong>
<br>Metal
<br>Gold
<br>Youth
</td>
<td class="c"><strong>Center</strong>
<br>All
<br>Purple
<br>Harmony
</td>
<td class="e"><strong>East</strong>
<br>Wood
<br>Blue
<br>Future
</td>
</tr>
<tr>
<td class="sw"><strong>Southwest</strong>
<br>Earth
<br>Brown
<br>Tranquility
</td>
<td class="s"><strong>South</strong>
<br>Fire
<br>Orange
<br>Fame
</td>
<td class="se"><strong>Southeast</strong>
<br>Wood
<br>Green
<br>Romance
</td>
</tr>
</table>
<script>
let table = document.getElementById('bagua-table');
let selectedTd;
// code for basic explanation, see after this codeblock
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)
if (!td) return; // (2)
if (!table.contains(td)) return; // (3)
highlight(td); // (4)
};
// more advaced code that do the same as 'table.oncklick = ...' block above
table.onclick = function(event) {
let target = event.target; // where was the click?
while (target != this) {
if (target.tagName == 'TD') {
highlight(target); // highlight it
return;
}
// while we are not on 'TD' we "level up" our target variable
// to the next 'parentNode' until it reaches 'TD' node
target = target.parentNode;
}
}
function highlight(node) {
if (selectedTd) { // remove the existing highlight if any
selectedTd.classList.remove('highlight');
}
selectedTd = node;
selectedTd.classList.add('highlight'); // highlight the new td
}
</script>
</body>
</html>
- The method element.closest(selector) returns the nearest ancestor that matches the selector. In our case we look for
<td>on the way up from the source element. - If
event.targetis not inside any<td>, then the call returns immediately, as there’s nothing to do. - In case of nested tables,
event.targetmay be a<td>, but lying outside of the current table. So we check if that’s actually our table’s<td>. - And, if it’s so, then highlight it.
As the result, we have a fast, efficient highlighting code, that doesn’t care about the total number of <td> in the table.
actions in markup
действия в разметке
Let’s say, we want to make a menu with buttons “Save”, “Load”, “Search” and so on. And there’s an object with methods save, load, search… How to match them?
The first idea may be to assign a separate handler to each button. But there’s a more elegant solution. We can add a handler for the whole menu and data-action attributes for buttons that has the method to call.
The handler reads the attribute and executes the method.
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem; // underscore in '_this.elem' is a naming convention for private vatiables
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action](); // (**)
}
};
}
new Menu(menu);
</script>
Please note that this.onClick is bound to this in (*). That’s important, because otherwise this inside it would reference the DOM element(elem), not the Menu object, and this[action] in (**) would not be what we need.
So, what advantages does delegation give us here?
- We don’t need to write the code to assign a handler to each button. Just make a method and put it in the markup.
- The HTML structure is flexible, we can add/remove buttons at any time.
We could also use classes .action-save, .action-load, but an attribute data-action is better semantically. And we can use it in CSS rules too.
The “behavior” pattern
We can also use event delegation to add “behaviors” to elements declaratively, with special attributes and classes.
The pattern has two parts:
- We add a custom attribute to an element that describes its behavior.
- A document-wide handler tracks events, and if an event happens on an attributed element – performs the action.
The “behavior” pattern can be an alternative to mini-fragments of JavaScript.
example: counter
Here the attribute data-counter adds a behavior: “increase value on click” to buttons:
Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>
<script>
document.addEventListener('click', function(event) {
if (event.target.dataset.counter != undefined) { // if the attribute exists...
event.target.value++;
}
});
</script>
If we click a button – its value is increased. Not buttons, but the general approach is important here.
There can be as many attributes with data-counter as we want. We can add new ones to HTML at any moment. Using the event delegation we “extended” HTML, added an attribute that describes a new behavior.
For document-level handlers – always use addEventListener.
When we assign an event handler to the document object, we should always use addEventListener, not document.on<event>, because the latter will cause conflicts: new handlers overwrite old ones.
For real projects it’s normal that there are many handlers on document set by different parts of the code.
example: toggler
A click on an element with the attribute data-toggle-id will show/hide the element with the given id:
<button data-toggle-id="subscribe-mail">
Show the subscription form
</button>
<form id="subscribe-mail" hidden>
Your mail: <input type="email">
</form>
<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;
let elem = document.getElementById(id);
elem.hidden = !elem.hidden;
});
</script>
Now, to add toggling functionality to an element no need to write JavaScript for every such element. Just use the behavior, i.e. the attribute data-toggle-id. The document-level handler makes it work for any element of the page.
We can combine multiple behaviors on a single element as well.
Browser default actions
There are many default browser actions:
mousedown– starts the selection (move the mouse to select).clickon<input type="checkbox">– checks/unchecks the input.contextmenu– the event happens on a right-click, the action is to show the browser context menu.submit– clicking an<input type="submit">or hitting Enter inside a form field causes this event to happen, and the browser submits the form after it.keydown– pressing a key may lead to adding a character into a field, or other actions.- …there are more…
Preventing browser actions
All the default actions can be prevented if we want to handle the event exclusively by JavaScript.
To prevent a default action:
-
event.preventDefault()- this is the main way -
return false- this way works only for handlers assigned withon<event>element methodreturn falseis an exception.The value returned by an event handler is usually ignored.
The only exception is
return falsefrom a handler assigned usingon<event>.In all other cases,
returnvalue is ignored. In particular, there’s no sense in returningtrue.
<!-- a click on a link doesn’t lead to navigation; the browser doesn’t do anything -->
<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>
Follow-up events.
Certain events flow one into another. If we prevent the first event, there will be no second.
For instance, mousedown on an <input> field leads to focusing in it, and the focus event. If we prevent the mousedown event, there’s no focus.
Stay semantic, don’t abuse. Сохраняйте семантику, не злоупотребляйте.
Technically, by preventing default actions and adding JavaScript we can customize the behavior of any elements. For instance, we can make a link <a> work like a button, and a button <button> behave as a link (redirect to another URL or so).
But we should generally keep the semantic meaning of HTML elements. For instance, <a> should perform navigation, not a button.
Besides being “just a good thing”, that makes your HTML better in terms of accessibility.
Also if we consider the example with <a>, then please note: a browser allows us to open such links in a new window (by right-clicking them and other means). And people like that. But if we make a button behave as a link using JavaScript and even look like a link using CSS, then <a>-specific browser features still won’t work for it.
<input value="Focus works" onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="Click me">
If the default action was prevented, the value of event.defaultPrevented becomes true, otherwise it’s false.
Sometimes we can use event.defaultPrevented instead of using event.stopPropagation(), to signal other event handlers that the event was handled.
Example: Preventing default actions of contextmenu for <button> element and also for whole document. The problem is that when we click on elem, we get two menus: the button-level and (the event bubbles up) the document-level menu. Here is the solution:
<p>Right-click for the document menu (added a check for event.defaultPrevented)</p>
<button id="elem">Right-click for the button menu</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Button context menu");
};
document.oncontextmenu = function(event) {
// solution: check if the default action was prevented?
// If it is so, then the event was handled, and we don’t need to react on it.
if (event.defaultPrevented) return;
event.preventDefault();
alert("Document context menu");
};
</script>
If we have nested elements, and each of them has a context menu of its own, that would also work. Just make sure to check for event.defaultPrevented in each contextmenu handler.
Dispatching custom events(TODO)
Генерация пользовательских событий
TODO: https://javascript.info/dispatch-events
UI Events
Mouse events
Such events may come not only from “mouse devices”.
But are also from other devices, such as phones and tablets, where they are emulated for compatibility.
Mouse event types:
-
Simple events:
mousedown/mouseup- Mouse button is clicked/released over an element.-
mouseover/mouseout- Mouse pointer comes over/out from an element. They trigger even when we go from the parent element to a child element. The browser assumes that the mouse can be only over one element at one time – the deepest one. See more on mouseover/out point. -
mouseenter/mouseleave- Mouse pointer enters/leaves the element. They only trigger when the mouse comes in and out the element as a whole. Also they do not bubble. See more on mouseenter/leave point. -
mousemove- Every mouse move over an element triggers that event.A fast mouse move may skip intermediate elements.
mousemovedoesn't trigger on every pixel. The browser checks the mouse position from time to time. That means that if the visitor is moving the mouse very fast then some DOM-elements may be skipped.That’s good for performance, because there may be many intermediate elements. We don’t really want to process in and out of each one.
-
contextmenu- Triggers when the right mouse button is pressed. There are other ways to open a context menu, e.g. using a special keyboard key, it triggers in that case also, so it’s not exactly the mouse event.
-
Complex events(consist of several simple events):
click- Triggers aftermousedownand thenmouseupover the same element if the left mouse button was used.dblclick- Triggers after two clicks on the same element within a short timeframe. Rarely used nowadays.
Mouse event properties
mouse button
-
event.button - returns the exact mouse button
Has following possible values:
0- Left button (primary)1- Middle button (auxiliary)2- Right button (secondary)3- X1 button (back)4- X2 button (forward)
-
event.buttons - return all currently pressed buttons as an integer, one bit per button. In practice this property is very rarely used, you can find details at MDN if you ever need it.
-
event.which - DEPRECATED! An old non-standard way of getting a button, with possible values:
event.which == 1– Left button,event.which == 2– Middle button,event.which == 3– Right button.
modifiers: shift, alt, ctrl and meta
shiftKey: ShiftaltKey: Alt (or Option for Mac)ctrlKey: CtrlmetaKey: Cmd for Mac
They are true if the corresponding key was pressed during the event.
For instance, the button below only works on Alt+Shift+ click:
<button id="button">Alt+Shift+Click on me!</button>
<script>
button.onclick = function(event) {
if (event.altKey && event.shiftKey) {
alert('Hooray!');
}
};
</script>
Attention: on Mac it’s usually Cmd instead of Ctrl.
Even if we’d like to force Mac users to Ctrl+click – that’s kind of difficult. The problem is: a left-click with Ctrl is interpreted as a right-click on MacOS, and it generates the contextmenu event, not click like Windows/Linux.
So if we want users of all operating systems to feel comfortable, then together with ctrlKey we should check metaKey.
For JS-code it means that we should check if (event.ctrlKey || event.metaKey).
coordinates
All mouse events provide coordinates in two flavours:
-
Window-relative:
clientXandclientY.Are counted from the current window left-upper corner. When the page is scrolled, they change.
For instance, if we have a window of the size 500x500, and the mouse is in the left-upper corner, then
clientXandclientYare0, no matter how the page is scrolled. And if the mouse is in the center, thenclientXandclientYare250, no matter what place in the document it is. They are similar toposition:fixedin that aspect. Move the mouse over the input field to seeclientX/clientY(the example is in theiframe, so coordinates are relative to thatiframe):<input onmousemove="this.value=event.clientX+':'+event.clientY" value="Mouse over me"> -
Document-relative:
pageXandpageY.Are counted from the left-upper corner of the document, and do not change when the page is scrolled.
Preventing selection on mousedown
Selection cases:
-
Left mouse holding pressing and moving: makes the selection, often unwanted. TODO: There are multiple ways to prevent the selection, that you can read in https://javascript.info/selection-range.
-
Double mouse click: has a side effect that may be disturbing in some interfaces: it selects text.
To prevent selection in this case is to prevent the browser action on
mousedown. It prevent first selection case too.Before... <b ondblclick="alert('Click!')" onmousedown="return false"> Double-click me </b> ...AfterNow the bold element is not selected on double clicks, and pressing the left button on it won’t start the selection. The text inside it is still selectable. However, the selection should start not on the text itself, but before or after it. Usually that’s fine for users.
Preventing copying.
If we want to disable selection to protect our page content from copy-pasting, then we can use another event: oncopy.
<div oncopy="alert('Copying forbidden!');return false">
Dear user,
The copying is forbidden for you.
If you know JS or HTML, then you can get everything from the page source though.
</div>
Moving the mouse
mouseover/out
The mouseover event occurs when a mouse pointer comes over an element, and mouseout – when it leaves.
If mouseover triggered, there must be mouseout.
In case of fast mouse movements, intermediate elements may be ignored, but one thing we know for sure: if the pointer “officially” entered an element (mouseover event generated), then upon leaving it we always get mouseout.
These event have special property event.relatedTarget
For mouseover:
event.target– is the element where the mouse came over.event.relatedTarget– is the element from which the mouse came (relatedTarget→target).
For mouseout the reverse:
event.target– is the element that the mouse left.event.relatedTarget– is the new under-the-pointer element, that mouse left for (target→relatedTarget).
event.relatedTarget can be null.
That’s normal and just means that the mouse came not from another element, but from out of the window. Or that it left the window.
If we access event.relatedTarget.tagName, then there will be an error.
mouseout when leaving for a child:
An important feature of mouseout – it triggers, when the pointer moves from an element to its descendant(just the same as if it was moving out of the parent element itself), e.g. if we’re on #parent and then move the pointer deeper into #child, we get mouseout on #parent in this HTML:
<div id="parent">
<div id="child">...</div>
</div>
According to the browser logic: the mouse cursor may be only over a single element at any time – the most nested one and top by z-index. So if it goes to another element (even a descendant), then it leaves the previous one.
mouseover when leaving for a child:
The mouseover event on a descendant bubbles up. So, if #parent has mouseover handler, it triggers.
In the example below moving the mouse from #parent to #child, generates two events on #parent:
mouseout [target: parent](left the parent), thenmouseover [target: child](came to the child, bubbled).
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<style>
#parent {
background: #99C0C3;
width: 160px;
height: 120px;
position: relative;
}
#child {
background: #FFDE99;
width: 50%;
height: 50%;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
textarea {
height: 140px;
width: 300px;
display: block;
}
</style>
<div id="parent" onmouseover="mouselog(event)" onmouseout="mouselog(event)">parent
<div id="child">child</div>
</div>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script>
function mouselog(event) {
let d = new Date();
text.value += `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n`.replace(/(:|^)(\d\D)/, '$10$2');
text.scrollTop = text.scrollHeight;
}
</script>
</body>
</html>
If there are some actions upon leaving the parent element, e.g. an animation runs in parent.onmouseout, we usually don’t want it when the pointer just goes deeper into #parent.
To avoid it, we can check event.relatedTarget in the handler and, if the mouse is still inside the element, then ignore such event.
Alternatively we can use other events: mouseenter and mouseleave, see next point.
mouseenter/leave
mouseenter/mouseleave like mouseover/mouseout trigger when the mouse pointer enters/leaves the element.
But there are two important differences:
- Transitions inside the element, to/from descendants, are not counted.
- Events
mouseenter/mouseleavedo not bubble.
So if in the example form previous point above we'll change the events on top element <div id='parent' ... > from mouseover/mouseout to mouseenter/mouseleave, we'll see that he only generated events are the ones related to moving the pointer in and out of the top element. Nothing happens when the pointer goes to the child and back. Transitions between descendants are ignored.
event delegation example
Highlighting TD elements as the mouse travels across them:
Beacuse of limitation of "not-bubbling" of the mouseenter/mouseleave events we use mouseover/mouseout events for "delegation" event handling pattern.
In our case we’d like to handle transitions between table cells <td>: entering a cell and leaving it. Other transitions, such as inside the cell or outside of any cells, don’t interest us. Let’s filter them out.
Here’s what we can do:
- Remember the currently highlighted
<td>in a variable, let’s call itcurrentElem. - On
mouseover– ignore the event if we’re still inside the current<td>. - On
mouseout– ignore if we didn’t leave the current<td>.
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<style>
#text {
display: block;
height: 100px;
width: 456px;
}
#table th {
text-align: center;
font-weight: bold;
}
#table td {
width: 150px;
white-space: nowrap;
text-align: center;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 12px;
cursor: pointer;
}
#table .nw {
background: #999;
}
#table .n {
background: #03f;
color: #fff;
}
#table .ne {
background: #ff6;
}
#table .w {
background: #ff0;
}
#table .c {
background: #60c;
color: #fff;
}
#table .e {
background: #09f;
color: #fff;
}
#table .sw {
background: #963;
color: #fff;
}
#table .s {
background: #f60;
color: #fff;
}
#table .se {
background: #0c3;
color: #fff;
}
#table .highlight {
background: red;
}
</style>
<table id="table">
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong>
<br>Metal
<br>Silver
<br>Elders
</td>
<td class="n"><strong>North</strong>
<br>Water
<br>Blue
<br>Change
</td>
<td class="ne"><strong>Northeast</strong>
<br>Earth
<br>Yellow
<br>Direction
</td>
</tr>
<tr>
<td class="w"><strong>West</strong>
<br>Metal
<br>Gold
<br>Youth
</td>
<td class="c"><strong>Center</strong>
<br>All
<br>Purple
<br>Harmony
</td>
<td class="e"><strong>East</strong>
<br>Wood
<br>Blue
<br>Future
</td>
</tr>
<tr>
<td class="sw"><strong>Southwest</strong>
<br>Earth
<br>Brown
<br>Tranquility
</td>
<td class="s"><strong>South</strong>
<br>Fire
<br>Orange
<br>Fame
</td>
<td class="se"><strong>Southeast</strong>
<br>Wood
<br>Green
<br>Romance
</td>
</tr>
</table>
<textarea id="text"></textarea>
<input type="button" onclick="text.value=''" value="Clear">
<script>
// <td> under the mouse right now (if any)
let currentElem = null;
table.onmouseover = function(event) {
// before entering a new element, the mouse always leaves the previous one
// if currentElem is set, we didn't leave the previous <td>,
// that's a mouseover inside it, ignore the event
if (currentElem) return;
let target = event.target.closest('td');
// we moved not into a <td> - ignore
if (!target) return;
// moved into <td>, but outside of our table (possible in case of nested tables)
// ignore
if (!table.contains(target)) return;
// hooray! we entered a new <td>
currentElem = target;
onEnter(currentElem);
};
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
// that's probably a move inside the table, but out of <td>,
// e.g. from <tr> to another <tr>
if (!currentElem) return;
// we're leaving the element – where to? Maybe to a descendant?
let relatedTarget = event.relatedTarget;
while (relatedTarget) {
// go up the parent chain and check – if we're still inside currentElem
// then that's an internal transition – ignore it
if (relatedTarget == currentElem) return;
relatedTarget = relatedTarget.parentNode;
}
// we left the <td>. really.
onLeave(currentElem);
currentElem = null;
};
// any functions to handle entering/leaving an element
function onEnter(elem) {
elem.style.background = 'pink';
// show that in textarea
text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
text.scrollTop = 1e6;
}
function onLeave(elem) {
elem.style.background = '';
// show that in textarea
text.value += `out <- ${elem.tagName}.${elem.className}\n`;
text.scrollTop = 1e6;
}
</script>
</body>
</html>
Drag'n'Drop with mouse events(TODO)
TODO: https://javascript.info/mouse-drag-and-drop
Pointer events(TODO)
TODO: https://javascript.info/pointer-events
Keyboard events
Pressing a key always generates a keyboard events:
keypress(legacy) – no need to use anymore!keydown– on pressing the key (auto-repeats, i.e. triggers again and again if the key is pressed for long)keyup– on releasing the key
The only exception is Fn key, because it’s often implemented on lower level than OS.
Main keyboard event properties:
event.keyCode/charCode/which(legacy, they use Javascript Char Codes (Key Codes) as their value) - no need to use anymore!-
event.code– the “key code” specific to the physical location of the key on keyboard. Key codes described in the UI Events code specification.For instance:
-
Letter keys have codes
Key<letter>:KeyA,KeyBetc.There are several widespread keyboard layouts, and the specification gives key codes for each of them. Read the alphanumeric section of the spec for more codes.
-
Digit keys have codes:
Digit<number>:Digit0,Digit1etc. -
Special keys are mostly coded by their names:
Enter,Backspace,Tab,ShiftRight,ShiftLeft,F1etc.
-
-
event.key– the character ("A","a"and so on), for non-character keys, such as Esc, usually has the same value asevent.code.
event.key vs. event.code
To handle keyboard layout-dependant keys → event.key is the way to go.
Because same letters in different layouts may map to different physical keys, leading to different codes.
See the full list in the specification.
To get a hotkey to work even after a language switch → event.code may be better.
event.repeat- for events triggered by auto-repeat this property set totrue(defatul value isfasle)
Examples:
-
Preventing default acitons on
keydown:We can cancel most of them, with the exception of OS-based special keys. For instance, on Windows Alt+F4 closes the current browser window. And there’s no way to stop it by preventing the default action in JavaScript.
For instance, the
<input>below expects a phone number, so it does not accept keys except digits,+,(),-,Left,Right,Delete,Backspace:<script> function checkPhoneKey(key) { return (key >= '0' && key <= '9') || ['+','(',')','-','ArrowLeft','ArrowRight','Delete','Backspace'].includes(key); } </script> <input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">The onkeydown handler here uses checkPhoneKey to check for the key pressed. If it’s valid, then it returns
true, otherwisefalse. As we know, thefalsevalue returned from the event handler, assigned using a DOM property or an attribute, such as above, prevents the default action, so nothing appears in the<input>for keys that don’t pass the test. (Thetruevalue returned doesn’t affect anything, only returningfalsematters) -
Text characters limit counter:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <style> .textarea { } .textarea__item { width: 300px; height: 150px; padding: 10px; font-size: 18px; color: #fff; background-color: rgb(44, 43, 43); } .textarea__counter { } </style> <div class="textarea"> <textarea autocomplete="off" maxlength="30" name="form[]" class="textarea__item"></textarea> <div class="textarea__counter">Осталось <span></span> символов</div> </div> <script> const txtItem = document.querySelector('.textarea__item'); const txtItemLimit = txtItem.getAttribute('maxlength'); const txtCounter = document.querySelector('.textarea__counter span'); txtCounter.innerHTML = txtItemLimit; txtItem.addEventListener("keyup", txtSetCounter); txtItem.addEventListener("keydown", function (event) { if (event.repeat) txtSetCounter(); }); function txtSetCounter() { const txtCounterResult = txtItemLimit - txtItem.value.length; txtCounter.innerHTML = txtCounterResult; } </script> </body> </html>
Not 100% reliable.
In the past, keyboard events were sometimes used to track user input in form fields. That’s not reliable, because the input can come from various sources(e.g. mobile keyboards formally known as IME(Input-Method Editor)). We have input and change events to handle any input (covered in TODO: Events: change, input, cut, copy, paste). They trigger after any kind of input, including copy-pasting or speech recognition.
We should use keyboard events when we really want keyboard.
For example, to react on hotkeys or special keys.
Scrolling
scroll event - allows reacting to a page or element scrolling
May be used for:
- Show/hide additional controls or information depending on where in the document the user is.
- Load more data when the user scrolls down till the end of the page.
BUT: there is more interesting way to implement these(and many others) functionalitis by using IntersectionObserver which allows asynchronously watch for intersection of the element with his parent or visible document area.
// a small function to show the current scroll:
window.addEventListener('scroll', function() {
document.getElementById('showScroll').innerHTML = window.pageYOffset + 'px';
});
/*
In action:
- The 'scroll' event works both on the 'window' and on scrollable elements.
*/
Prevent scrolling
We can’t prevent scrolling by using event.preventDefault() in onscroll listener, because it triggers after the scroll has already happened.
But we can prevent scrolling by event.preventDefault() on an event that causes the scroll, for instance keydown event for Page Up and Page Down.
If we add an event handler to these events and event.preventDefault() in it, then the scroll won’t start.
BUT: There are many ways to initiate a scroll, so it’s more reliable to use CSS, overflow: hidden; property.
Document and resource loading
Page: DOMContentLoaded, load, beforeunload, unload
Page load events:
-
DOMContentLoaded- triggers ondocumentwhen the browser fully loaded HTML, and the DOM tree is built, but external resources like pictures<img>and stylesheets, etc. may not yet have loaded.Usage: the handler can lookup DOM nodes, initialize the interface.
We must use
addEventListenerto catch it:<script> function ready() { alert('DOM is ready'); // image is not yet loaded (unless it was cached), so the size is 0x0 alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`); } /* The 'DOMContentLoaded' handler runs when the document is loaded, so it can see all the elements, including <img> below. But it doesn’t wait for the image to load. So 'alert' shows zero sizes. */ document.addEventListener("DOMContentLoaded", ready); // not "document.onDOMContentLoaded = ..." </script> <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">Peculiarities(особенности) regarding
DOMContentLoadedevent running:-
Script such as
<script>...</script>or<script src="..."></script>block DOMContentLoaded.When the browser processes an HTML-document and comes across a
<script>tag, it needs to execute before continuing building the DOM. That’s a precaution, as scripts may want to modify DOM, and evendocument.writeinto it, soDOMContentLoadedhas to wait.<!-- we first see “Library loaded…”, and then “DOM ready!” (all scripts are executed) --> <script> document.addEventListener("DOMContentLoaded", () => { alert("DOM ready!"); }); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script> <script> alert("Library loaded, inline script executed"); </script>Scripts that don’t block DOMContentLoaded.
There are two exceptions from this rule:
- Scripts with the
asyncattribute, don’t blockDOMContentLoaded. See TODO: https://javascript.info/script-async-defer - Scripts that are generated dynamically with
document.createElement('script')and then added to the webpage also don’t block this event.
- Scripts with the
-
Images and other resources may also still continue loading.
External style sheets don’t affect DOM, so
DOMContentLoadeddoes not wait for them.BUT: If we have a script after the style, then that script must wait until the stylesheet loads:
<link type="text/css" rel="stylesheet" href="style.css"> <script> // the script doesn't execute until the stylesheet is loaded alert(getComputedStyle(document.body).marginTop); </script>The reason for this is that the script may want to get coordinates and other style-dependent properties of elements, like in the example above. Naturally, it has to wait for styles to load. As
DOMContentLoadedwaits for scripts, it now waits for styles before them as well. -
Built-in browser autofill.
Firefox, Chrome and Opera autofill forms on
DOMContentLoaded.For instance, if the page has a form with login and password, and the browser remembered the values, then on
DOMContentLoadedit may try to autofill them (if approved by the user).So if
DOMContentLoadedis postponed by long-loading scripts, then autofill also awaits – the login/password fields don’t get autofilled immediately, but there’s a delay till the page fully loads. That’s actually the delay until theDOMContentLoadedevent.
-
-
load- triggers onwindowwhen the page and all external resources(images, styles etc.) are loaded.Usage: external resources are loaded, so styles are applied, image sizes are known etc. We rarely use it, because there’s usually no need to wait for so long.
<!-- correctly shows image sizes, because 'window.onload' waits for all images: --> <script> window.onload = function() { // can also use window.addEventListener('load', (event) => { alert('Page loaded'); // image is loaded at this time alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`); }; </script> <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0"> -
beforeunload- triggers onwindowwhen the user initiated navigation away from the page or tries to close the window; in this case the handler asks for additional confirmationUsage: cancel the transition to another page - if we cancel the event, browser asks whether the user really wants to leave(e.g. we have unsaved changes).
window.onbeforeunload = function() { return false; };For historical reasons, returning a non-empty string also counts as canceling the event. Some time ago browsers used to show it as a message, but as the modern specification says, they shouldn’t. The behavior was changed, because some webmasters abused this event handler by showing misleading and annoying messages. So right now old browsers still may show it as a message, but aside of that – there’s no way to customize the message shown to the user.
window.onbeforeunload = function() { return "There are unsaved changes. Leave now?"; };The
event.preventDefault()doesn’t work from abeforeunloadhandlerThat may sound weird, but most browsers ignore
event.preventDefault(). Which means, following code may not work:window.addEventListener("beforeunload", (event) => { // doesn't work, so this event handler doesn't do anything event.preventDefault(); });Instead, in such handlers one should set
event.returnValueto a string to get the result similar to the code above:window.addEventListener("beforeunload", (event) => { // works, same as returning from window.onbeforeunload event.returnValue = "There are unsaved changes. Leave now?"; }); -
unload- triggers onwindowwhen the user is finally leavingUsage: the user almost left and in the handler we can only do simple things that do not involve delays or asking a user. Because of that limitation, it’s rarely used. For instanse:
- we can close related popup windows
-
we can send out a network request with a special
navigator.sendBeacon(url, data)method (described in the specification https://w3c.github.io/beacon/), that contains e.g. the data about how the page is used: mouse clicks, scrolls, viewed page areas, and so onsendBeaconsends data in background without delaying the transition to another page: the browser leaves the page, but still performssendBeacon. Here’s how to use it:let analyticsData = { /* object with gathered data */ }; window.addEventListener("unload", function() { navigator.sendBeacon("/analytics", JSON.stringify(analyticsData)); });- the request is sent as POST
- we can send not only a string, but also forms and other formats(see Fetch), but usually it’s a stringified object
- the data is limited by 64kb
When the
sendBeaconrequest is finished, the browser probably has already left the document, so there’s no way to get server response (which is usually empty for analytics). There’s also akeepaliveflag for doing such “after-page-left” requests in fetch method for generic network requests. You can find more information in the chapter Fetch API.
-
readystatechange- tracks the changes in value of thedocument.readyStatemethod(see below at this point)It is an alternative mechanics of tracking the document loading state, it appeared long ago. Nowadays, it is rarely used.
// current state console.log(document.readyState); // prints 'loading' // print state changes document.addEventListener('readystatechange', () => console.log(document.readyState)); // prints 'interactive' and then 'complete'
document.readyState - current state of the document, has 3 following values:
loading– the document is loading.interactive– the document is parsed(= was fully read), happens at about the same time asDOMContentLoaded, but before it.complete– the document and resources are loaded(= was fully read and all resources(like images) are loaded too), happens at about the same time aswindow.onload, but before it
We can check document.readyState and setup a handler or execute the code immediately if it’s ready, like this:
function work() { /*...*/ }
if (document.readyState == 'loading') {
// still loading, wait for the event
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM is ready!
work();
}