DOM manipulation in pure JavaScript. DOM Techniques: Parents, Children, and Neighbors Inserting Content into the DOM

Typically, developers use jQuery when they need to do something with the DOM. However, almost any DOM manipulation can be done in pure JavaScript using its DOM API.

Let's look at this API in more detail:

In the end, you will write your own simple DOM library that can be used in any project.

DOM queries

DOM queries are performed using the .querySelector() method, which takes an arbitrary CSS selector as an argument.

Const myElement = document.querySelector("#foo > div.bar")

It will return the first matching element. You can do the opposite - check whether the element matches the selector:

MyElement.matches("div.bar") === true

If you want to get all elements that match a selector, use the following construct:

Const myElements = document.querySelectorAll(".bar")

If you know which parent element you want to reference, you can simply search through its children instead of searching through the entire code:

Const myChildElemet = myElement.querySelector("input") // Instead of: // document.querySelector("#foo > div.bar input")

The question arises: why then use other, less convenient methods like .getElementsByTagName() ? There is a small problem - the output of .querySelector() is not updated, and when we add a new element (see ), it will not change.

Const elements1 = document.querySelectorAll("div") const elements2 = document.getElementsByTagName("div") const newElement = document.createElement("div") document.body.appendChild(newElement) elements1.length === elements2.length // false

Also querySelectorAll() collects everything into one list, which makes it not very efficient.

How to work with lists?

On top of that, .querySelectorAll() has two small caveats. You can't just call methods on the results and expect them to be applied to each one (like you might be used to doing with jQuery). In any case, you will need to iterate through all the elements in a loop. Second, the returned object is a list of elements, not an array. Therefore, array methods will not work. Of course, there are methods for lists, something like .forEach() , but, alas, they are not suitable for all cases. So it's better to convert the list to an array:

// Using Array.from() Array.from(myElements).forEach(doSomethingWithEachElement) // Or array prototype (pre-ES6) Array.prototype.forEach.call(myElements, doSomethingWithEachElement) // Simpler: .forEach.call(myElements , doSomethingWithEachElement)

Each element has some properties that refer to a "family".

MyElement.children myElement.firstElementChild myElement.lastElementChild myElement.previousElementSibling myElement.nextElementSibling

Since the Element interface is inherited from the Node interface, the following properties are also present:

MyElement.childNodes myElement.firstChild myElement.lastChild myElement.previousSibling myElement.nextSibling myElement.parentNode myElement.parentElement

The first properties refer to the element, and the last ones (with the exception of .parentElement) can be lists of elements of any type. Accordingly, you can check the element type:

MyElement.firstChild.nodeType === 3 // this element will be a text node

Adding classes and attributes

Add new class very simple:

MyElement.classList.add("foo") myElement.classList.remove("bar") myElement.classList.toggle("baz")

Adding a property to an element is the same as for any object:

// Get the attribute value const value = myElement.value // Set the attribute as an element property myElement.value = "foo" // Для установки нескольких свойств используйте.Object.assign() Object.assign(myElement, { value: "foo", id: "bar" }) // Удаление атрибута myElement.value = null !}

You can use the .getAttibute() , .setAttribute() and .removeAttribute() methods. They will immediately change the element's HTML attributes (as opposed to DOM properties), which will cause a browser re-render (you can see all the changes by examining the element using the browser's developer tools). Such redraws not only require more resources than setting DOM properties, but can also lead to unexpected errors.

Typically, they are used for elements that do not have corresponding DOM properties, such as colspan . Or if their use is really necessary, for example for HTML properties when inheriting (see).

Adding CSS Styles

They are added in the same way as other properties:

MyElement.style.marginLeft = "2em"

Some specific properties can be set using .style , but if you want to get the values ​​after some calculations, then it is better to use window.getComputedStyle() . This method receives an element and returns a CSSStyleDeclaration containing the styles of both the element itself and its parent:

Window.getComputedStyle(myElement).getPropertyValue("margin-left")

Changing the DOM

You can move elements:

// Append element1 as the last child of element2 element1.appendChild(element2) // Insert element2 as the child of element1 before element3 element1.insertBefore(element2, element3)

If you don’t want to move it, but need to insert a copy, use:

// Create a clone const myElementClone = myElement.cloneNode() myParentElement.appendChild(myElementClone)

The .cloneNode() method takes a boolean value as an argument, and if true, child elements are also cloned.

Of course you can create new elements:

Const myNewElement = document.createElement("div") const myNewTextNode = document.createTextNode("some text")

And then insert them as shown above. You can't delete an element directly, but you can do it through the parent element:

MyParentElement.removeChild(myElement)

You can also contact indirectly:

MyElement.parentNode.removeChild(myElement)

Methods for elements

Each element has properties such as .innerHTML and .textContent , they contain HTML code and, accordingly, the text itself. The following example changes the content of an element:

// Change the HTML myElement.innerHTML = ` New content ( el.addEventListener("change", function (event) ( console.log(event.target.value) )) ))

Preventing Default Actions

To do this, use the .preventDefault() method, which blocks standard actions. For example, it will block form submission if client-side authorization was not successful:

MyForm.addEventListener("submit", function (event) ( const name = this.querySelector("#name") if (name.value === "Donald Duck") { alert("You gotta be kidding!") event.preventDefault() } }) !}

The .stopPropagation() method will help if you have a specific event handler assigned to a child element and a second event handler assigned to the parent for the same event.

As stated earlier, the .addEventListener() method takes an optional third argument in the form of a configuration object. This object must contain any of the following boolean properties (all set to false by default):

  • capture: the event will be attached to this element before any other element below in the DOM;
  • once: an event can be assigned only once;
  • passive: event.preventDefault() will be ignored (exception during error).

The most common property is .capture , and it's so common that there's a shortcut for it: instead of passing it in a configuration object, just pass its value here:

MyElement.addEventListener(type, listener, true)

Handlers are removed using the .removeEventListener() method, which takes two arguments: the event type and a reference to the handler to remove. For example, the once property can be implemented like this:

MyElement.addEventListener("change", function listener (event) ( console.log(event.type + " got triggered on " + this) this.removeEventListener("change", listener) ))

Inheritance

Let's say you have an element and you want to add an event handler for all its children. Then you would have to loop through them using the myForm.querySelectorAll("input") method as shown above. However, you can simply add elements to the form and check their contents using event.target .

MyForm.addEventListener("change", function (event) ( const target = event.target if (target.matches("input")) ( console.log(target.value) ) ))

And another advantage of this method is that the handler will be automatically attached to new child elements.

Animation

The easiest way to add animation is using CSS with the transition property. But for greater flexibility (for example, for games), JavaScript is better suited.

Calling the window.setTimeout() method until the animation ends is not a good idea, as your application may freeze, especially on mobile devices. It's better to use window.requestAnimationFrame() to save all changes until the next redraw. It takes a function as an argument, which in turn receives a timestamp:

Const start = window.performance.now() const duration = 2000 window.requestAnimationFrame(function fadeIn (now)) ( const progress = now - start myElement.style.opacity = progress / duration if (progress< duration) { window.requestAnimationFrame(fadeIn) } }

In this way, very smooth animation is achieved. In his article, Mark Brown discusses this topic.

Writing our own library

The fact that in the DOM you have to iterate over elements all the time to perform any operations on elements can seem quite tedious compared to jQuery's $(".foo").css((color: "red")) syntax. But why not write a few of your own methods to make this task easier?

Const $ = function $ (selector, context = document) ( const elements = Array.from(context.querySelectorAll(selector)) return ( elements, html (newHtml) ( this.elements.forEach(element => ( element.innerHTML = newHtml )) return this ), css (newCss) ( this.elements.forEach(element => ( Object.assign(element.style, newCss) )) return this ), on (event, handler, options) ( this.elements .forEach(element => ( element.addEventListener(event, handler, options) )) return this ) ) )

Pseudocode

$(".rightArrow").click(function() ( rightArrowParents = this.dom(); //.dom(); is the pseudo function ... it should show the whole alert(rightArrowParents); ));

The alarm message will be:

body div.lol a.rightArrow

How can I get this using javascript/jquery?

Using jQuery like this (followed by a solution that doesn't use jQuery except for the event, a lot less function calls if that's important):

Real time example:

$(".rightArrow").click(function() ( var rightArrowParents = ; $(this).parents().addBack().not("html").each(function() ( var entry = this.tagName .toLowerCase(); if (this.className) ( entry += "." + this.className.replace(/ /g, "."); ) rightArrowParents.push(entry); )); alert(rightArrowParents.join (" ")); return false; )); Click here

(In the live examples, I updated the class attribute on the div to be lol multi to demonstrate handling multiple classes.)

Here is a solution for exact element matching.

It is important to understand that the selector (is not real) that chrome tools show do not uniquely identify the element in the DOM. ( for example, it will not distinguish between a list of consecutive span elements. No positioning/indexing information)

$.fn.fullSelector = function () ( var path = this.parents().addBack(); var quickCss = path.get().map(function (item) ( var self = $(item), id = item .id ? "#" + item.id: "", clss = item.classList.length ? item.classList.toString().split(" ").map(function (c) ( return "." + c; )).join("") : "", name = item.nodeName.toLowerCase(), index = self.siblings(name).length ? ":nth-child(" + (self.index() + 1) + ")" : ""; if (name === "html" || name === "body") ( return name; ) return name + index + id + clss; )).join(" > ") ; return quickCss; );

And you can use it like this:

Console.log($("some-selector").fullSelector());

I moved a piece from T.J. Crowder to tiny jQuery plugin. I used the jQuery version, even if he's right that it's completely unnecessary overhead, but I'm only using it for debugging purposes so I don't care.

Usage:

Nested span Simple span Pre

// result (array): ["body", "div.sampleClass"] $("span").getDomPath(false) // result (string): body > div.sampleClass $("span").getDomPath( ) // result (array): ["body", "div#test"] $("pre").getDomPath(false) // result (string): body > div#test $("pre").getDomPath ()

Var obj = $("#show-editor-button"), path = ""; while (typeof obj.prop("tagName") != "undefined")( if (obj.attr("class"))( path = "."+obj.attr("class").replace(/\s /g , ".") + path; ) if (obj.attr("id"))( path = "#"+obj.attr("id") + path; ) path = " " +obj.prop( "tagName").toLowerCase() + path; obj = obj.parent(); ) console.log(path);

hello this function resolves the error related to the current element not being shown in the path

Check it out now

$j(".wrapper").click(function(event) ( selectedElement=$j(event.target); var rightArrowParents = ; $j(event.target).parents().not("html,body") .each(function() ( var entry = this.tagName.toLowerCase(); if (this.className) ( entry += "." + this.className.replace(/ /g, "."); )else if (this.id)( entry += "#" + this.id; ) entry=replaceAll(entry,.","."); rightArrowParents.push(entry); )); rightArrowParents.reverse(); //if(event.target.nodeName.toLowerCase()=="a" || event.target.nodeName.toLowerCase()=="h1")( var entry = event.target.nodeName.toLowerCase(); if (event.target.className) ( entry += "." + event.target.className.replace(/ /g, "."); )else if(event.target.id)( entry += "#" + event.target.id; ) rightArrowParents.push(entry); // )

Where $j = jQuery Variable

also solve the problem with .. in class name

here's the replacement function:

Function escapeRegExp(str) ( return str.replace(/([.*+?^=!:$()()|\[\]\/\\])/g, "\\$1"); ) function replaceAll(str, find, replace) ( return str.replace(new RegExp(escapeRegExp(find), "g"), replace); )

Here's a native JS version that returns the jQuery path. I also add IDs for the elements if they exist. This will give you the option to take the shortest path if you see the id in the array.

Var path = getDomPath(element); console.log(path.join(" > "));

Body > section:eq(0) > div:eq(3) > section#content > section#firehose > div#firehoselist > article#firehose-46813651 > header > h2 > span#title-46813651

Here's the function.

Function getDomPath(el) ( var stack = ; while (el.parentNode != null) ( console.log(el.nodeName); var sibCount = 0; var sibIndex = 0; for (var i = 0; i< el.parentNode.childNodes.length; i++) { var sib = el.parentNode.childNodes[i]; if (sib.nodeName == el.nodeName) { if (sib === el) { sibIndex = sibCount; } sibCount++; } } if (el.hasAttribute("id") && el.id != "") { stack.unshift(el.nodeName.toLowerCase() + "#" + el.id); } else if (sibCount >1) ( stack.unshift(el.nodeName.toLowerCase() + ":eq(" + sibIndex + ")"); ) else ( stack.unshift(el.nodeName.toLowerCase()); ) el = el.parentNode ; ) return stack.slice(1); // removes the html element)

Complex and heavy web applications have become common these days. Cross-browser and easy-to-use libraries like jQuery with their rich functionality can greatly help in manipulating the DOM on the fly. Therefore, it is not surprising that many developers use such libraries more often than working with the native DOM API, with which there were many problems. While browser differences are still an issue, the DOM is in better shape now than it was 5-6 years ago when jQuery was gaining popularity.

In this article, I'll demonstrate the HTML manipulation capabilities of the DOM, focusing on parent, child, and neighbor relationships. In conclusion, I will give information about browser support for these features, but keep in mind that a library like jQuery is still a good option due to the presence of bugs and inconsistencies in the implementation of native functionality.

Counting child nodes

For demonstration I will use the following HTML markup, we will change it several times throughout the article:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Var myList = document.getElementById("myList"); console.log(myList.children.length); // 6 console.log(myList.childElementCount); // 6

As you can see, the results are the same, although the techniques used are different. In the first case I use the children property. This is a read-only property, it returns a collection HTML elements, located inside the requested element; To count their number, I use the length property of this collection.

In the second example, I'm using the childElementCount method, which I think is a neater and potentially more maintainable way (discuss this more later, I don't think you'll have trouble understanding what it does).

I could try using childNodes.length (instead of children.length), but look at the result:

Var myList = document.getElementById("myList"); console.log(myList.childNodes.length); // 13

It returns 13 because childNodes is a collection of all nodes, including spaces - keep this in mind if you care about the difference between child nodes and child element nodes.

Checking the existence of child nodes

To check if an element has child nodes, I can use the hasChildNodes() method. The method returns a Boolean value indicating their presence or absence:

Var myList = document.getElementById("myList"); console.log(myList.hasChildNodes()); // true

I know that my list has child nodes, but I can change the HTML so that there aren't any; The markup now looks like this:

And here is the result of running hasChildNodes() again:

Console.log(myList.hasChildNodes()); // true

The method still returns true. Although the list does not contain any elements, it does contain a space, which is a valid node type. This method takes into account all nodes, not just element nodes. In order for hasChildNodes() to return false, we need to change the markup again:

And now the expected result is displayed in the console:

Console.log(myList.hasChildNodes()); // false

Of course, if I know I might encounter whitespace, I'll first check for the existence of child nodes, then use the nodeType property to determine if there are any element nodes among them.

Adding and Removing Children Elements

There are techniques you can use to add and remove elements from the DOM. The most famous of them is based on a combination of the createElement() and appendChild() methods.

Var myEl = document.createElement("div"); document.body.appendChild(myEl);

In this case, I create using the createElement() method and then add it to the body . It's very simple and you've probably used this technique before.

But instead of inserting specifically element being created, I can also use appendChild() and just move the existing element. Let's say we have the following markup:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Example text

I can change the location of the list with the following code:

Var myList = document.getElementById("myList"), container = document.getElementById("c"); container.appendChild(myList);

The final DOM will look like this:

Example text

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Notice that the entire list has been removed from its place (above the paragraph) and then inserted after it before the closing body . While the appendChild() method is typically used to add elements created with createElement() , it can also be used to move existing elements.

I can also completely remove a child element from the DOM using removeChild() . Here's how to delete our list from the previous example:

Var myList = document.getElementById("myList"), container = document.getElementById("c"); container.removeChild(myList);

The element has now been removed. The removeChild() method returns the removed element so I can save it in case I need it later.

Var myOldChild = document.body.removeChild(myList); document.body.appendChild(myOldChild);

There is also a ChildNode.remove() method that was relatively recently added to the specification:

Var myList = document.getElementById("myList"); myList.remove();

This method does not return the remote object and does not work in IE (Edge only). And both methods remove text nodes in the same way as element nodes.

Replacing child elements

I can replace an existing child element with a new one, whether that new element exists or whether I created it from scratch. Here's the markup:

Example Text

Var myPar = document.getElementById("par"), myDiv = document.createElement("div"); myDiv.className = "example"; myDiv.appendChild(document.createTextNode("New element text")); document.body.replaceChild(myDiv, myPar);

New element text

As you can see, the replaceChild() method takes two arguments: the new element and the old element it replaces.

I can also use this method to move an existing element. Take a look at the following HTML:

Example text 1

Example text 2

Example text 3

I can replace the third paragraph with the first paragraph using the following code:

Var myPar1 = document.getElementById("par1"), myPar3 = document.getElementById("par3"); document.body.replaceChild(myPar1, myPar3);

Now the generated DOM looks like this:

Example text 2

Example text 1

Selecting specific children

There are several different ways selecting a specific element. As shown earlier, I can start by using the children collection or the childNodes property. But let's look at other options:

The firstElementChild and lastElementChild properties do exactly what their names suggest they do: select the first and last child elements. Let's return to our markup:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

I can select the first and last elements using these properties:

Var myList = document.getElementById("myList"); console.log(myList.firstElementChild.innerHTML); // "Example one" console.log(myList.lastElementChild.innerHTML); // "Example six"

I can also use the previousElementSibling and nextElementSibling properties if I want to select child elements other than the first or last one. This is done by combining the firstElementChild and lastElementChild properties:

Var myList = document.getElementById("myList"); console.log(myList.firstElementChild.nextElementSibling.innerHTML); // "Example two" console.log(myList.lastElementChild.previousElementSibling.innerHTML); // "Example five"

There are also similar properties firstChild , lastChild , previousSibling , and nextSibling , but they take into account all types of nodes, not just elements. In general, properties that consider only element nodes are more useful than those that select all nodes.

Inserting content into the DOM

I've already looked at ways to insert elements into the DOM. Let's move on to similar topic and take a look at the new options for inserting content.

First, there is a simple insertBefore() method, much like replaceChild(), it takes two arguments and works with both new elements and existing ones. Here's the markup:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Example Paragraph

Notice the paragraph I'm going to remove first and then insert it before the list, all in one fell swoop:

Var myList = document.getElementById("myList"), container = document.getElementBy("c"), myPar = document.getElementById("par"); container.insertBefore(myPar, myList);

In the resulting HTML, the paragraph will appear before the list and this is another way to wrap the element.

Example Paragraph

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Like replaceChild(), insertBefore() takes two arguments: the element to be added and the element before which we want to insert it.

This method is simple. Let's now try a more powerful insertion method: the insertAdjacentHTML() method.