Техники работы с 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.