Как вы уже поняли, DOM — это огромное дерево с ветвями, на которых висит множество элементов. Если выразиться более точно, элементы в DOM упорядочены иерархически (рис. 28.1), определяя то, что мы в итоге видим в браузере.
Рис. 28.1. DOM и браузер неразрывно связаны
Эта иерархия помогает нам организовывать HTML-элементы. Помимо этого, она объясняет CSS, какие стили и к чему применять. С точки же зрения JavaScript эта иерархия привносит некоторую сложность, и нам придется тратить немало времени, определяя, где именно в DOM мы находимся в данный момент и куда хотим попасть. Это станет более очевидно, когда мы взглянем на создание новых элементов или их перемещение. С этой сложностью важно научиться ладить и чувствовать себя комфортно.
Этому и будет посвящена данная глава. Чтобы понять, как с легкостью переходить от ветви к ветви, DOM предоставляет ряд свойств, которые можно совмещать с уже известными техниками.
Поехали!
Поиск пути
Прежде чем найти элементы и работать с ними, важно понять, где они находятся. Самый простой способ решить эту проблему — начать сверху и постепенно спускаться, что мы и сделаем.
Вид вершины нашей DOM представлен элементами window, document и html (рис. 28.2).
Рис. 28.2. Вид вершины этого дерева всегда одинаков
В связи с повышенной важностью этих трех элементов DOM предоставляет к ним легкий доступ посредством window, document и document.documentElement:
let windowObject = window; // хм-м….
let documentObject = document; // Это наверняка необязательно
let htmlElement = document.documentElement;
Здесь стоит отметить, что и window, и document являются глобальными свойствами. Нам не обязательно явно объявлять их подобно тому, как сделал я. Используйте их сразу.
Как только мы спускаемся ниже уровня HTML-элемента, наша DOM начинает ветвление и становится гораздо интереснее. С этого места можно использовать несколько способов навигации. Один из них мы уже видели многократно, и связан он с использованием querySelector и querySelectorAll для получения в точности тех элементов, которые нам нужны. На практике зачастую эти два метода слишком ограничены.
Иногда мы не знаем, куда именно нужно направиться. В этом случае методы querySelector и querySelectorAll уже не помогут. Нам просто нужно сесть за руль и ехать в надежде, что удастся найти то, что мы ищем. Когда дело доходит до навигации по DOM, мы зачастую будем оказываться в подобном положении. Именно здесь нам и помогут различные свойства, предлагаемые DOM, которые мы изучим далее.
Разобраться нам поможет знание того, что все элементы в DOM имеют по меньшей мере одну комбинацию родителей, братьев (соседних элементов) и потомков, на которых можно ориентироваться. Для наглядного представления посмотрите на ряд, содержащий элементы div и script, как показано на рис. 28.3.
Элементы div и script являются братьями, так как имеют одного родителя — элемент body. Элемент script не имеет потомков, но у div, напротив, они есть. Img, h1, p и div являются потомками элемента div, при этом все потомки одного родителя между собой являются братьями. Как и в реальной жизни, положение родителя, потомка и брата зависит от того, на какой части дерева мы фокусируемся. То есть практически каждый элемент в зависимости от угла обзора может выступать в различных ролях.
Для упрощения работы с этим всем у нас есть несколько свойств, на которые мы и будем полагаться. Ими являются firstChild, lastChild, parentNode, children, previousSibling и nextSibling. Просто глядя на их названия, вы можете догадаться, какую именно роль они играют. Дьявол в деталях, поэтому рассмотрим все подробно.
Рис. 28.3. Пример нашего дерева с родителями, братьями и потомками
Работа с братьями и родителями
Из всех свойств легче всего работать с относящимися к родителям и братьям, а именно parentNode, previousSibling и nextSibling. Схема на рис. 28.4 дает представление о том, как эти свойства работают.
Рис. 28.4. Связь между братьями и родителями с позиции DOM
Схема несколько перегружена, но понять, что на ней происходит, можно. Свойство parentNode указывает на родителя элемента. Свойства previousSibling и nextSibling позволяют элементу найти его предыдущего или следующего брата — если мы будем следовать по стрелкам на рисунке, то увидим это. В нижней строке nextSibling нашего img это div. previousSibling для div — это img. Обращение к parentNode в любом из этих элементов приведет вас к родителю div во втором ряду. Здесь все достаточно понятно.
Давай заведем детей!
Менее понятно, как во все это вписываются потомки. Поэтому давайте взглянем на свойства firstChild, lastChild и children, показанные на рис. 28.5.
Рис. 28.5. Представление потомков и их потомков
Свойства firstChild и lastChild относятся к первому и последнему дочерним элементам родителя. Если у родителя есть всего один потомок, как в случае с элементом body из нашего примера, тогда и firstChild, и lastChild будут указывать на одно и то же. Если у элемента нет потомков, то эти свойства будут возвращать null.
Самое хитрое из всех этих свойств — свойство children. Когда вы обращаетесь к свойству children в родителе, то по умолчанию получаете коллекцию дочерних элементов, которые у него есть. Эта коллекция не является Array, но при этом имеет некоторые присущие массиву возможности. Как и в случае с массивом, вы можете перебирать эту коллекцию или обращаться к ее потомкам по отдельности. У этой коллекции есть свойство length, которое сообщает, с каким количеством потомков взаимодействует родитель. Если у вас уже голова пошла кругом, не переживайте. Код из следующего раздела прояснят сказанное.
Складываем все воедино
Теперь, когда вы разобрались во всех важных свойствах, имеющихся для перемещения по DOM, давайте рассмотрим листинги, которые свяжут все диаграммы и слова в несколько приятных строк JavaScript.
Проверка наличия потомка
Чтобы проверить, есть ли у элемента потомок, мы можем сделать следующее:
let bodyElement = document.querySelector("body");
if (bodyElement.firstChild) {
// Делает что-нибудь интересное
}
Эта инструкция if вернет null, если потомков не существует. Мы могли бы также использовать bodyElement.lastChild или bodyElement.children.count, если бы любили много печатать, но я предпочитаю простые варианты.
Обращение ко всем потомкам
Если нужно обратиться ко всем потомкам родителя, всегда можно прибегнуть к старому доброму циклу for:
let bodyElement = document.body;
for (let i = 0; i < bodyElement.children.length; i++) {
let childElement = bodyElement.children[i];
document.writeln(childElement.tagName);
}
Обратите внимание, что мы используем свойства children и length так же, как делали бы это в случае с Array. Стоит отметить, что эта коллекция по сути не является Array. Практически все методы Array, которые мы можем захотеть использовать, не будут доступны для этой коллекции, возвращенной свойством children.
Прогулка по DOM
Последний фрагмент затрагивает все, что мы видели до сих пор. Он рекурсивно проходится по DOM и касается каждого HTML-элемента, который находит:
function theDOMElementWalker(node) {
if (node.nodeType == Node.ELEMENT_NODE) {
console.log(node.tagName);
node = node.firstChild;
while (node) {
theDOMElementWalker(node);
node = node.nextSibling;
}
}
}
Чтобы увидеть эту функцию в деле, мы просто вызываем ее, передавая узел, с которого хотим начать путь:
let texasRanger = document.querySelector("#texas");
theDOMElementWalker(texasRanger);
В этом примере мы вызываем функцию theDOMElementWalker для элемента, на который ссылается переменная texasRanger. Если вы хотите выполнить некий код для элемента, найденного этим скриптом, замените закомментированную строку на то, что хотите сделать.
КОРОТКО О ГЛАВНОМ
Нахождения пути внутри DOM — это один из тех навыков, которым должен обладать каждый JavaScript-разработчик. Этот урок предоставляет обзор доступных техник. Применение же всего этого материала на практике уже полностью ложится на вас… или хорошего друга, который поможет со всем этим разобраться. В последующих уроках мы углубимся в эту тему еще больше. Разве это не звучит захватывающе?
Появился вопрос? Обращайтесь на https://forum.kirupa.com за ответом от дружественных разработчиков, таких же, как мы с вами.
Глава 29. Создание и удаление элементов DOM
Здесь реально может заштормить. Поэтому держитесь крепче во время чтения следующих разделов:
Независимо от того, какое представление сформировалось у вас на основе предыдущих обсуждений DOM, наша DOM не обязана состоять только из HTML-элементов, существующих в разметке. Есть возможность создавать HTML-элементы прямо из воздуха и добавлять их в DOM, используя всего несколько строк JavaScript. Также есть возможность перемещать элементы, удалять их и проделывать с ними многие другие богоподобные действия. Сделайте паузу, осмыслите прочитанное, дайте ему осесть в своем сознании.
Помимо изначальной крутизны всего этого возможность динамического создания и изменения элементов в DOM является важной деталью, благодаря которой работают многие из наших любимых сайтов и приложений. Если об этом как следует подумать, то все встает на свои места. Если в нашем HTML все будет предопределено, то это сильно ограничит его возможности. Нам же нужно, чтобы содержимое изменялось и адаптировалось при поступлении новых данных, при взаимодействии со страницей, при прокручивании или при выполнении множества других действий.
В этой главе мы познакомимся с основами того, что позволяет всему этому работать. Мы рассмотрим создание элементов, удаление элементов, их переподчинение и клонирование. И эта глава будет последней, где изучаются особенности DOM. Так что можете звать друзей и надувать шары к празднику!
Поехали!
Создание элементов
Для интерактивных сайтов и приложений динамическое создание HTML-элементов и помещение их в DOM — вполне обыденная процедура. Если вы впервые слышите о том, что такое возможно, то этот раздел точно вам понравится!
Создавать элементы можно с помощью метода createElement, который работает достаточно просто. Он вызывается через объект document, и ему передается имя HTML-тега элемента, который нужно создать. В следующем фрагменте кода создается элемент абзаца, представленный буквой p:
let myElement = document.createElement("p");
Переменная myElement содержит ссылку на только что созданный элемент.
Если мы запустим приложение, добавив эту строку, то при своем выполнении она создаст элемент p. Создание элемента — это простая часть. Усилия придется приложить, чтобы сделать его веселым и ответственным членом нашей DOM. Требуется поместить этот элемент в определенное место DOM, и пока что наш динамически созданный элемент p где-то совершенно бесцельно блуждает:
Причина в том, что DOM не знает о существовании этого элемента, и, чтобы он стал полноценной частью DOM, нужно сделать две вещи:
1. Найти элемент, который выступит в качестве его родителя.
2. Использовать appendChild и присоединить элемент к этому родителю.
Легче всего это будет понять на примере, который все объединяет. Если вы хотите проработать это самостоятельно, то создайте HTML-документ и добавьте в него следующий код HTML, CSS и JS:
body {
background-color: #0E454C;
padding: 30px;
}
h1 {
color: #14FFF7;
font-size: 72px;
font-family: sans-serif;
text-decoration: underline;
}
p {
color: #14FFF7;
font-family: sans-serif;
font-size: 36px;
font-weight: bold;
}
Am I real?
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
newElement.textContent = "Or do I exist entirely in your
imagination?";
bodyElement.appendChild(newElement);
Сохраните файл и просмотрите его в браузере. Если все сработало, то вы увидите что-то похожее на следующий скриншот:
Отступим на шаг назад и разберем этот пример подробней. Все, что нам нужно для создания элемента и его добавления в DOM, находится между тегами script:
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
newElement.textContent = "Or do I exist entirely in your
imagination?";
bodyElement.appendChild(newElement);
newElement хранит ссылку на наш созданный тег p. В bodyElement хранится ссылка на элемент body. В только что созданном элементе (newElement) мы устанавливаем свойство textContent, указав, что в итоге нужно отобразить.
В конце мы берем бесцельно блуждающий newElement и присоединяем его к элементу-родителю body с помощью функции appendChild.
На рис. 29.1 показано, как выглядит DOM для нашего простого примера.
Рис. 29.1. Как выглядит DOM после выполнения приведенного кода
Следует учитывать, что функция appendChild всегда добавляет элемент после всех потомков, которые могут быть у родителя. В нашем случае элемент body уже имеет в качестве потомков элементы h1 и script. Элемент p присоединяется после них как самый новый. В итоге появлятся контроль над позицией, в которой будет размещен конкретный элемент под родителем.
Если мы хотим вставить newElement сразу после тега h1, то можем сделать это, вызвав функцию insertBefore для родителя. Функция insertBefore получает два аргумента. Первый из них является элементом-вставкой, а второй — ссылкой на брата (то есть другого потомка родителя), которому этот элемент должен предшествовать. Далее приведен измененный пример, где наш newElement помещен после элемента h1 (и перед элементом script):
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
let scriptElement = document.querySelector("script");
newElement.textContent = "I exist entirely in your imagination.";
bodyElement.insertBefore(newElement, scriptElement);
Обратите внимание, что мы вызываем insertBefore для bodyElement и указываем, что newElement должен быть вставлен перед элементом script. Наша DOM в этом случае будет выглядеть так, как показано рис. 29.2.
Рис. 29.2. Вставленный элемент находится между элементами h1 и script
Вы можете подумать, что если есть метод insertBefore, то должен быть и метод insertAfter. Но на самом деле это не так. Здесь нет встроенного способа для вставки элемента после, а не до другого элемента. Мы можем только перехитрить функцию insertBefore, сказав ей вставить элемент перед элементом, следующим за нужным. Но будет ли в этом смысл? Сначала я покажу пример и затем все объясню:
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
let h1Element = document.querySelector("h1");
newElement.textContent = "I exist entirely in your imagination.";
bodyElement.insertBefore(newElement, h1Element.nextSibling);
Обратите внимание на выделенные строки, а затем взгляните на рис. 29.3, где видно положение до и после выполнения кода.
Рис. 29.3. Трюк для имитирования действия insertAfter
Вызов hiElement.nextSibling находит элемент script. Вставка newElement перед элементом script удовлетворяет нашу цель вставить элемент после h1. А что, если нет элемента-брата, на который можно указать? В таком случае функция insertBefore достаточно умна и просто автоматически вставляет элемент в конец.
УДОБНАЯ ФУНКЦИЯ
Если по какой-то причине вам нужно все время вставлять элементы после другого потомка, то можете использовать эту функцию, чтобы немного упростить себе жизнь:
function insertAfter(target, newElement) {
target.parentNode.insertBefore(newElement,
target.nextSibling);
}
Да, я понимаю, что это окольный путь, но он работает, и весьма прилично. Вот пример этой функции в действии:
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
let h1Element = document.querySelector("h1");
newElement.textContent = "I exist entirely in your imagination.";
function insertAfter(target, element) {
target.parentNode.insertBefore(element, target.nextSibling);
}
insertAfter(bodyElement, newElement);
Можно пойти еще дальше и расширить этой функцией HTML-элемент, чтобы предоставить ее функциональность для всех HTML-элементов. В главе 19 «Расширение встроенных объектов» рассматривается, как сделать нечто подобное. Имейте в виду, что некоторые разработчики не одобряют расширение DOM, поэтому заготовьте какие-нибудь остроумные отговорки на случай, если эти некоторые начнут вам досаждать.
Более обобщенный подход к добавлению потомков родителю основан на понимании, что элементы-родители рассматривают потомков как точки входа в массив. Чтобы обратиться к этому массиву потомков, у нас есть свойства children и childNodes. Свойство children возвращает только HTML-элементы, а childNodes возвращает более обобщенные узлы, представляющие много того, до чего нам нет дела. Да, я осознаю, что повторяюсь, и вы можете пересмотреть главу 28 «Перемещение по DOM», чтобы лучше разобраться в способах точного указания на элемент.
Удаление элементов
Мне кажется, что следующая фраза принадлежит какому-то умному человеку: «То, что имеет силу создавать, имеет силу и удалять». В предыдущем разделе мы видели, как можно использовать метод createElement для создания элементов. В текущем разделе мы рассмотрим метод removeChild, который, несмотря на пугающее имя, занимается именно удалением элементов.
Взгляните на следующий фрагмент кода, который можно создать для работы со знакомым нам примером:
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
let h1Element = document.querySelector("h1");
newElement.textContent = "I exist entirely in your imagination.";
bodyElement.appendChild(newElement);
bodyElement.removeChild(newElement);
Элемент p, хранящийся в newElement, добавляется к элементу body с помощью метода appendChild. Это мы уже видели раньше. Чтобы удалить этот элемент, вызывается removeChild для элемента body и передается указатель на элемент, который нужно удалить. Конечно же, это элемент newElement. Как только будет выполнен метод removeChild, все станет так, как будто DOM никогда не знала о существовании newElement.
Главное, обратите внимание, что нужно вызывать removeChild из родителя потомка, которого мы хотим удалить. Этот метод не будет отыскивать элемент для удаления по всей DOM. А теперь предположим, что у нас нет прямого доступа к родителю элемента и тратить время на его поиск мы не хотим. При этом все равно с легкостью можно удалить его с помощью свойства parentNode:
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
let h1Element = document.querySelector("h1");
newElement.textContent = "I exist entirely in your imagination.";
bodyElement.appendChild(newElement);
newElement.parentNode.removeChild(newElement);
В этом варианте мы удаляем newElement, вызывая removeChild для его родителя, указав newElement.parentNode. Выглядит замысловато, но работает.
Теперь познакомимся с новым и лучшим способом удаления элемента, подразумевающим прямой вызов метода remove для элемента, который нужно удалить. Вот пример его использования:
let newElement = document.createElement("p");
let bodyElement = document.querySelector("body");
let h1Element = document.querySelector("h1");
newElement.textContent = "I exist entirely in your imagination.";
bodyElement.appendChild(newElement);
newElement.remove();
Я не собираюсь заканчивать тему удаления элементов на этом методе remove. Почему? Все дело в поддержке браузера. Этот подход все еще нов, поэтому более старые версии браузеров вроде Internet Explorer его не поддерживают. Если для вас принципиальна поддержка IE, то подойдут другие рассмотренные подходы.
Если вы ищите универсальный способ удаления элементов, то функция removeChild, несмотря на ее причуды, весьма эффективна. Если нужен более прямолинейный способ, присмотритесь к remove. Оба этих подхода успешно справляются с удалением элементов DOM, включая те, что были изначально созданы в разметке. При этом мы не ограничены возможностью удаления динамически добавленных элементов. Если удаляемый элемент DOM имеет несколько уровней потомков и их потомков, то все они будут также удалены.
Клонирование элементов
По мере продвижения эта глава становится все запутаннее, но, к счастью, мы уже дошли до последнего раздела. Оставшаяся техника управления DOM, о которой стоит знать, связана с клонированием элементов, а именно с созданием их идентичных копий:
Клонирование производится с помощью вызова функции cloneNode для нужного элемента с аргументом true или false — это определяет, хотим мы клонировать только сам элемент или еще и всех его потомков. Вот как будет выглядеть код для клонирования элемента (и добавления его в DOM):
let bodyElement = document.querySelector("body");
let item = document.querySelector("h1");
let clonedItem = item.cloneNode(false);
// добавление клонированного элемента в DOM
bodyElement.appendChild(clonedItem);
Как только клонированные элементы будут добавлены в DOM, можно применить уже изученные нами техники. Клонирование элементов является весьма важной функцией, поэтому давайте перейдем от рассмотрения фрагмента к более полному примеру:
body {
background-color: #60543A;
padding: 30px;
}
h1 {
color: #F2D492;
font-size: 72px;
font-family: sans-serif;
text-decoration: underline;
}
p {
color: #F2D492;
font-family: sans-serif;
font-size: 36px;
font-weight: bold;
}
Am I real?
let bodyElement = document.querySelector("body");
let textElement = document.querySelector(".message");
setInterval(sayWhat, 1000);
function sayWhat() {
let clonedText = textElement.cloneNode(true);
bodyElement.appendChild(clonedText);
}
Если вы поместите весь этот код в HTML-документ и просмотрите его в браузере, то увидите нечто напоминающее недавний пример:
Но спустя пару секунд вы заметите, что этот пример несколько отличается — тем, что его сообщение продолжает повторяться:
Секрет происходящего кроется в самом коде. Давайте вернемся назад, взглянем на код внутри тега script и попытаемся разобраться в происходящем:
let bodyElement = document.querySelector("body");
let textElement = document.querySelector(".message");
На самом верху есть переменная bodyElement, которая ссылается на элемент body в нашем HTML. Также есть переменная textElement, ссылающаяся на элемент p со значением класса message. Здесь нет ничего необычного.
А дальше уже интереснее. Есть функция-таймер setInterval, вызывающая функцию sayWhat каждые 1000 миллисекунд (1 секунду):
setInterval(sayWhat, 1000);
Сам процесс клонирования происходит внутри функции sayWhat:
function sayWhat() {
let clonedText = textElement.cloneNode(true);
bodyElement.appendChild(clonedText);
}
Мы вызываем cloneNode для textElement. В результате этого создается копия textElement, которая хранится как часть переменной clonedText. Последним шагом добавляем созданный элемент в DOM, чтобы он начал отображаться. Благодаря функции setTimer весь код после sayWhat повторяется и продолжает добавлять клонированный элемент на страницу.
Вы могли заметить, что мы клонируем следующий элемент абзаца:
В коде же мы указали следующее:
let clonedText = textElement.cloneNode(true);
Мы вызываем cloneNode с флагом true, обозначая, что хотим клонировать и всех потомков. Зачем? Ведь у нашего элемента абзаца, кажется, нет потомков. Что ж, именно здесь и проявляется различие между elements и nodes. Тег абзаца не имеет дочерних elements, но текст, обернутый тегом p, является дочерним node. Этот нюанс важно учитывать, когда вы клонируете что-либо и в итоге получаете не то, что хотели, указав, что потомков клонировать не нужно.
КОРОТКО О ГЛАВНОМ
Подытожим: DOM можно не только использовать, но и всячески изменять. Мы уже мимоходом обсуждали, как все в DOM может быть изменено, но именно здесь впервые увидели глубину и ширину доступных изменений, которые можно производить с помощью таких методов, как createElement, removeElement, remove и cloneNode.
Изучив весь этот материал, вы сможете начать с абсолютно чистой страницы и с помощью всего нескольких строк JavaScript-кода заполнить ее всем необходимым:
let bodyElement = document.querySelector("body");
let h1Element = document.createElement("h1");
h1Element.textContent = "Do they speak English
in 'What'?";
bodyElement.appendChild(h1Element);
let pElement = document.createElement("p");
pElement.textContent = "I am adding some text here…
like a boss!";
bodyElement.appendChild(pElement);