Техники работы с DOM: родительские, дочерние и соседние элементы
Оригинал статьи: DOM Tips and Techniques: Parent, Child, and Siblings
Оглавление:- Подсчет дочерних узлов
- Проверка существования дочерних узлов
- Добавление и удаление дочерних элементов
- Замена дочерних элементов
- Выборка конкретных дочерних элементов
- Вставка контента в DOM
- Поддержка в браузерах
- Заключение
Сложные и тяжелые веб-приложения стали обычными в наши дни. Кроссбраузерные и простые в использовании библиотеки типа jQuery с их широким функционалом могут сильно помочь в манипулировании DOM на лету. Поэтому неудивительно, что многие разработчики использую подобные библиотеки чаще, чем работают с нативным DOM API, с которым было немало проблем. И хотя различия в браузерах по-прежнему остаются проблемой, DOM находится сейчас в лучшей форме, чем 5-6 лет назад, когда jQuery набирал популярность.
В этой статье я продемонстрирую возможности DOM по манипулированию HTML, сфокусировавшись на отношения родительских, дочерних и соседних элементов. В заключении я дам данные о поддержке этих возможностей в браузерах, но учитывайте, что библиотека типа jQuery по-прежнему остается хорошей опцией в силу наличия багов и непоследовательностей в реализации нативного функционала.
Подсчет дочерних узлов
Для демонстрации я буду использовать следующую разметку HTML, в течение статьи мы ее несколько раз изменим:
<body>
<ul id="myList">
<li>Example one</li>
<li>Example two</li>
<li>Example three</li>
<li>Example four</li>
<li>Example five</li>
<li>Example Six</li>
</ul>
</body>
Если я хочу подсчитать, сколько элементов внутри <ul>
, я могу сделать это двумя способами.
var myList = document.getElementById('myList');
console.log(myList.children.length); // 6
console.log(myList.childElementCount); // 6
See the Pen Counting Child Elements: children & childElementCount by SitePoint (@SitePoint) on CodePen.
Как видите, результаты одинаковые, хотя техники используются разные. В первом случае я использую свойство children
. Это свойство только для чтения, оно возвращает коллекцию элементов HTML, находящихся внутри запрашиваемого элемента; для подсчета их количества я использую свойство length
этой коллекции.
Во втором примере я использую метод childElementCount
, который мне кажется более аккуратным и потенциально более поддерживаемым способом (подробнее обсудим это позже, я не думаю, что у вас возникнут проблемы с пониманием того, что он делает).
Я мог бы попытаться использовать childNodes.length
(вместо children.length
), но посмотрите на результат:
var myList = document.getElementById('myList');
console.log(myList.childNodes.length); // 13
See the Pen Counting Child Nodes with childNodes.length by SitePoint (@SitePoint) on CodePen.
Он возвращает 13, потому что childNodes
это коллекция всех узлов, включая пробелы — учитывайте это, если вам важна разница между дочерними узлами и дочерними узлами-элементами.
Проверка существования дочерних узлов
Для проверки наличия у элемента дочерних узлов я могу использовать метод hasChildNodes()
. Метод возвращает логическое значение, сообщающие об их наличии или отсутствии:
var myList = document.getElementById('myList');
console.log(myList.hasChildNodes()); // true
See the Pen hasChildNodes() by SitePoint (@SitePoint) on CodePen.
Я знаю, что в моем списке есть дочерние узлы, но я могу изменить HTML так, чтобы их не было; теперь разметка выглядит так:
<body>
<ul id="myList">
</ul>
</body>
И вот результат нового запуска hasChildNodes()
:
console.log(myList.hasChildNodes()); // true
See the Pen hasChildNodes() on empty element with white space by SitePoint (@SitePoint) on CodePen.
Метод по прежнему возвращает true
. Хотя список не содержит никаких элементов, в нем есть пробел, являющийся валидным типом узла. Данный метод учитывает все узлы, не только узлы-элементы. Чтобы hasChildNodes()
вернул false
нам надо еще раз изменить разметку:
<body>
<ul id="myList"></ul>
</body>
И теперь в консоль выводится ожидаемый результат:
console.log(myList.hasChildNodes()); // false
See the Pen hasChildNodes() on empty element with no white space by SitePoint (@SitePoint) on CodePen.
Конечно, если я знаю, что могу столкнуться с пробелом, то сначала я проверю существование дочерних узлов, затем с помощью свойства nodeType определяю, есть ли среди них узлы-элементы.
Добавление и удаление дочерних элементов
Есть техника, которые можно использовать для добавления и удаления элементов из DOM. Наиболее известная из них основана на сочетании методов createElement()
и appendChild()
.
var myEl = document.createElement('div');
document.body.appendChild(myEl);
В данном случае я создаю <div>
с помощью метода createElement()
и затем добавляю его к body
. Очень просто и вы наверняка использовали эту технику раньше.
Но вместо вставки специально создаваемого элемента, я также могу использовать appendChild()
и просто переместить существующий элемент. Предположим, у нас следующая разметка:
<div id="c">
<ul id="myList">
<li>Example one</li>
<li>Example two</li>
<li>Example three</li>
<li>Example four</li>
<li>Example five</li>
<li>Example Six</li>
</ul>
<p>Example text</p>
</div>
Я могу изменить место расположения списка с помощью следующего кода:
var myList = document.getElementById('myList'),
container = document.getElementById('c');
container.appendChild(myList);
Итоговый DOM будет выглядеть следующим образом:
<div id="c">
<p>Example text</p>
<ul id="myList">
<li>Example one</li>
<li>Example two</li>
<li>Example three</li>
<li>Example four</li>
<li>Example five</li>
<li>Example Six</li>
</ul>
</div>
See the Pen Using appendChild() to change an element's location by SitePoint (@SitePoint) on CodePen.
Обратите внимание, что весь список был удален со своего места (над параграфом) и затем вставлен после него перед закрывающим body
. И хотя обычно метод appendChild()
используется для добавления элементов созданных с помощью createElement()
, он также может использоваться для перемещения существующих элементов.
Я также могу полностью удалить дочерний элемент из DOM с помощью removeChild()
. Вот как удаляется наш список из предыдущего примера:
var myList = document.getElementById('myList'),
container = document.getElementById('c');
container.removeChild(myList);
See the Pen Using removeChild() by SitePoint (@SitePoint) on CodePen.
Теперь элемент удален. Метод removeChild()
возвращает удаленный элемент и я могу его сохранить на случай, если он потребуется мне позже.
var myOldChild = document.body.removeChild(myList);
document.body.appendChild(myOldChild);
See the Pen Using removeChild() to move an element by SitePoint (@SitePoint) on CodePen.
Таке существует метод ChildNode.remove()
, относительно недавно добавленный в спецификацию:
var myList = document.getElementById('myList');
myList.remove();
See the Pen Using childNode.remove() by SitePoint (@SitePoint) on CodePen.
Этот метод не возвращает удаленный объект и не работает в IE (только в Edge). И оба метода удаляют текстовые узлы точно так же, как и узлы-элементы.
Замена дочерних элементов
Я могу заменить существующий дочерний элемент новым, независимо от того, существует ли этот новый элемент или я создал его с нуля. Вот разметка:
<p id="par">Example Text</p>
И JavaScript:
var myPar = document.getElementById('par'),
myDiv = document.createElement('div');
myDiv.className = 'example';
myDiv.appendChild(document.createTextNode('New element text'));
document.body.replaceChild(myDiv, myPar);
See the Pen Using replaceChild() by SitePoint (@SitePoint) on CodePen.
Здесь я создал элемент, добавил текстовый узел с помощью createTextNode()
и вставил его на страницу на место параграфа. Теперь на месте первоначального параграфа следующий HTML:
<div class="example">New element text</div>
Как видите, метод replaceChild()
принимает два аргумента: новый элемент и заменяемый им старый элемент.
Я также могу использовать это метод для перемещения существующего элемента. Взгляните на следующий HTML:
<p id="par1">Example text 1</p>
<p id="par2">Example text 2</p>
<p id="par3">Example text 3</p>
Я могу заменить третий параграф первым параграфом с помощью следующего кода:
var myPar1 = document.getElementById('par1'),
myPar3 = document.getElementById('par3');
document.body.replaceChild(myPar1, myPar3);
Теперь сгенерированный DOM выглядит так:
<p id="par2">Example text 2</p>
<p id="par1">Example text 1</p>
See the Pen Using replaceChild() to swap one element for another by SitePoint (@SitePoint) on CodePen.
Выборка конкретных дочерних элементов
Существует несколько разных способов выбора конкретного элемента. Как показано ранее, я могу начать с использования коллекции children
или свойства childNodes
. Но взглянем на другие варианты:
Свойства firstElementChild
и lastElementChild
делают именно то, чего от них можно ожидать по их названию: выбирают первый и последний дочерние элементы. Вернемся к нашей разметке:
<body>
<ul id="myList">
<li>Example one</li>
<li>Example two</li>
<li>Example three</li>
<li>Example four</li>
<li>Example five</li>
<li>Example Six</li>
</ul>
</body>
Я могу выбрать первый и последний элементы с помощью этих свойств:
var myList = document.getElementById('myList');
console.log(myList.firstElementChild.innerHTML); // "Example one"
console.log(myList.lastElementChild.innerHTML); // "Example six"
See the Pen Using firstElementChild and lastElementChild by SitePoint (@SitePoint) on CodePen.
Я также могу использовать свойства 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"
See the Pen Using nextElementSibling and previousElementSibling by SitePoint (@SitePoint) on CodePen.
Также есть сходные свойства firstChild
, lastChild
, previousSibling
, и nextSibling
, но они учитывают все типы узлов, а не только элементы. Как правило, свойства, учитывающие только узлы-элементы полезнее тех, которые выбирают все узлы.
Вставка контента в DOM
Я уже рассматривал способы вставки элементов в DOM. Давайте перейдем к похожей теме и взглянем на новые возможности по вставке контента.
Во-первых, есть простой метод insertBefore()
, он во многом похож на replaceChild()
, принимает два аргумента и при этом работает как с новыми элементами, так и с существующими. Вот разметка:
<div id="c">
<ul id="myList">
<li>Example one</li>
<li>Example two</li>
<li>Example three</li>
<li>Example four</li>
<li>Example five</li>
<li>Example Six</li>
</ul>
<p id="par">Example Paragraph</p>
</div>
Обратите внимание на параграф, я собираюсь сначала убрать его, а затем вставить перед списком, все одним махом:
var myList = document.getElementById('myList'),
container = document.getElementBy('c'),
myPar = document.getElementById('par');
container.insertBefore(myPar, myList);
See the Pen Using insertBefore() by SitePoint (@SitePoint) on CodePen.
В полученном HTML параграф будет перед списком и это еще один способ перенести элемент.
<div id="c">
<p id="par">Example Paragraph</p>
<ul id="myList">
<li>Example one</li>
<li>Example two</li>
<li>Example three</li>
<li>Example four</li>
<li>Example five</li>
<li>Example Six</li>
</ul>
</div>
Как и replaceChild()
, insertBefore()
принимает два аргумента: добавляемый элемент и элемент, перед которым мы хотим его вставить.
Этот метод прост. Попробуем теперь более мощный способ вставки: метод insertAdjacentHTML()
.
var myEl = document.getElementById('el');
myEl.insertAdjacentHTML('beforebegin', '<p>New element</p>');
Этот код вставит определенную строку контента перед целевым элементом (в данном случае перед списком). Второй аргумент это строка с HTML, которую вы хотите вставить (это может быть и простой текст). Первый аргумент, это строка с одним из четырех возможных значений:
beforebegin
– Вставляет строку перед указанным элементом.afterbegin
– Вставляет строку внутри указанного элемента перед первым дочерним элементом.beforeend
– Вставляет строку внутри указанного элемента после последнего дочернего элементаafterend
– Вставляет строку после указанного элемента
Чтобы было проще понять, как работает каждое из этих значений, взгляните на комментарии в сниппете разметки. Подразумевая, что #el
div это целевой элемент, каждый комментарий показывает, где будет находится вставленный HTML.
<!-- beforebegin -->
<div id="el">
<!-- afterbegin -->
<p>Some example text</p>
<!-- beforeend -->
</div>
<!-- afterend -->
Этот пример делает понятным, что делает каждое из этих значений.
See the Pen Using insertAdjacentHTML() by SitePoint (@SitePoint) on CodePen.
Поддержка в браузерах
При обсуждении любых веб-технологий, их поддержка в браузерах всегда является важным фактором. Чтобы не ходить по граблям, ниже приводится список, что и как поддерживается:
Следующие возможности поддерживаются везде, включая старые версии IE:
- childNodes
- hasChildNodes()
- appendChild()
- removeChild()
- replaceChild()
- createTextNode()
- previousSibling
- nextSibling
- insertBefore()
- insertAdjacentHTML()
Эти возможности поддерживаются в IE9+ и других современных браузерах:
- children
- childElementCount
- firstElementChild
- lastElementChild
- previousElementSibling
- nextElementSibling
И остается метод Node.remove(), который, как уже упоминалось, поддерживается Microsoft только начиная с браузера Edge.
В общем и целом, поддержка этих возможностей в браузерах превосходна. Конечно, остаются баги и проблемы с совместимостью, поэтому не забудьте тщательно протестировать все, если вы используете эти методы и свойства.
Заключение
В идеале, возможностей языка JavaScript и DOM API должно хватать для того, чтобы все делалось просто и кроссбраузерно. В итоге, мы к этому обязательно придем, но пока эти проблемы будут продолжать решаться с помощью инструментов типа jQuery.