13 липня 2023 р.

Навігація по DOM

DOM дозволяє нам робити будь-що з елементами та їх вмістом, але спочатку нам потрібно отримати відповідний DOM об’єкт.

Усі операції з DOM починаються з об’єкта document. Це головна “точка входу” в DOM. З нього ми можемо отримати доступ до будь-якого вузла.

Ось так виглядають основні посилання, які дозволяють переміщатися між вузлами DOM:

Обговоримо їх більш детально.

Зверху: documentElement і body

Найвищі вузли дерева доступні безпосередньо як властивості document:

<html> = document.documentElement
Найвищий вузол документа – document.documentElement. В DOM він відповідає тегу <html>.
<body> = document.body
Іншим широко використовуваним вузлом DOM є елемент <body>document.body.
<head> = document.head
Тег <head> доступний як document.head.
Але є заковика: document.body може бути null

Скрипт не може отримати доступ до елемента, який не існує на момент виконання цього скрипта.

Зокрема, якщо скрипт знаходиться всередині <head>, то document.body недоступний, оскільки браузер ще не прочитав його.

Отже, у прикладі нижче перший alert виведе null:

<html>

<head>
  <script>
    alert( "Якщо у HEAD: " + document.body ); // null, ще немає <body>
  </script>
</head>

<body>

  <script>
    alert( "Якщо у BODY: " + document.body ); // HTMLBodyElement, тепер body існує
  </script>

</body>
</html>
У світі DOM null означає “не існує”

У DOM значення null означає “не існує” або “такого вузла немає”.

Дочірні вузли: childNodes, firstChild, lastChild

Відтепер ми будемо використовувати два терміни:

  • Дочірні вузли (або діти) – елементи, які є безпосередніми дітьми. Іншими словами, вони вкладені саме в цей вузол. Наприклад, <head> і <body> є дочірніми елементами <html>.
  • Нащадки – всі елементи, які вкладені в даний, включаючи дітей, їхніх дітей тощо.

Наприклад, тут <body> має дочірні <div> і <ul> (і кілька пустих текстових вузлів):

<html>
<body>
  <div>Begin</div>

  <ul>
    <li>
      <b>Information</b>
    </li>
  </ul>
</body>
</html>

…А нащадками <body> є не тільки прямі дочірні елементи <div>, <ul>, але й більш глибоко вкладені елементи, такі як <li> (дочірній елемент <ul>) та <b> (дочірній елемент <li>) – тобто усі елементи піддерева.

Колекція childNodes містить список усіх дочірніх вузлів, включаючи текстові вузли.

Наведений нижче приклад показує дочірні елементи document.body:

<html>
<body>
  <div>Початок</div>

  <ul>
    <li>Інформація</li>
  </ul>

  <div>Кінець</div>

  <script>
    for (let i = 0; i < document.body.childNodes.length; i++) {
      alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
    }
  </script>
  ...щось ще...
</body>
</html>

Зверніть увагу на цікаву деталь. Якщо ми запустимо наведений вище приклад, останнім показаним елементом буде <script>. Насправді нижче в документі є більше коду, але на момент виконання скрипту браузер його ще не прочитав, тому скрипт його не бачить.

Властивості firstChild і lastChild надають швидкий доступ до першого та останнього дочірнього вузла.

Це лише скорочення. Якщо існують дочірні вузли, то завжди вірно наступне:

elem.childNodes[0] === elem.firstChild
elem.childNodes[elem.childNodes.length - 1] === elem.lastChild

Існує також спеціальна функція elem.hasChildNodes(), що перевіряє, чи є взагалі дочірні вузли.

DOM колекції

Як бачимо, childNodes виглядає як масив. Але насправді це не масив, а скоріше колекція – спеціальний ітеративний об’єкт-псевдомасив.

Є два важливих наслідки з цього:

  1. Ми можемо використовувати for..of, щоб перебирати його:
for (let node of document.body.childNodes) {
  alert(node); // показує всі вузли з колекції
}

Це працює, бо колекція є ітерованим об’єктом (є потрібний для цього метод Symbol.iterator).

  1. Методи масиву не працюватимуть, бо колекція це не масив:
alert(document.body.childNodes.filter); // undefined (методу filter немає!)

Перший наслідок приємний, з другим можна змиритися, оскільки ми можемо використовувати Array.from для створення “справжнього” масиву з колекції, якщо нам потрібні методи масиву:

alert( Array.from(document.body.childNodes).filter ); // function
Колекції DOM доступні лише для зчитування

Колекції DOM і навіть більше – всі властивості навігації, перелічені в цьому розділі, доступні лише для зчитування.

Ми не можемо замінити дочірній елемент на щось інше, призначивши childNodes[i] = ....

Для зміни DOM потрібні інші методи. Ми розберемо їх у наступному розділі.

DOM колецкції живі

Майже всі колекції DOM, за незначними винятками, є живими. Іншими словами, вони завжди відображають поточний стан DOM.

Якщо ми зберегли посилання на elem.childNodes і після цього додамо/видалимо вузли в DOM, вони автоматично з’являться в колекції.

Не використовуйте for..in для перебору колекцій

Колекції можна перебирати за допомогою for..of. Але іноді люди намагаються використовувати для цього for..in.

Будь ласка, не треба. Цикл for..in перебирає всі властивості без виключення. А колекції мають деякі “додаткові” рідко використовувані властивості, які ми зазвичай не хочемо отримувати:

<body>
<script>
  // показує 0, 1, length, item, values і більше.
  for (let prop in document.body.childNodes) alert(prop);
</script>
</body>

Сусіди та батьківський вузол

Сусіди, або сусідні вузли – це вузли, які є нащадками одного батька.

Наприклад, тут <head> і <body> є сусідами:

<html>
  <head>...</head><body>...</body>
</html>
  • <body> вважається “наступним” або сусідом “праворуч” для <head>,
  • <head> вважається “попереднім” або сусідом “ліворуч” для <body>.

Наступний сусід знаходиться у властивості nextSibling, а попередній – у previousSibling.

Батьківський вузол доступний як parentNode.

Наприклад:

// батьком <body> є <html>
alert( document.body.parentNode === document.documentElement ); // true

// після <head> іде <body>
alert( document.head.nextSibling ); // HTMLBodyElement

// після <body> іде <head>
alert( document.body.previousSibling ); // HTMLHeadElement

Навігація лише за елементами

Властивості навігації, перераховані вище, відносяться до всіх вузлів в документі. Наприклад, у childNodes ми можемо побачити як текстові вузли, так і вузли елементів і навіть вузли коментарів, якщо вони існують.

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

Тож давайте розглянемо додатковий набір посилань, які враховують лише вузли-елементи:

Посилання подібні до наведених вище, лише із словом Element всередині:

  • children – колекція дітей, які є елементами.
  • firstElementChild, lastElementChild – перший і останній дочірні елементи.
  • previousElementSibling, nextElementSibling – сусідні елементи.
  • parentElement – батьківський елемент.
Чому parentElement? Чи може батько бути не елементом?

Властивість parentElement повертає батьківський елемент “element”, тоді як parentNode повертає батьківський “будь-який вузол”. Ці властивості зазвичай однакові: обидві вони отримують батьківський елемент.

За винятком document.documentElement:

alert( document.documentElement.parentNode ); // document
alert( document.documentElement.parentElement ); // null

Причина в тому, що кореневий вузол document.documentElement (<html>) має document як батьківський. Але він має тип document – це не елемент, тому parentNode повертає його, а parentElement ні.

Ця деталь може бути корисною, коли ми хочемо перейти від довільного елемента elem до <html>, але не до document:

while(elem = elem.parentElement) { // ідемо вгору, поки не дійдемо до <html>
  alert( elem );
}

Давайте змінимо один із прикладів вище: замінимо childNodes на children. Тепер він показує лише елементи:

<html>
<body>
  <div>Begin</div>

  <ul>
    <li>Information</li>
  </ul>

  <div>End</div>

  <script>
    for (let elem of document.body.children) {
      alert(elem); // DIV, UL, DIV, SCRIPT
    }
  </script>
  ...
</body>
</html>

Ще корисних властивостей: таблиці

До цих пір ми описали основні властивості для навігації.

Деякі типи елементів DOM можуть надавати додаткові властивості, специфічні для їх типу, для зручності.

Таблиці є чудовим прикладом цього і представляють особливо важливий випадок:

Елемент<table> підтримує (на додаток до наведених вище) такі властивості:

  • table.rows – колекція рядків <tr> таблиці.
  • table.caption/tHead/tFoot – посилання на елементи <caption>, <thead>, <tfoot>.
  • table.tBodies – колекція елементів <tbody> (за стандартом може бути багато, але завжди буде принаймні один – навіть якщо його немає у вихідному HTML, браузер помістить його в DOM).

Елементи <thead>, <tfoot>, <tbody> забезпечують властивість rows:

  • tbody.rows – колекція рядків <tr> всередині.

<tr>:

  • tr.cells – колекція клітинок <td> і <th> всередині заданого рядка <tr>.
  • tr.sectionRowIndex – позиція (індекс) заданого <tr> всередині батьківського <thead>/<tbody>/<tfoot>.
  • tr.rowIndex – номер (індекс) рядка <tr> у таблиці в цілому (враховуючи всі рядки таблиці без виключення).

<td> і <th>:

  • td.cellIndex – номер клітинки у рядку <tr>.

Приклад використання:

<table id="table">
  <tr>
    <td>one</td><td>two</td>
  </tr>
  <tr>
    <td>three</td><td>four</td>
  </tr>
</table>

<script>
  // отримати td з "two" (перший рядок, друга колонка)
  let td = table.rows[0].cells[1];
  td.style.backgroundColor = "red"; // виділити червоним
</script>

Специфікація: табличні дані.

Існують також додаткові властивості навігації для HTML-форм. Ми розглянемо їх пізніше, коли почнемо працювати з формами.

Підсумки

Для вузла DOM, ми можемо перейти до його безпосередніх сусідів за допомогою властивостей навігації.

Існує два основних набори:

  • Для всіх вузлів: parentNode, childNodes, firstChild, lastChild, previousSibling, nextSibling.
  • Лише для вузлів елементів: parentElement, children, firstElementChild, lastElementChild, previousElementSibling, nextElementSibling.

Деякі типи елементів DOM (наприклад, таблиці) надають додаткові властивості та колекції для доступу до їх вмісту.

Завдання

важливість: 5

Подивіться на цю сторінку:

<html>
<body>
  <div>Користувачи:</div>
  <ul>
    <li>Іван</li>
    <li>Петро</li>
  </ul>
</body>
</html>

Вкажіть принаймні один спосіб доступу до кожного з перелічених нижче DOM вузлів:

  • До DOM вузла <div>?
  • До DOM вузла <ul>?
  • До другого <li> (Петро)?

Існує багато способів, наприклад:

До DOM вузла <div>:

document.body.firstElementChild
// або
document.body.children[0]
// або (перший вузол -- це пробіл, тому беремо 2-й)
document.body.childNodes[1]

До DOM вузла <ul>:

document.body.lastElementChild
// або
document.body.children[1]

До другого <li> (Петро):

// отримати <ul>, а потім отримати його останній дочірній елемент
document.body.lastElementChild.lastElementChild
важливість: 5

Якщо elem – це довільний DOM елемент…

  • Чи правда що elem.lastChild.nextSibling завжди null?
  • Чи правда що elem.children[0].previousSibling завжди null?
  1. Так, це правда. Елемент elem.lastChild завжди останній, у нього немає nextSibling.
  2. Ні, це неправда, тому що elem.children[0] — перший дочірній серед елементів. Але перед ним можуть існувати вузли інших типів. Отже, previousSibling може бути, наприклад, текстовим вузлом.

Зверніть увагу: в обох випадках якщо немає дітей, то буде помилка.

Якщо дочірніх елементів немає, elem.lastChild матиме значення null, тому ми не зможемо отримати доступ до elem.lastChild.nextSibling. А колекція elem.children порожня (як порожній масив []).

важливість: 5

Напишіть код, щоб зафарбувати всі діагональні клітинки таблиці червоним кольором.

Вам потрібно буде отримати всі діагоналі <td> з <table> і розфарбувати їх за допомогою коду:

// у td має бути посилання на клітинку таблиці
td.style.backgroundColor = 'red';

Результат повинен бути таким:

Відкрити пісочницю для завдання.

Ми будемо використовувати властивості rows та cells для доступу до діагональних клітинок таблиці.

Відкрити рішення в пісочниці.

Навчальна карта