“The front frame smells so good, I can’t tear DOM with my bare hands!”

Most front-end ERS have this problem, but in keeping with the basics, ripping the DOM by hand should be a must-have skill for a front-end siege lion, which is why this article was born — the DOM isn’t that difficult, and if you can make the most of it, you’ll soon love it.

When I first entered the front-end pit three years ago, I discovered a treasure called jQuery that had a magical $function that allowed me to quickly select a DOM element or set of DOM elements and provide chain calls to reduce code redundancy. Although the term jQuery may sound corny now, “9102 years and you’re talking to me about Nokia?” . Dust to dust, but it really smells good. ** Although the decline of jQuery has been exacerbated by Vue and React in recent years, jQuery is still used on over 66 million websites worldwide, accounting for 74% of all websites in the world.

JQuery has also left a legacy, with the W3C implementing querySelector and querySelectorAll, modeled after its $function. Ironically, it is these two native methods that have greatly accelerated the demise of jQuery because they have replaced one of the former’s most common features, the quick selection of DOM elements.

Although these two new methods are a bit long to write (no problem, encapsulate them), they are really useful.

I’m going to start with the querySelector, which is pretty common now, and introduce a nice wave of DOM apis. Come on, rush!

Get the DOM element

Getting a single element

Select a single DOM element by passing any valid CSS selector to Document. querySelector:

document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')
Copy the code

Returns null if there is no specified element on the page

Get the collection of elements

Using document. QuerySelectorAll can obtain a collection of elements that its participation and document. QuerySelector a MAO. It returns a ** static NodeList **, or an empty NodeList if no element is found.

NodeList is a traversable object (aka pseudo-array). Although it is similar to an array, it is not an array. Although it can be traversed by forEach, it does not have some of the methods of an array, such as Map, Reduce, and find.

So, how does ** convert a pseudo-array into an array? **ES6 offers two convenient options for developers:

const arr = [...document.querySelectorAll('div')]
// or
const alsoArr = Array.from(document.querySelectorAll('div'))
Copy the code

In ancient times, getElementsByTagName and getElementsByClassName were used to retrieve collections of elements, but unlike querySelectorAll, they retrieve a dynamic HTMLCollection. This means that its results will always change as the DOM changes.

Local search of elements

When you need to look up elements, you don’t have to do it based on document every time. Developers can perform a local search for DOM elements on any HTMLElement:

const container = document.querySelector('#container')
container.querySelector('#target')
Copy the code

Too much typing hello!

As it turns out, every good developer is lazy. To minimize the wear and tear on my baby keyboard, HERE’s what I do:

const$=document.querySelector.bind(document)
Copy the code

Protect the mechanical keyboard, start from me.

Boy, climb the DOM tree

The topic of this article is finding DOM elements, which is a top-down process: a parent element initiates a query to its contained child elements.

But there is no API to help developers query parent elements from child elements.

Closest to the problem, MDN offered me a treasure trove: Closest.

Starting with the Element itself, the closest() method traverses parents (heading toward the document root) of the Element until it finds a node that matches the provided selectorString. Will return itself or the matching ancestor. If no such element exists, it returns null.

That is, closest looks upwards from a specific HTMLElement, finds the first parent element (or the element itself) that matches a specified CSS expression, and returns NULL if the document root is found and the target is not yet found.

Add a DOM element

If native JavaScript is used to add one or more elements to the DOM, the average developer’s gut will resist. Why? Suppose you add an A tag to the page:

<a href="/home" class="active">Home page</a>
Copy the code

Normally, you would write code like this:

const link = document.createElement('a')
link.setAttribute('href'.'/home')
link.className = 'active'
link.textContent = 'home'

// finally
document.body.appendChild(link)
Copy the code

Really trouble.

JQuery can be simplified as:

$('body').append(')
Copy the code

But, folks, it’s now possible to do this with native JavaScript:

document.body.insertAdjacentHTML(
  'beforeend'.'
)
Copy the code

This method allows you to insert any valid HTML string into a DOM element at four positions specified by the first argument to the method:

  • ‘beforeBEGIN ‘: Precedes the element
  • ‘AfterBEGIN ‘: Inside the element, before the first existing child element
  • ‘beforeend’: Inside the element, after the last existing child element
  • ‘afterend’: after the element
<! -- beforebegin -->
<div>
  <! -- afterbegin -->
  <span></span>
  <! -- beforeend -->
</div>
<! -- afterend -->
Copy the code

Comfortable.

Even better, it also has two nice brothers that allow developers to quickly insert HTML elements and strings:

// Insert HTML elements
document.body.insertAdjacentElement(
  'beforeend'.document.createElement('a'))// Insert text
document.body.insertAdjacentText('afterbegin'.'cool! ')
Copy the code

Moving DOM elements

The insertAdjacentElement method mentioned above can also be used to move existing elements, in other words: when an element is passed into the method that already exists in the document, that element will only be moved (rather than copied and moved).

If you have the following HTML:

<div class="first">
  <h1>Title</h1>
</div>

<div class="second">
  <h2>Subtitle</h2>
</div>
Copy the code

Then do something to put

after

:

const h1 = document.querySelector('h1')
const h2 = document.querySelector('h2')

h1.insertAdjacentElement('afterend', h2)
Copy the code

So we get something like this:

<div class="first">
  <h1>Title</h1>
  <h2>Subtitle</h2>
</div>

<div class="second">
	
</div>
Copy the code

Replacing DOM elements

replaceChild? This was done a few years ago. Whenever a developer needed to replace two DOM elements, they needed to retrieve their immediate parent element in addition to the required two elements:

parentNode.replaceChild(newNode, oldNode)
Copy the code

Now, developers can use replaceWith to replace two elements:

oldElement.replaceWith(newElement)
Copy the code

In terms of usage, a little more refreshing than the former.

Note that:

  1. If the newElement passed in already exists in the document, the result of the method execution is that the newElement is moved and the oldElement is replaced
  2. If the newElement passed in is a string, it replaces the original element as a TextNode

Remove the DOM element

As with the old method of replacing an element, the old method of removing an element requires obtaining the immediate parent of the target element:

const target = document.querySelector('#target')
target.parentNode.removeChild(target)
Copy the code

Now we just need to execute the remove method once on the target element:

const target = document.querySelector('#target')
target.remove()
Copy the code

Create DOM elements with HTML strings

As you may have noticed, the insertAdjacent method allows developers to insert HTML directly into the document. What if we just want to generate a DOM element for future use?

The DOMParser object’s parseFromString method satisfies this requirement. This method converts an HTML or XML string into a DOM document. That is, when we need to get the expected DOM element, we need to get the DOM element from the DOM document returned by the method:

const createSingleElement = (domString) = > {
  const parser = new DOMParser()
  return parser.parseFromString(domString, 'text/html').body.firstChild
}

// usage
const element = createSingleElement('<a href="./home">Home</a>')
Copy the code

Be a dab hand at checking the DOM

The standard DOM API provides many convenient ways for developers to examine the DOM. For example, the matches method checks whether an element matches a certain selector:

// 
      
Hello DOM!
const div = document.querySelector('div') div.matches('div') // true div.matches('.say-hi') // true div.matches('#hi') // false Copy the code

The contains method checks if an element contains another element (or if an element is a child of another element) :

// <div><h1>Title</h1></div>
// <h2>Subtitle</h2>

const$=document.querySelector.bind(document)
const div = $('div')
const h1 = $('h1')
const h2 = $('h2')

div.contains(h1)   // true
div.contains(h2)   // false
Copy the code

One trick: compareDocumentPosition

CompareDocumentPosition is a powerful API that can quickly determine the position of two DOM elements such as precede, follow, and contain. It returns an integer representing the relationship between two elements.

Still in the example above / / container.com pareDocumentPosition (h1) / / 20 h1.com pareDocumentPosition (container) / / 10 h1.compareDocumentPosition(h2) // 4 h2.compareDocumentPosition(h1) // 2Copy the code

Standard statement:

element.compareDocumentPosition(otherElement)
Copy the code

The return value is defined as follows:

  • 1: Two elements are not in the same document
  • 2: otherElement precedes element
  • 4: otherElement comes after element
  • 8: otherElement contains element
  • OtherElement is contained by an element

So why is the result of the first row in the above example 20 and 10 in the second row?

H1 is both contained by container (16) and after container (10), so 16+4=20 is the result of the statement.

DOM Observer: MutationObserver

When dealing with user interaction, there are often many changes to the DOM elements of the current page, and some scenarios require developers to listen for these changes and perform actions when triggered. MutationObserver is a browser-provided interface designed to listen for DOM changes. It is powerful enough to observe almost all changes to an element, including text changes, addition and removal of child nodes, and changes to any element attributes.

As always, if you want to construct any object, new its constructor:

const observer = new MutationObserver(callback)
Copy the code

The constructor is passed in a callback function that is executed when the DOM element being listened on changes, taking MutationRecords, the list containing all of the changes, and the Observer itself. Where each entry of MutationRecords is a change record, it is a common object containing the following common properties:

  • Type: Change type, Attributes/characterData/childList
  • Target: the DOM element that was changed
  • AddedNodes: Adds NodeList consisting of child elements
  • RemovedNodes: NodeList with removed child elements
  • AttributeName: The name of the attribute whose value changed, or null if not
  • PreviousSibling: The sibling node before the child element to be added or removed
  • NextSibling: The sibling node after the child element that is added or removed

Based on the current information, we can write a callback function:

const callback = (mutationRecords, observer) = > {
  mutationRecords.forEach({
    type,
    target,
    attributeName,
    oldValue,
    addedNodes,
    removedNodes,
  } => {
   switch(type) {
      case 'attributes':
        console.log(`attribute ${attributeName} changed`)
        console.log(`previous value: ${oldValue}`)
        console.log(`current value: ${target.getAttribite(attributeName)}`)
        break
      case 'childList':
      	console.log('child nodes changed')
        console.log('added: ${addedNodes}')
        console.log('removed: ${removedNodes}')
        break
      // ...}})}Copy the code

At this point, we have a DOM observer, and a fully available DOM callback, except for one DOM element that needs to be observed:

const target = document.querySelector('#target')
observer.observe(target, {
  attributes: true.attributeFilter: ['class'].attributesOldValue: true.childList: true,})Copy the code

In the above code, we observe the DOM element id target (the first parameter is the object to observe) by calling the observe object’s observe method. For the second element, we pass in a configuration object: Enable observation of attributes/Only observation of class attributes/passing old values of attributes when attributes change/Enable observation of lists of child elements.

The configuration object supports the following fields:

  • Attributes: Boolean, whether to listen for changes to element attributes
  • AttributeFilter: String[], an array of specific attribute names to listen on
  • AttributeOldValue: Boolean, whether the last value of the listening element’s attribute is logged and passed when the attribute changes
  • CharacterData: Boolean, whether to listen for changes in characterData contained by nodes in the target element or child element tree
  • CharacterDataOldValue: Boolean, whether to record and pass the previous value of character data when it changes
  • ChildList: Boolean, whether to listen to the target element to add or remove child elements
  • Subtree: Boolean, whether to extend the monitoring scope to all elements of the entire subtree under the target element

When the change of the target element is no longer monitored, the Observer’s Disconnect method can be called. If necessary, the Observer’s takeRecords method can be first called to remove all pending notifications from the Observer’s notification queue. And returns them to an array of MutationRecord objects:

const mutationRecords = observer.takeRecords()
callback(mutationRecords)
observer.disconnect()
Copy the code

Don’t be afraid of DOM

Although most DOM apis have long (and cumbersome to write) names, they are very powerful and generic. These apis are often designed to provide developers with the underlying building blocks on which to build more general and concise abstract logic, so from this perspective, they must provide a full name to be clear and unambiguous.

As long as these apis live up to the potential they’re supposed to live up to, what’s a few more keystrokes?

DOM is essential knowledge for every JavsScript developer because we use it almost every day. Don’t be afraid to use your primitive DOM skills and become a DOM senior engineer as soon as possible.