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

Let's look at this API in more detail:

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

DOM queries

DOM queries are made 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 if an element matches a selector:

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

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

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

If you know which parent element to refer to, you can simply search among 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 result of the output.querySelector() is not updated, and when we add 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 little quirks. You can't just call methods on results and expect them to be applied to each of them (as you might be used to doing with jQuery). In any case, you will need to iterate over all the elements in the loop. Second, the returned object is a list of elements, not an array. Therefore, array methods won't 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 an array prototype (pre-ES6) Array.prototype.forEach.call(myElements, doSomethingWithEachElement) // Easier: .forEach.call(myElements , doSomethingWithEachElement)

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

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

Because 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 former properties refer to the element, while the latter (with the exception of .parentElement) can be lists of elements of any type. Accordingly, you can check the type of the element:

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

Adding Classes and Attributes

Adding a new class is very easy:

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

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

// Get the value of the attribute const value = myElement.value // Set the attribute as a property of the element myElement.value = "(!LANG: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 trigger a browser rerender (you can see any changes by inspecting the element using the developer tools in the browser). Such redraws not only require more resources than setting DOM properties, but can also lead to unexpected errors.

They are typically used on elements that don't 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 certain properties can be set using .style , but if you want to get the values ​​after some calculations, it's better to use window.getComputedStyle() . This method takes the 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:

// Adding element1 as the last child of element2 element1.appendChild(element2) // Inserting element2 as a child of element1 before element3 element1.insertBefore(element2, element3)

If you do not want to move, but need to paste 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 , it also clones and child elements.

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 refer indirectly:

MyElement.parentNode.removeChild(myElement)

Methods on elements

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

// Change the HTML myElement.innerHTML = `

new content

beep boop beep boop

` // This removes the content myElement.innerHTML = null // Add to the HTML myElement.innerHTML += ` continue reading...

In fact, changing the HTML is a bad idea, because it loses all changes that were made earlier, and also overloads event handlers. It is better to use this method only by completely discarding all HTML and replacing it with a copy from the server. Like this:

const link = document.createElement("a") const text = document.createTextNode("continue reading...") const hr = document.createElement("hr") link.href = "foo.html" link.appendChild( text) myElement.appendChild(link) myElement.appendChild(hr)

However, this will result in two browser redraws, while .innerHTML will only result in one. You can work around this if you first add everything to the DocumentFragment , and then add the fragment you need:

Const fragment = document.createDocumentFragment() fragment.appendChild(text) fragment.appendChild(hr) myElement.appendChild(fragment)

Event handlers

One of the simplest handlers:

MyElement.onclick = function onclick (event) ( console.log(event.type + " got fired") )

But as a general rule, it should be avoided. Here, .onclick is a property of the element, and in theory you can change it, but you won't be able to add other handlers using yet another function that references the old one.

It's better to use .addEventListener() to add handlers. It takes three arguments: an event type, a function that will be called whenever it fires, and a configuration object (we'll get to that later).

MyElement.addEventListener("click", function (event) ( console.log(event.type + " got fired") )) myElement.addEventListener("click", function (event) ( console.log(event.type + " got fired again") ))

The event.target property refers to the element that the event is attached to.

And so you can access all the properties:

// The `forms` property is an array containing links to all forms const myForm = document.forms const myInputElements = myForm.querySelectorAll("input") Array.from(myInputElements).forEach(el => ( el.addEventListener("change ", function (event) ( console.log(event.target.value) )) ))

Preventing Default Actions

For this, the .preventDefault() method is used, 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 === "(!LANG:Donald Duck") { alert("You gotta be kidding!") event.preventDefault() } }) !}

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

As mentioned earlier, the .addEventListener() method takes an optional third argument as 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 only be pinned 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 shorthand for it: instead of passing it in a config object, just specify 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 of its child elements. 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 content with event.target .

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

And one more plus of this method is that the handler will be attached automatically to new child elements.

Animation

The easiest way to add animation is to use CSS with the transition property. But for more flexibility (for example, for a game), JavaScript is better suited.

Calling the window.setTimeout() method until the animation is over 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 do something with them can seem quite tedious compared to jQuery's $(".foo").css((color: "red")) syntax. But why not write some 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 with 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 matters):

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've updated the class attribute on the div to lol multi to demonstrate handling of 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 an 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 the snippet from T.J. Crowder to a tiny jQuery plugin. I've used the jQuery version, even if he's right 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 an error related to the current element not appearing 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 replace function:

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

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

varpath = 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 is 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 wide functionality can greatly help with DOM manipulation on the fly. Therefore, it is not surprising that many developers use such libraries more often than they work with the native DOM API, which had a lot of problems. And while browser differences are still a problem, 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 DOM's ability to manipulate HTML, focusing on parent, child, and neighbor relationships. I'll conclude with a breakdown of browser support for these features, but keep in mind that the jQuery type library is still a good option due to bugs and inconsistencies in the implementation of native functionality.

Counting child nodes

I'll be using the following HTML markup for the demonstration, and we'll 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 and 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 find to be a neater and potentially more maintainable way (more on that later, I don't think you'll have trouble understanding what it does).

I could try to use 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 for the Existence of Child Nodes

To check if an element has child nodes, I can use the hasChildNodes() method. Method returns boolean 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 are none; now the markup 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 considers 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 a space, I first check for the existence of child nodes, then use the nodeType property to determine if there are element nodes among them.

Adding and Removing Child Elements

There are techniques that can be used to add and remove elements from the DOM. The most famous of these is based on a combination of the createElement() and appendChild() methods.

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

In this case, I am creating

using the createElement() method and then adding it to the body . Very simple and you have probably used this technique before.

But instead of inserting a specially crafted element, I can also use appendChild() and simply move the existing element. Suppose 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

Note 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 with removeChild() . Here's how our list is removed 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 and I can store it in case I need it later.

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

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

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

This method does not return a 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 I created it from scratch. Here is 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 with 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 Child Elements

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 you would expect from their name: select the first and last child elements. Let's go back to our markup:

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

I can select the first and last elements with 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. 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 node types, 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 a similar topic and take a look at the new features for inserting content.

First, there's a simple insertBefore() method that's a lot like replaceChild() , takes two arguments, and works on new elements as well as existing ones. Here is the markup:

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

Example Paragraph

Notice the paragraph, I'm going to remove it 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 come 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 add and the element before which we want to insert it.

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