Техники работы с DOM: родительские, дочерние и соседние элементы. Манипуляции с DOM на чистом JavaScript Вставка контента в DOM

Сложные и тяжелые веб-приложения стали обычными в наши дни. Кроссбраузерные и простые в использовании библиотеки типа jQuery с их широким функционалом могут сильно помочь в манипулировании DOM на лету. Поэтому неудивительно, что многие разработчики использую подобные библиотеки чаще, чем работают с нативным DOM API, с которым было немало проблем . И хотя различия в браузерах по-прежнему остаются проблемой, DOM находится сейчас в лучшей форме, чем 5-6 лет назад , когда jQuery набирал популярность.

В этой статье я продемонстрирую возможности DOM по манипулированию HTML, сфокусировавшись на отношения родительских, дочерних и соседних элементов. В заключении я дам данные о поддержке этих возможностей в браузерах, но учитывайте, что библиотека типа jQuery по-прежнему остается хорошей опцией в силу наличия багов и непоследовательностей в реализации нативного функционала.

Подсчет дочерних узлов

Для демонстрации я буду использовать следующую разметку HTML, в течение статьи мы ее несколько раз изменим:

  • 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

Как видите, результаты одинаковые, хотя техники используются разные. В первом случае я использую свойство children . Это свойство только для чтения, оно возвращает коллекцию элементов HTML, находящихся внутри запрашиваемого элемента; для подсчета их количества я использую свойство length этой коллекции.

Во втором примере я использую метод childElementCount , который мне кажется более аккуратным и потенциально более поддерживаемым способом (подробнее обсудим это позже, я не думаю, что у вас возникнут проблемы с пониманием того, что он делает).

Я мог бы попытаться использовать childNodes.length (вместо children.length), но посмотрите на результат:

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

Он возвращает 13, потому что childNodes это коллекция всех узлов, включая пробелы - учитывайте это, если вам важна разница между дочерними узлами и дочерними узлами-элементами.

Проверка существования дочерних узлов

Для проверки наличия у элемента дочерних узлов я могу использовать метод hasChildNodes() . Метод возвращает логическое значение, сообщающие об их наличии или отсутствии:

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

Я знаю, что в моем списке есть дочерние узлы, но я могу изменить HTML так, чтобы их не было; теперь разметка выглядит так:

И вот результат нового запуска hasChildNodes() :

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

Метод по прежнему возвращает true . Хотя список не содержит никаких элементов, в нем есть пробел, являющийся валидным типом узла. Данный метод учитывает все узлы, не только узлы-элементы. Чтобы hasChildNodes() вернул false нам надо еще раз изменить разметку:

И теперь в консоль выводится ожидаемый результат:

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

Конечно, если я знаю, что могу столкнуться с пробелом, то сначала я проверю существование дочерних узлов, затем с помощью свойства nodeType определяю, есть ли среди них узлы-элементы.

Добавление и удаление дочерних элементов

Есть техника, которые можно использовать для добавления и удаления элементов из DOM. Наиболее известная из них основана на сочетании методов createElement() и appendChild() .

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

В данном случае я создаю с помощью метода createElement() и затем добавляю его к body . Очень просто и вы наверняка использовали эту технику раньше.

Но вместо вставки специально создаваемого элемента, я также могу использовать appendChild() и просто переместить существующий элемент. Предположим, у нас следующая разметка:

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

Example text

Я могу изменить место расположения списка с помощью следующего кода:

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

Итоговый DOM будет выглядеть следующим образом:

Example text

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

Обратите внимание, что весь список был удален со своего места (над параграфом) и затем вставлен после него перед закрывающим body . И хотя обычно метод appendChild() используется для добавления элементов созданных с помощью createElement() , он также может использоваться для перемещения существующих элементов.

Я также могу полностью удалить дочерний элемент из DOM с помощью removeChild() . Вот как удаляется наш список из предыдущего примера:

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

Теперь элемент удален. Метод removeChild() возвращает удаленный элемент и я могу его сохранить на случай, если он потребуется мне позже.

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

Таке существует метод ChildNode.remove() , относительно недавно добавленный в спецификацию:

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

Этот метод не возвращает удаленный объект и не работает в IE (только в Edge). И оба метода удаляют текстовые узлы точно так же, как и узлы-элементы.

Замена дочерних элементов

Я могу заменить существующий дочерний элемент новым, независимо от того, существует ли этот новый элемент или я создал его с нуля. Вот разметка:

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

Как видите, метод replaceChild() принимает два аргумента: новый элемент и заменяемый им старый элемент.

Я также могу использовать это метод для перемещения существующего элемента. Взгляните на следующий HTML:

Example text 1

Example text 2

Example text 3

Я могу заменить третий параграф первым параграфом с помощью следующего кода:

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

Теперь сгенерированный DOM выглядит так:

Example text 2

Example text 1

Выборка конкретных дочерних элементов

Существует несколько разных способов выбора конкретного элемента. Как показано ранее, я могу начать с использования коллекции children или свойства childNodes . Но взглянем на другие варианты:

Свойства firstElementChild и lastElementChild делают именно то, чего от них можно ожидать по их названию: выбирают первый и последний дочерние элементы. Вернемся к нашей разметке:

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

Я могу выбрать первый и последний элементы с помощью этих свойств:

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

Я также могу использовать свойства previousElementSibling и nextElementSibling , если я хочу выбрать дочерние элементы, отличные от первого или последнего. Это делается сочетанием свойств firstElementChild и lastElementChild:

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

Также есть сходные свойства firstChild , lastChild , previousSibling , и nextSibling , но они учитывают все типы узлов, а не только элементы. Как правило, свойства, учитывающие только узлы-элементы полезнее тех, которые выбирают все узлы.

Вставка контента в DOM

Я уже рассматривал способы вставки элементов в DOM. Давайте перейдем к похожей теме и взглянем на новые возможности по вставке контента.

Во-первых, есть простой метод insertBefore() , он во многом похож на replaceChild() , принимает два аргумента и при этом работает как с новыми элементами, так и с существующими. Вот разметка:

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

Example Paragraph

Обратите внимание на параграф, я собираюсь сначала убрать его, а затем вставить перед списком, все одним махом:

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

В полученном HTML параграф будет перед списком и это еще один способ перенести элемент.

Example Paragraph

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

Как и replaceChild() , insertBefore() принимает два аргумента: добавляемый элемент и элемент, перед которым мы хотим его вставить.

Этот метод прост. Попробуем теперь более мощный способ вставки: метод insertAdjacentHTML() .

Как правило, когда нужно выполнить какие-либо действия с DOM, разработчики используют jQuery. Однако практически любую манипуляцию с DOM можно сделать и на чистом JavaScript с помощью его DOM API.

Рассмотрим этот API более подробно:

В конце вы напишете свою простенькую DOM-библиотеку, которую можно будет использовать в любом проекте.

DOM-запросы

DOM-запросы осуществляются с помощью метода.querySelector() , который в качестве аргумента принимает произвольный СSS-селектор.

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

Он вернёт первый подходящий элемент. Можно и наоборот - проверить, соответствует ли элемент селектору:

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

Если нужно получить все элементы, соответствующие селектору, используйте следующую конструкцию:

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

Если же вы знаете, на какой родительский элемент нужно сослаться, можете просто проводить поиск среди его дочерних элементов, вместо того чтобы искать по всему коду:

Const myChildElemet = myElement.querySelector("input") // Вместо: // document.querySelector("#foo > div.bar input")

Возникает вопрос: зачем тогда использовать другие, менее удобные методы вроде.getElementsByTagName() ? Есть маленькая проблема - результат вывода.querySelector() не обновляется, и когда мы добавим новый элемент (смотрите ), он не изменится.

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

Также querySelectorAll() собирает всё в один список, что делает его не очень эффективным.

Как работать со списками?

Вдобавок ко всему у.querySelectorAll() есть два маленьких нюанса. Вы не можете просто вызывать методы на результаты и ожидать, что они применятся к каждому из них (как вы могли привыкнуть делать это с jQuery). В любом случае нужно будет перебирать все элементы в цикле. Второе - возвращаемый объект является списком элементов, а не массивом. Следовательно, методы массивов не сработают. Конечно, есть методы и для списков, что-то вроде.forEach() , но, увы, они подходят не для всех случаев. Так что лучше преобразовать список в массив:

// Использование Array.from() Array.from(myElements).forEach(doSomethingWithEachElement) // Или прототип массива (до ES6) Array.prototype.forEach.call(myElements, doSomethingWithEachElement) // Проще: .forEach.call(myElements, doSomethingWithEachElement)

У каждого элемента есть некоторые свойства, ссылающиеся на «семью».

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

Поскольку интерфейс элемента (Element) унаследован от интерфейса узла (Node), следующие свойства тоже присутствуют:

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

Первые свойства ссылаются на элемент, а последние (за исключением.parentElement) могут быть списками элементов любого типа. Соответственно, можно проверить и тип элемента:

MyElement.firstChild.nodeType === 3 // этот элемент будет текстовым узлом

Добавление классов и атрибутов

Добавить новый класс очень просто:

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

Добавление свойства для элемента происходит точно так же, как и для любого объекта:

// Получение значения атрибута const value = myElement.value // Установка атрибута в качестве свойства элемента myElement.value = "foo" // Для установки нескольких свойств используйте.Object.assign() Object.assign(myElement, { value: "foo", id: "bar" }) // Удаление атрибута myElement.value = null

Можно использовать методы.getAttibute() , .setAttribute() и.removeAttribute() . Они сразу же поменяют HTML-атрибуты элемента (в отличие от DOM-свойств), что вызовет браузерную перерисовку (вы сможете увидеть все изменения, изучив элемент с помощью инструментов разработчика в браузере). Такие перерисовки не только требуют больше ресурсов, чем установка DOM-свойств, но и могут привести к непредвиденным ошибкам.

Как правило, их используют для элементов, у которых нет соответствующих DOM-свойств, например colspan . Или же если их использование действительно необходимо, например для HTML-свойств при наследовании (смотрите ).

Добавление CSS-стилей

Добавляют их точно так же, как и другие свойства:

MyElement.style.marginLeft = "2em"

Какие-то определённые свойства можно задавать используя.style , но если вы хотите получить значения после некоторых вычислений, то лучше использовать window.getComputedStyle() . Этот метод получает элемент и возвращает CSSStyleDeclaration , содержащий стили как самого элемента, так и его родителя:

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

Изменение DOM

Можно перемещать элементы:

// Добавление element1 как последнего дочернего элемента element2 element1.appendChild(element2) // Вставка element2 как дочернего элемента element1 перед element3 element1.insertBefore(element2, element3)

Если не хочется перемещать, но нужно вставить копию, используем:

// Создание клона const myElementClone = myElement.cloneNode() myParentElement.appendChild(myElementClone)

Метод.cloneNode() принимает булевое значение в качестве аргумента, при true также клонируются и дочерние элементы.

Конечно, вы можете создавать новые элементы:

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

А затем вставлять их как было показано выше. Удалить элемент напрямую не получится, но можно сделать это через родительский элемент:

MyParentElement.removeChild(myElement)

Можно обратиться и косвенно:

MyElement.parentNode.removeChild(myElement)

Методы для элементов

У каждого элемента присутствуют такие свойства, как.innerHTML и.textContent , они содержат HTML-код и, соответственно, сам текст. В следующем примере изменяется содержимое элемента:

// Изменяем HTML myElement.innerHTML = ` New content { el.addEventListener("change", function (event) { console.log(event.target.value) }) })

Предотвращение действий по умолчанию

Для этого используется метод.preventDefault() , который блокирует стандартные действия. Например, он заблокирует отправку формы, если авторизация на клиентской стороне не была успешной:

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

Метод.stopPropagation() поможет, если у вас есть определённый обработчик события, закреплённый за дочерним элементом, и второй обработчик того же события, закреплённый за родителем.

Как говорилось ранее, метод.addEventListener() принимает третий необязательный аргумент в виде объекта с конфигурацией. Этот объект должен содержать любые из следующих булевых свойств (по умолчанию все в значении false):

  • capture: событие будет прикреплено к этому элементу перед любым другим элементом ниже в DOM;
  • once: событие может быть закреплено лишь единожды;
  • passive: event.preventDefault() будет игнорироваться (исключение во время ошибки).

Наиболее распространённым свойством является.capture , и оно настолько распространено, что для этого существует краткий способ записи: вместо того чтобы передавать его в объекте конфигурации, просто укажите его значение здесь:

MyElement.addEventListener(type, listener, true)

Обработчики удаляются с помощью метода.removeEventListener() , принимающего два аргумента: тип события и ссылку на обработчик для удаления. Например свойство once можно реализовать так:

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

Наследование

Допустим, у вас есть элемент и вы хотите добавить обработчик событий для всех его дочерних элементов. Тогда бы вам пришлось прогнать их в цикле, используя метод myForm.querySelectorAll("input") , как было показано выше. Однако вы можете просто добавить элементы в форму и проверить их содержимое с помощью event.target .

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

И ещё один плюс данного метода заключается в том, что к новым дочерним элементам обработчик будет привязываться автоматически.

Анимация

Проще всего добавить анимацию используя CSS со свойством transition . Но для большей гибкости (например для игры) лучше подходит JavaScript.

Вызывать метод window.setTimeout() , пока анимация не закончится, - не лучшая идея, так как ваше приложение может зависнуть, особенно на мобильных устройствах. Лучше использовать window.requestAnimationFrame() для сохранения всех изменений до следующей перерисовки. Он принимает функцию в качестве аргумента, которая в свою очередь получает метку времени:

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) } }

Таким способом достигается очень плавная анимация. В своей статье Марк Браун рассуждает на данную тему.

Пишем свою библиотеку

Тот факт, что в DOM для выполнения каких-либо операций с элементами всё время приходится перебирать их, может показаться весьма утомительным по сравнению с синтаксисом jQuery $(".foo").css({color: "red"}) . Но почему бы не написать несколько своих методов, облегчающую данную задачу?

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 } } }

Псевдокод

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

Сообщение о тревоге будет:

body div.lol a.rightArrow

Как я могу получить это с помощью javascript / jquery?

Использование jQuery, как и это (за ним следует решение, которое не использует jQuery, за исключением события, намного меньше вызовов функций, если это важно):

Пример в реальном времени:

$(".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

(В живых примерах я обновил атрибут class на div как lol multi до продемонстрировать обработку нескольких классов.)

Вот решение для точного соответствия элемента.

Важно понимать, что селектор ( не является реальным ), которые показывают хром-инструменты, однозначно не идентифицируют элемент в DOM. (, например, он не будет различать список последовательных элементов span . Нет информации о позиционировании / индексировании )

$.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; };

И вы можете использовать его так:

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

Я переместил фрагмент из T.J. Кроудер к крошечному плагину jQuery. Я использовал версию jQuery, даже если он прав, что это совершенно лишние накладные расходы, но я использую его только для целей отладки, поэтому мне все равно.

Использование:

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 эта функция разрешает ошибку, связанную с текущим элементом, не отображаемым в пути

Проверьте это сейчас

$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); // }

Где $j = jQuery Variable

также решить проблему с.. в классе имя

здесь функция замены:

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

Вот родная версия JS, которая возвращает путь jQuery. Я также добавляю идентификаторы для элементов, если они есть. Это даст вам возможность сделать кратчайший путь, если вы увидите идентификатор в массиве.

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

Вот функция.

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 }



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: