DOM Traversal and Manipulation with VanillaJS
I've moved this article/cheatsheet to my blog, to check the latest version click here.
// $(document).ready(callback);
document.addEventListener('DOMContentLoaded', callback);
// $(window).load(callback);
window.addEventListener('load', callback);
window.onload = callback;
// const divs = $('ul.nav > li');
const items = document.querySelectorAll('ul.nav > li');
const firstItem = document.querySelector('ul.nav > li');
// const title = $('#title');
const title = document.getElementById('title');
// const images = $('.image');
const images = document.getElementsByClassName('image');
// const articles = $('article');
const articles = document.getElementsByTagName('article');
Selector | Returns a collection | Returns a LIVE collection | Return type | Built-in forEach | Works with any root element |
---|---|---|---|---|---|
getElementById |
🚫 | N/A | Reference to an Element object or null |
N/A | 🚫 |
getElementsByClassName |
✅ | ✅ | HTMLCollection |
🚫 | ✅ |
getElementsByTagName |
✅ | ✅ | HTMLCollection according to the spec (NodeList in WebKit) (*) |
🚫 | ✅ |
querySelector |
🚫 | N/A | Element object or null |
N/A | ✅ |
querySelectorAll |
✅ | 🚫 | Static NodeList of Element objects |
✅ | ✅ |
(*) The latest W3C specification says it returns an HTMLCollection
; however, this method returns a NodeList
in WebKit browsers. See bug 14869 for details.
Since all of the selectors (except getElementById
) support querying a node other than document
, finding results trivial:
// $(el).find(selector);
el.querySelectorAll(selector);
function $$(selector) {
var nodes = document.querySelectorAll(selector);
return Array.prototype.slice.call(nodes);
}
$$('selector');
// $('<div />');
const newDiv = document.createElement('div');
// There is no equivalent in jQuery for createTextNode.
// You can always use the DOM method, or write a jQuery wrapper around it.
// The closest thing you may be able to find is when creating new elements,
// you can specify the text part separately.
// $('<div>', { text: 'hello world' });
const newTextNode = document.createTextNode('hello world');
// $(parent).append(el);
parent.appendChild(el);
// $(parent).prepend(el);
parent.prepend(el);
// ⚠️ Heads up: needs to be polyfilled on IE and Edge.
// $(parent).prepend(el);
parent.insertBefore(el, parent.firstChild);
el.insertBefore(node) // @TODO
// $(el).before(htmlString);
el.insertAdjacentHTML('beforebegin', htmlString);
// $(el).after(htmlString);
el.insertAdjacentHTML('afterend', htmlString);
// $(el).children();
el.children // only HTMLElements
el.childNodes // includes comments and text nodes
// ⚠️ Heads up: you can't `forEach` through `children` unless you turn it into an array first.
// $(el).parent();
el.parentNode
// $(el).closest(selector);
el.closest(selector);
// $(el).first();
el.firstElementChild; // only HTMLElements
el.firstChild; // includes comments and text nodes
// $(el).last();
el.lastElementChild; // only HTMLElements
el.lastChild; // includes comments and text nodes
// First and last alternative
var nodeList = document.querySelectorAll('.some-class');
var first = nodeList[0];
var last = nodeList[nodeList.length - 1];
// $(el).siblings();
[].filter.call(el.parentNode.children, function (child) {
return child !== el;
});
// $(el).prev();
el.previousElementSibling // only HTMLElements
el.previousSibling // includes comments and text nodes
// $(el).next();
el.nextElementSibling // only HTMLElements
node.nextSibling // includes comments and text nodes
// $.contains(el, child);
el !== child && el.contains(child);
var nodes = document.querySelectorAll('.class-name');
// 1.
var elements = Array.prototype.slice.call(nodes);
elements.forEach(noop);
// 2. (clean, but creates a new array)
[].forEach.call(nodes, noop);
// 3.
Array.prototype.forEach.call(nodes, noop);
Find the closest element that matches the target selector:
var node = document.getElementById('my-id');
var isFound = false;
while (node instanceof Element) {
if (node.matches('.target-class')) {
isFound = true;
break;
}
node = node.parentNode;
}
Element.prototype.closest
Polyfill:
if (Element && !Element.prototype.closest) {
Element.prototype.closest = function (selector) {
var el = this;
while (el instanceof Element) {
if (el.matches(selector)) {
return el;
}
el = el.parentNode;
}
}
}
Heads up: Element.remove() needs to be polyfilled on IE.
// $(el).remove();
el.parentNode.removeChild(el);
el.remove(); ⚠️
// Remove all GIF images from the page
[].forEach.call(document.querySelectorAll('img'), function (img) {
if (/\.gif/i.test(img.src)) {
img.remove();
}
});
// $(el).replaceWith($('.first'));
el.parentNode.replaceChild(newNode, el);
// $(el).replaceWith(string);
el.outerHTML = string;
// $(el).clone();
el.cloneNode(true);
Pass in true
to also clone child nodes.
// $(el).is(':empty')
!el.hasChildNodes();
// $(el).empty();
const el = document.getElementById('el');
while (el.firstChild) {
el.removeChild(el.firstChild);
}
You could also do el.innerHTML = ''
.
// $(el).is($(otherEl));
el === otherEl
// $(el).is('.my-class');
el.matches('.my-class');
// $(el).is('a');
el.matches('a');
Note that matches
needs to be polyfilled in older browsers. Also, many browsers implement Element.matches
prefixed, under the non-standard name matchesSelector
. We can play safe by using something along the lines of:
function matches(el, selector) {
return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
};
matches(el, '.my-class');
// $(el).text();
el.textContent
// $(el).text(string);
el.textContent = string;
There's also innerText
and outerText
:
innerText
was non-standard, whiletextContent
was standardised earlier.innerText
returns the visible text contained in a node, whiletextContent
returns the full text. For example, on the following element:<span>Hello <span style="display: none;">World</span></span>
,innerText
will return 'Hello', whiletextContent
will return 'Hello World'. As a result,innerText
is much more performance-heavy: it requires layout information to return the result.
Here is the official warning for innerText
: This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behavior may change in the future.
// $('<div>').append($(el).clone()).html();
el.outerHTML;
// $(el).replaceWith(string);
el.outerHTML = string;
// $(el).html();
el.innerHTML
// $(el).html(string);
el.innerHTML = string;
// $(el).empty();
el.innerHTML = '';
// $(el).attr('tabindex');
el.getAttribute('tabindex');
// $(el).attr('tabindex', 3);
el.setAttribute('tabindex', 3);
Since elements are just objects, most of the times we can directly access (and set) their properties:
const oldId = el.id;
el.id = 'foo';
Some other properties we can access directly:
node.href;
node.checked;
node.disabled;
node.selected;
For data attributes we can either use el.getAttribute('data-something')
or resort to the dataset
object:
// $(el).data('camelCaseValue');
string = element.dataset.camelCaseValue;
// $(el).data('camelCaseValue', 'foo');
element.dataset.camelCaseValue = 'foo';
// $(el).css('background-color', '#3cca5e');
el.style.backgroundColor = '#3cca5e';
// $(el).hide();
el.style.display = 'none';
// $(el).show();
el.style.display = '';
To get the values of all CSS properties for an element you should use window.getComputedStyle(element)
instead:
// $(el).css(ruleName);
getComputedStyle(el)[ruleName];
// $(el).addClass('foo');
el.classList.add('foo');
// $(el).removeClass('foo');
el.classList.remove('foo');
// $(el).toggleClass('foo');
el.classList.toggle('foo');
// $(el).hasClass('foo');
el.classList.contains('foo');
// $(el).outerHeight();
el.offsetHeight
// $(el).outerWidth();
el.offsetWidth
// $(el).position();
{ left: el.offsetLeft, top: el.offsetTop }
// $(el).offset();
const rect = el.getBoundingClientRect();
{
top: rect.top + document.body.scrollTop,
left: rect.left + document.body.scrollLeft
}
// $(el).on(eventName, eventHandler);
el.addEventListener(eventName, eventHandler);
// $(el).off(eventName, eventHandler);
el.removeEventListener(eventName, eventHandler);
If working with a collection of elements, you can bind an event handler to each one of them by using a loop:
// $('a').on(eventName, eventHandler);
const links = document.querySelectorAll('a');
[].forEach.call(links, function (link) {
link.addEventListener(eventName, eventHandler);
});
Can add to higher element and use 'matches' to see if specific child was clicked (similar to jQuery's .on
):
// $('ul').on('click', 'li > a', eventHandler);
const el = document.querySelector('ul');
el.addEventListener('click', event => {
if (event.target.matches('li')) {
// event handling logic
}
});
var node = document.getElementById('my-node');
var onClick = function (event) {
// this = element
// can filter by target = event delegation
if (!event.target.matches('.tab-header')) {
return;
}
// stop the default browser behaviour
event.preventDefault();
// stop the event from bubbling up the dom
event.stopPropagation();
// other listeners on this node will not fire
event.stopImmediatePropagation();
};
node.addEventListener('click', onClick);
node.removeEventListener('click', onClick);
var anchor = document.getElementById('my-anchor');
var event = new Event('click');
anchor.dispatchEvent(event);
// $(el).fadeIn();
function fadeIn(el) {
el.style.opacity = 0;
var last = +new Date();
var tick = function () {
el.style.opacity = +el.style.opacity + (new Date() - last) / 400;
last = +new Date();
if (+el.style.opacity < 1) {
(window.requestAnimationFrame && requestAnimationFrame(tick)) || setTimeout(tick, 16);
}
};
tick();
}
Or, if you are only supporting IE10+:
el.classList.add('show');
el.classList.remove('hide');
.show {
transition: opacity 400ms;
}
.hide {
opacity: 0;
}
Note this is not necessary if you are using querySelector(All)
.
// $(selector).each(function (index, element) { ... });
const elements = document.getElementsByClassName(selector);
[].forEach.call(elements, function (element, index, arr) { ... });
//or
Array.prototype.forEach.call(elements, function (element, index, array) { ... });
// or
Array.from(elements).forEach((element, index, arr) => { ... }); // ES6 ⚠️
Same concept applies to filtering:
// $(selector).filter(":even");
const elements = document.getElementsByClassName(selector);
[].filter.call(elements, function (element, index, arr) {
return index % 2 === 0;
});
Recall that :even
and :odd
use 0-based indexing.
Another filtering example:
var nodeList = document.getElementsByClassName('my-class');
var filtered = Array.prototype.filter.call(nodeList, function (item) {
return item.innerText.indexOf('Item') !== -1;
});
// $.proxy(fn, context);
fn.bind(context);
// $.parseJSON(string);
JSON.parse(string);
// $.trim(string);
string.trim();
// $.type(obj);
Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1').toLowerCase();
(function () {
function addWindowEvent(event, fn) {
var old = window[event];
if (typeof old !== 'function') {
window[event] = fn;
return;
}
window[event] = function () {
old.apply(window, arguments);
fn.apply(window, arguments);
}
}
window.addOnLoad = function (fn) {
addWindowEvent('onload', fn)
}
window.addOnError = function (fn) {
addWindowEvent('onerror', fn)
}
})();
Despite its name, XMLHttpRequest
can be used to retrieve any type of data, not just XML, and it supports protocols other than HTTP (including file and ftp).
Getting data from the server:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/url', true);
xhr.onload = function () {
if (this.status === 200) {
console.log('success!');
} else {
console.log('failed', this.status);
}
};
xhr.send();
Posting data back to the server:
var xhrPost = new XMLHttpRequest();
xhrPost.open('POST', '/url/post', true);
xhrPost.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhrPost.onload = function () {};
xhrPost.send();
- AJAX: Axios, Superagent
- Animations: Animate.css, Move.js
- Working with arrays, numbers, objects, strings, etc.: Lodash