Skip to content
Traversing and Manipulating the DOM with VanillaJS
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
README.md

README.md

DOM Traversal and Manipulation with VanillaJS

DOM Traversal and Manipulation

DOM ready and window load

// $(document).ready(callback);
document.addEventListener('DOMContentLoaded', callback);

// $(window).load(callback);
window.addEventListener('load', callback);
window.onload = callback;

Selectors

// 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);

jQuery-like selector

function $$(selector) {
  var nodes = document.querySelectorAll(selector);
  return Array.prototype.slice.call(nodes);
}

$$('selector');

Creating elements

// $('<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');

Adding elements to the DOM

// $(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);

Traversing the DOM

// $(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);

Traversing a node list

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);

Closest

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;
    }
  }
}

Removing and replacing nodes

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;

Cloning nodes

// $(el).clone();
el.cloneNode(true);

Pass in true to also clone child nodes.

Checking if a node is empty

// $(el).is(':empty')
!el.hasChildNodes();

Emptying an element

// $(el).empty();
const el = document.getElementById('el');

while (el.firstChild) {
  el.removeChild(el.firstChild);
}

You could also do el.innerHTML = ''.

Comparing elements

// $(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');

Getting and setting text content

// $(el).text();
el.textContent

// $(el).text(string);
el.textContent = string;

There's also innerText and outerText:

  • innerText was non-standard, while textContent was standardised earlier.
  • innerText returns the visible text contained in a node, while textContent returns the full text. For example, on the following element: <span>Hello <span style="display: none;">World</span></span>, innerText will return 'Hello', while textContent 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.

Getting and setting outer/inner HTML

// $('<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 = '';

Getting and setting attributes

// $(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';

Styling an element

// $(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];

Working with CSS classes

// $(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');

Getting the position of an element

// $(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
}

Binding events

// $(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);
});

Event delegation

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
  }
});

The event object

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);

Mocking events

var anchor = document.getElementById('my-anchor');
var event = new Event('click');

anchor.dispatchEvent(event);

Animations

// $(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;
}

Looping over and filtering through collections of DOM elements

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;
});

Random utilities

// $.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();

Adding multiple window.{onload, onerror} events

(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)
  }
})();

XMLHttpRequest (XHR)

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();

Alternative libraries

Credits and further resources

You can’t perform that action at this time.