DEV Community

loading...
Cover image for A Vanilla JS Guide On Mastering the DOM

A Vanilla JS Guide On Mastering the DOM

bouhm profile image Brian Pak Updated on ・8 min read

Note: The contents of this post are intended to be introductory and does not include use of any libraries like jQuery.

JavaScript is the language we use to interact with the contents of a page, served on a browser or some other platform. What this means is you have the power to manipulate everything you see on the page. As we all know, with great power comes great understanding of DOM trees and knowing how to use selectors to get the DOM elements and handling events and, oh, responsibility.

The DOM

We can access the page contents through the Document Object Model (DOM). The DOM represents the page as a document in the form of a tree. This tree is made up of nodes which are just objects representing HTML tags such as div (referred to as elements) or just text. These objects have many properties and methods we can use for manipulation.

DOM Tree

 

Element Properties and Methods

In our code, we can use console.dir() in order to interact with the object and instead of its literal HTML you get from console.log().

Then we can fiddle around with the browser's console and view the many properties and methods of a node inside the browser:
right click on the page → inspect → console tab

console-dir-element

the tip of the element iceberg

Let's take a look at some of the useful properties and methods.

 

Nested Elements

  • element.children returns an array-like object of the element nodes nested under this element.
  • element.childNodes returns an array-like object and includes any nodes including text that is not wrapped in HTML tags.
<div id="example">
  Just text
  <p>Text in a p tag</p>
</div>
Enter fullscreen mode Exit fullscreen mode
const el = document.getElementById('example')
el.children   
// - [0] p

el.childNodes
// - [0] text
// - [1] p
Enter fullscreen mode Exit fullscreen mode

So which do you use? Generally you would probably use element.children more as you only want to manipulate elements. However, whenever you need to access content that is not a node element (not wrapped in an HTML tag) then you would use element.childNodes.

Keep in mind that any nested node will also have a parent node!

Text

  • element.textContent returns all the text content in the element.
  • This is preferred over element.innerText which returns only visible text and would exclude text hidden by css.

While this works well for changing text directly inside an element, if you were to try to change the textContent of an element with nested elements, that would affect all of the nested content. Using the same HTML example as above:

const el = document.getElementById('example')
el.textContent = "New text"
Enter fullscreen mode Exit fullscreen mode
<!-- Result: -->
<div id="example">
  New Text
</div>
Enter fullscreen mode Exit fullscreen mode

This is where using element.childNodes would come in handy, since we can access just the text without changing the other child nodes.

const el = document.getElementById('example')

el.childNodes
// -[0] text
//   - textContent: "Just text"
// -[1] p
//   - textContent: "Text in a p tag"

el.childNodes[0].textContent = "New text" 
Enter fullscreen mode Exit fullscreen mode
<!-- Result: -->
<div id="example">
  New Text
  <p>Text in a p tag</p>
</div>
Enter fullscreen mode Exit fullscreen mode

Attributes

  • element.id can be set to directly to change the id of the element.
  • element.classList returns an array-like object that can be added to with element.classList.add("hidden") or removed from with element.classList.remove("hidden").

For other elements with properties specific to that element, you can access the properties directly:

  • element.src for img elements
  • element.href for a elements

You can also generally modify any attributes (including the above).

  • element.setAttribute(attr, value)

Examples of other attributes:

  • name
  • title
  • disabled
  • data-*

Events

Events can be attached to elements (the EventTarget) via addEventListener() by listening for certain events and calling a callback function when the event is fired.

The callback functions will pass in an event parameter for that event with a lot of specific properties for the events. All of the event objects have a target property, which will give you the element on which the event was fired. It will be very useful to have this object reference since you can then access its methods and properties discussed above.

There are LOTS of DOM events but I'll just go over the basic ones (excluding deprecated events) used for creating interactive UI.

Keyboard Events

  • 'keydown' is fired when any key is pressed.
  • 'keyup' is fired when any key is released.

You can check to see if the user pressed or released a specific key by comparing the keycode value. It is recommended to use e.keyCode since e.code can vary across browers. Keycode.info is a great resource for quickly getting keycodes.

element.addEventListener('keydown', e => {
  if (e.keyCode === 17) // (e.code === "control") works as well
    // Do something    
})

Enter fullscreen mode Exit fullscreen mode

Mouse Events

  • 'click' is fired when the element has been clicked (pressed & released).
  • 'mouseover' is fired when the element is moved over the element.
element.addEventListener('click', () => {
  clickNum++
})

Enter fullscreen mode Exit fullscreen mode

Forms and Input Events

  • 'submit' is fired when the form is submitted by pressing the submit button or pressing enter on a form input.
  • 'change' is fired when the value of an input element (input, select, textarea) is changed.

It is important to note that when a form is submitted, the corresponding action will attempt a page redirect, which will trigger a page refresh. To prevent this, you must add e.preventDefault() to suppress the default behavior.

element.addEventListener('change', e => {
  newValue = e.target.value
})

Enter fullscreen mode Exit fullscreen mode
element.addEventListener('submit', e => {
  e.preventDefault()
  submitForm()
})

Enter fullscreen mode Exit fullscreen mode
e.PreventDefault()

Let's talk a bit more about e.preventDefault(). We know it can be used on the 'submit' event to prevent the default page redirect behavior. What about other behaviors?

If we wanted to listen for Arrow Key inputs, the 'up' and 'down' arrow keys will cause the page to scroll (if any). This is another example of where we can use e.preventDefault() to suppress the default behavior. It is applicable to many different elements, and can be used whenever we want more control over events. Other examples can be found here.

 

Grabbing Elements

We can use the various attributes and types of HTML elements to grab the ones we need. Here are some of the widely used as well as recommended methods I gathered from my research.

A Single Element

  • document.getElementById is the way to grab a very specific element. It will of course require an id on the element which must be unique. Assigning ids to elements should be done judiciously.

Multiple Elements

  • document.getElementsByClassName() returns an array-like object of all elements with the given class name.
  • getElementsByTagName() returns an array-like object of all elements of the given HTML tag.

By Query

  • document.querySelector() is another way to get a single element by query. It will grab the first element that matches the query.
  • document.querySelectorAll() functions the same way as querySelector() but returns all elements that matches the query.

Queries

  • You can grab an element by id 'example by #example or the first element of a class "card" by .card.
  • You can specify multiple queries with the comma to signify an OR. document.querySelector('.card, div') will grab the first element that has a class "card" or the div tag.
  • You can use CSS selectors for the query.

Nested Elements

Query Selector

We will use document.querySelector() again to provide an example of how we can grab a nested element. If we wanted to grab a div element inside the first div with the class name "card" we would use a CSS selector document.querySelector('.card div')

Iterating Children

As discussed previously, element.children returns an array-like object. What I mean by an array-like object is that it is a list of objects that can be accessed by indices, but not quite iterable like an Array. So if we wanted to use a forEach loop instead of a for loop to access its elements, we can do the following to convert it into an Array first:

Array.from(element.children).forEach(el => {
  // You now have access each element el
})
Enter fullscreen mode Exit fullscreen mode

Forms and Inputs

We could get the inputs inside a form by iterating its children, but what if we wanted to know exactly which input to get instead of keeping track of the indices?

A suggestion: Use id to grab the form, name to grab the inputs

A form element has a property elements which is an array-like but also an associative array-like object that allows us to get the form inputs by name.

<form id="form">
  <input name="name" type="text">
  <input name="number" type="number">
  <input type="submit">
</form>
Enter fullscreen mode Exit fullscreen mode
const form = document.getElementById('form')
const nameVal = form.elements['name'].value
const numberVal = form.elements['number'].value
Enter fullscreen mode Exit fullscreen mode

 

Displaying Changes To The Page

New Elements

We can create new elements with document.createElement() and specifying the tag name, for example, a div. But once we create a new element, where does it exist?

In order to actually display our newly created element, we must append it to the DOM somewhere. Typically we will grab a parent node and append it to that parent. Say for example, we have a div container and we want to add a list to it.

<div id="list-container">
</div>
Enter fullscreen mode Exit fullscreen mode
const listContainer = document.getElementById('list-container')
const list = document.createElement('ul')
const listItem1 = document.createElement('li')
const listItem2 = document.createElement('li')
const listItem3 = document.createElement('li')
listItem1.textContent = "foo"
listItem2.textContent = "bar"
listItem3.textContent = "baz"

// This would not display the list to the DOM since our <ul> is still not included in the DOM!
list.appendChild(listItem1)
list.appendChild(listItem2)
list.appendChild(listItem3)

// Now the list will be displayed since we have appended it to an element in the DOM.
listContainer.appendChild(list)
Enter fullscreen mode Exit fullscreen mode
<!-- Result: -->
<div id="list-container">
  <ul>
    <li>foo</li>
    <li>bar</li>
    <li>baz</li>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode

Updating/Replacing Elements

If we want to render something different that involves more changes than a simple text change (for example, a UI with tabs and displaying different content on tab change), then we need to do some kind of a "re-render". Since we are doing appendChild operations, we would end up appending the different content on top of the previous content.

So how do we make sure we render changes the right way? We can remove the "old" elements and then append the new ones.

<div id="list-container">
  <ul>
    <li>old foo</li>
    <li>old bar</li>
    <li>old baz</li>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode
const listContainer = document.getElementById('list-container')
const list = document.createElement('ul')

// Iterate through list's children and remove them all
while (list.firstChild)
  list.firstChild.remove()

// New content
const listItem1 = document.createElement('li')
const listItem2 = document.createElement('li')
const listItem3 = document.createElement('li')
listItem1.textContent = "foo"
listItem2.textContent = "bar"
listItem3.textContent = "baz"


// Then append the new children
list.appendChild(listItem1)
list.appendChild(listItem2)
list.appendChild(listItem3)

listContainer.appendChild(list)
Enter fullscreen mode Exit fullscreen mode
<!-- Result: -->
<div id="list-container">
  <ul>
    <li>foo</li>
    <li>bar</li>
    <li>baz</li>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode

 

What next?

After you are comfortable with vanilla JS, if you are looking for ways to refactor or write more powerful code, you may be interested in looking at a popular library called jQuery or modern front-end frameworks like React.


References:

Discussion (1)

pic
Editor guide
Collapse
kashif_shamaz profile image
Kashif Shamaz • Edited

@bouhm Pretty nice article to glance all the frequently used Vanilla JS Snippets, thanks for writing this!

Just had one observation:
In the Updating/Replacing Elements section, I think you're trying to remove existing dom elements in the List, so I think the following should be the correct code to get existing list element:

// Get the existing list element
const list = document. querySelector('#list-container ul')

// Iterate through list's children and remove them all
while (list.firstChild)
  list.firstChild.remove()
Enter fullscreen mode Exit fullscreen mode