- querying and modifying the DOM,
- modifying classes and attributes,
- listening to events, and
I’ll finish off by showing you how to create your own super slim DOM-library that you can drop into any project. Along the way, you’ll learn that DOM manipulation with VanillaJS is not rocket science and that many jQuery methods in fact have direct equivalents in the native API.
So let’s get to it …
DOM Manipulation: Querying the DOM
Please note: I won’t explain the Vanilla DOM API in full detail, but only scratch the surface. In the usage examples, you may encounter methods I haven’t introduced explicitly. In this case just refer to the excellent Mozilla Developer Network for details.
The DOM can be queried using the
.querySelector() method, which takes an arbitrary CSS selector as an argument:
const myElement = document.querySelector('#foo > div.bar')
This will return the first match (depth first). Conversely, we can check if an element matches a selector:
myElement.matches('div.bar') === true
If we want to get all occurrences, we can use:
const myElements = document.querySelectorAll('.bar')
If we already have a reference to a parent element, we can just query that element’s children instead of the whole
document. Having narrowed down the context like this, we can simplify selectors and increase performance.
const myChildElemet = myElement.querySelector('input[type="submit"]') // Instead of // document.querySelector('#foo > div.bar input[type="submit"]')
Then why use those other, less convenient methods like
.getElementsByTagName() at all? Well, one important difference is that the result of
.querySelector() is not live, so when we dynamically add an element (see section 3 for details) that matches a selector, the collection won’t update.
const elements1 = document.querySelectorAll('div') const elements2 = document.getElementsByTagName('div') const newElement = document.createElement('div') document.body.appendChild(newElement) elements1.length === elements2.length // false
Another consideration is that such a live collection doesn’t need to have all of the information up front, whereas
.querySelectorAll() immediately gathers everything in a static list, making it less performant.
Working with Nodelists
Now there are two common gotchas regarding
.querySelectorAll(). The first one is that we can’t call Node methods on the result and propagate them to its elements (like you might be used from jQuery objects). Rather we have to explicitly iterate over those elements. And this is the other gotcha: the return value is a NodeList, not an Array. This means we can’t call any array methods (e.g.
.forEach) on it. We have to convert it to an array first.
// Using Array.from() Array.from(myElements).forEach(doSomethingWithEachElement) // Or prior to ES6 Array.prototype.forEach.call(myElements, doSomethingWithEachElement) // Shorthand: .forEach.call(myElements, doSomethingWithEachElement)
Each element also has a couple of rather self-explanatory read-only properties referencing the “family”, all of which are live:
myElement.children myElement.firstElementChild myElement.lastElementChild myElement.previousElementSibling myElement.nextElementSibling
myElement.childNodes myElement.firstChild myElement.lastChild myElement.previousSibling myElement.nextSibling myElement.parentNode myElement.parentElement
Where the former only reference elements, the latter (except for
.parentElement) can be any kind of node, e.g. text nodes. We can then check the type of a given node like e.g.
myElement.firstChild.nodeType === 3 // this would be a text node
As with any object, we can check a node’s prototype chain using the
myElement.firstChild.nodeType instanceof Text