Сложные и тяжелые веб-приложения стали обычными в наши дни. Кроссбраузерные и простые в использовании библиотеки типа 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:

Эти возможности поддерживаются в IE9+ и других современных браузерах:

И остается метод Node.remove(), который, как уже упоминалось, поддерживается Microsoft только начиная с браузера Edge.

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

Заключение

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