Техніки роботи з DOM: батьківські, дочірні та сусідні елементи. Маніпуляції з DOM на чистому JavaScript Підрахунок дочірніх вузлів

💖 Подобається?Поділися з друзями посиланням

Як правило, коли потрібно виконати якісь дії з DOM, розробники використовують jQuery. Однак практично будь-яку маніпуляцію з DOM можна зробити і на чистому JavaScript за допомогою його DOM API.

Розглянемо цей API докладніше:

В кінці ви напишете свою просту DOM-бібліотеку, яку можна буде використовувати в будь-якому проекті.

DOM-запити

DOM-запити здійснюються за допомогою методу.querySelector(), який як аргумент приймає довільний СSS-селектор.

Const myElement = document.querySelector("#foo > div.bar")

Він поверне перший відповідний елемент. Можна і навпаки - перевірити, чи елемент селектору відповідає:

MyElement.matches("div.bar") === true

Якщо потрібно отримати всі елементи, що відповідають селектору, використовуйте таку конструкцію:

Const myElements = document.querySelectorAll(".bar")

Якщо ж ви знаєте, на який батьківський елемент потрібно послатися, можете просто проводити пошук серед його дочірніх елементів замість того, щоб шукати по всьому коду:

Const myChildElemet = myElement.querySelector("input") // Замість: // document.querySelector("#foo > div.bar input")

Виникає питання: навіщо тоді використовувати інші, менш зручні методи.getElementsByTagName() ? Є невелика проблема - результат вывода.querySelector() не оновлюється, і коли ми додамо новий елемент (дивіться ), він не зміниться.

Const elements1 = document.querySelectorAll("div") const elements2 = document.getElementsByTagName("div") const newElement = document.createElement("div") document.body.appendChild(newElement) elements1.length === elements2.length // false

Також querySelectorAll() збирає все в один список, що робить його не дуже ефективним.

Як працювати зі списками?

На додаток до всього у.querySelectorAll() є два маленькі нюанси. Ви не можете просто викликати методи на результати і очікувати, що вони застосовуються до кожного з них (як ви могли звикнути робити це з jQuery). У будь-якому випадку потрібно буде перебирати всі елементи у циклі. Друге - об'єкт, що повертається, є списком елементів, а не масивом. Отже, методи масивів не спрацюють. Звичайно, є методи і для списків, щось на зразок forEach() , але, на жаль, вони підходять не для всіх випадків. Так що краще перетворити список на масив:

// Використання Array.from() Array.from(myElements).forEach(doSomethingWithEachElement) // Або прототип масиву (до ES6) Array.prototype.forEach.call(myElements, doSomethingWithEachElement) // Простіше: .forEachscal , doSomethingWithEachElement)

Кожен елемент має деякі властивості, що посилаються на «сім'ю».

MyElement.children myElement.firstElementChild myElement.lastElementChild myElement.previousElementSibling myElement.nextElementSibling

Оскільки інтерфейс елемента (Element) успадкований від інтерфейсу вузла (Node), такі властивості також присутні:

MyElement.childNodes myElement.firstChild myElement.lastChild myElement.previousSibling myElement.nextSibling myElement.parentNode myElement.parentElement

Перші властивості посилаються на елемент, а останні (крім.parentElement) можуть бути списками елементів будь-якого типу. Відповідно, можна перевірити і тип елемента:

MyElement.firstChild.nodeType === 3 // цей елемент буде текстовим вузлом

Додавання класів та атрибутів

Додати новий клас дуже просто:

MyElement.classList.add("foo") myElement.classList.remove("bar") myElement.classList.toggle("baz")

Додавання властивості для елемента відбувається так само, як і для будь-якого об'єкта:

// Отримання значення атрибуту const value = myElement.value // Встановлення атрибуту як властивість елемента myElement.value = "foo" // Для установки нескольких свойств используйте.Object.assign() Object.assign(myElement, { value: "foo", id: "bar" }) // Удаление атрибута myElement.value = null !}

Можна використовувати методи .getAttibute() , .setAttribute() та .removeAttribute() . Вони відразу ж поміняють HTML-атрибути елемента (на відміну від DOM-властивостей), що викличе браузерне перемальовування (ви зможете побачити всі зміни, вивчивши елемент за допомогою інструментів розробника у браузері). Такі перемальовки не тільки вимагають більше ресурсів, ніж встановлення DOM-властивостей, але можуть призвести до непередбачених помилок.

Як правило, їх використовують для елементів, які не мають відповідних DOM-властивостей, наприклад colspan . Або якщо їх використання дійсно необхідно, наприклад для HTML-властивостей при успадкування (дивіться ).

Додавання CSS-стилів

Додають їх так само, як і інші властивості:

MyElement.style.marginLeft = "2em"

Якісь певні властивості можна задавати використовуючи .style , але якщо ви хочете отримати значення після деяких обчислень, краще використовувати window.getComputedStyle() . Цей метод отримує елемент і повертає CSSStyleDeclaration , що містить стилі як елемента, так і його батька:

Window.getComputedStyle(myElement).getPropertyValue("margin-left")

Зміна DOM

Можна переміщати елементи:

// Додавання element1 як останнього дочірнього елемента element2 element1.appendChild(element2) // Вставка element2 як дочірнього елемента element1 перед element3 element1.insertBefore(element2, element3)

Якщо не хочеться переміщати, але потрібно вставити копію, використовуємо:

// Створення клону const myElementClone = myElement.cloneNode() myParentElement.appendChild(myElementClone)

Метод.cloneNode() приймає булеве значення як аргумент, при true також клонуються і дочірні елементи.

Звичайно, ви можете створювати нові елементи:

Const myNewElement = document.createElement("div") const myNewTextNode = document.createTextNode("some text")

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

MyParentElement.removeChild(myElement)

Можна звернутись і побічно:

MyElement.parentNode.removeChild(myElement)

Методи для елементів

Кожен елемент має такі властивості, як .innerHTML і .textContent , вони містять HTML-код і, відповідно, сам текст. У наступному прикладі змінюється вміст елемента:

// Змінюємо HTML myElement.innerHTML = ` New content ( el.addEventListener("change", function (event) ( console.log(event.target.value) )) )))

Запобігання стандартним діям

Для цього використовується метод preventDefault() , який блокує стандартні дії. Наприклад, він заблокує відправлення форми, якщо авторизація на стороні клієнта не була успішною:

MyForm.addEventListener("submit", function (event) ( const name = this.querySelector("#name") if (name.value === "Donald Duck") { alert("You gotta be kidding!") event.preventDefault() } }) !}

Метод.stopPropagation() допоможе, якщо у вас є певний обробник події, закріплений за дочірнім елементом, і другий обробник тієї ж події, закріплений за батьком.

Як уже говорилося, метод.addEventListener() приймає третій необов'язковий аргумент у вигляді об'єкта з конфігурацією. Цей об'єкт повинен містити будь-які з таких булевих властивостей (за умовчанням все у значенні false):

  • capture: подія буде прикріплена до цього елемента перед будь-яким іншим елементом нижче DOM;
  • once: подія може бути закріплена лише один раз;
  • passive: event.preventDefault() буде ігноруватися (виключення під час помилки).

Найбільш поширеною властивістю є .capture і воно настільки поширене, що для цього існує короткий спосіб запису: замість того, щоб передавати його в об'єкті конфігурації, просто вкажіть його значення тут:

MyElement.addEventListener(type, listener, true)

Обробники видаляються за допомогою методу. removeEventListener() , що приймає два аргументи: тип події та посилання на обробник для видалення. Наприклад властивість once можна реалізувати так:

MyElement.addEventListener("change", function listener (event) ( console.log(event.type + " got triggered on " + this) this.removeEventListener("change", listener) ))

успадкування

Допустимо, у вас є елемент і ви хочете додати обробник подій для всіх його дочірніх елементів. Тоді вам довелося прогнати їх у циклі, використовуючи метод myForm.querySelectorAll("input") , як було показано вище. Однак ви можете просто додати елементи у форму та перевірити їх вміст за допомогою event.target.

MyForm.addEventListener("change", function (event) ( const target = event.target if (target.matches("input")) ( console.log(target.value) ) ))

І ще один плюс даного методу полягає в тому, що до нових дочірніх елементів обробник буде прив'язуватися автоматично.

Анімація

Найпростіше додати анімацію використовуючи CSS із властивістю transition. Але для більшої гнучкості (наприклад, для гри) краще підходить JavaScript.

Викликати метод window.setTimeout() , доки анімація не закінчиться, - не найкраща ідея, тому що ваш додаток може зависнути, особливо на мобільних пристроях. Найкраще використовувати window.requestAnimationFrame() для збереження всіх змін до наступного перемальовування. Він приймає функцію як аргумент, яка у свою чергу отримує мітку часу:

Const start = window.performance.now() const duration = 2000 window.requestAnimationFrame(function fadeIn (now)) ( const progress = now - start myElement.style.opacity = progress / duration if (progress< duration) { window.requestAnimationFrame(fadeIn) } }

У такий спосіб досягається дуже плавна анімація. У своїй статті Марк Браун розмірковує на цю тему.

Пишемо свою бібліотеку

Той факт, що в DOM для виконання будь-яких операцій з елементами постійно доводиться перебирати їх, може здатися дуже стомлюючим у порівнянні з синтаксисом jQuery $(".foo").css((color: "red")) . Але чому б не написати кілька своїх методів, що полегшує це завдання?

Const $ = function $ (selector, context = document) ( const elements = Array.from(context.querySelectorAll(selector)) return ( elements, html (newHtml) ( this.elements.forEach(element => ( element.innerHTML = newHtml )) return this ), css (newCss) ( this.elements.forEach(element => ( Object.assign(element.style, newCss) )) return this ), on (event, handler, options) ( this.elements .forEach(element => ( element.addEventListener(event, handler, options) )) return this ) ) )

Складні та важкі веб-програми стали звичайними в наші дні. Кросбраузерні та прості у використанні бібліотеки типу jQuery з їх широким функціоналом можуть сильно допомогти в маніпулюванні DOM на льоту. Тому не дивно, що багато розробників використовують подібні бібліотеки частіше, ніж працюють з нативним DOM API, з яким було чимало проблем. І хоча відмінності в браузерах, як і раніше, залишаються проблемою, DOM знаходиться зараз у кращій формі, ніж 5-6 років тому, коли jQuery набирав популярності.

У цій статті я продемонструю можливості DOM з маніпулювання HTML, сфокусувавшись на відносини батьківських, дочірніх та сусідніх елементів. На закінчення я дам дані про підтримку цих можливостей у браузерах, але враховуйте, що бібліотека типу jQuery, як і раніше, залишається гарною опцією через наявність багів і непослідовностей у реалізації нативного функціоналу.

Підрахунок дочірніх вузлів

Для демонстрації я використовуватиму наступну розмітку HTML, Протягом статті ми її кілька разів змінимо:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Var myList = document.getElementById("myList"); console.log(myList.children.length); // 6 console.log(myList.childElementCount); // 6

Як бачите, результати однакові, хоч техніки використовуються різні. У першому випадку я використовую властивість children. Ця властивість тільки для читання, вона повертає колекцію елементів HTML, що знаходяться всередині елемента, що запитується; Для підрахунку їхньої кількості я використовую властивість length цієї колекції.

У другому прикладі я використовую метод childElementCount , який мені здається більш акуратним і потенційно більш підтримуваним способом (докладніше обговоримо це пізніше, я не думаю, що у вас виникнуть проблеми з розумінням того, що він робить).

Я міг би спробувати використати childNodes.length (замість children.length), але подивіться на результат:

Var myList = document.getElementById("myList"); console.log(myList.childNodes.length); // 13

Він повертає 13, тому що childNodes це колекція всіх вузлів, включаючи пробіли - враховуйте це, якщо вам важлива різниця між дочірніми вузлами та дочірніми вузлами-елементами.

Перевірка існування дочірніх вузлів

Для перевірки наявності елемента дочірніх вузлів я можу використовувати метод hasChildNodes() . Метод повертає логічне значення, що повідомляють про їх наявність чи відсутність:

Var myList = document.getElementById("myList"); console.log(myList.hasChildNodes()); // true

Я знаю, що в моєму списку є дочірні вузли, але я можу змінити HTML так, щоб їх не було; тепер розмітка виглядає так:

І ось результат нового запуску hasChildNodes() :

Console.log(myList.hasChildNodes()); // true

Метод як і раніше повертає true. Хоча список не містить жодних елементів, у ньому є пробіл, що є валідним типом вузла. Цей методвраховує всі вузли, як вузли-элементы. Щоб hasChildNodes() повернув false, нам треба ще раз змінити розмітку:

І тепер у консоль виводиться очікуваний результат:

Console.log(myList.hasChildNodes()); // false

Звичайно, якщо я знаю, що можу зіткнутися з пробілом, то спочатку я перевірю існування дочірніх вузлів, потім за допомогою властивості nodeType визначаю, чи серед них є вузли-елементи.

Додавання та видалення дочірніх елементів

Є техніка, які можна використовувати для додавання та видалення елементів з DOM. Найбільш відома з них заснована на поєднанні методів createElement() і appendChild().

Var myEl = document.createElement("div"); document.body.appendChild(myEl);

В даному випадку я створюю за допомогою методу createElement() і потім додаю його до body. Дуже просто і ви, напевно, використовували цю техніку раніше.

Але замість вставки спеціально створюваного елемента я також можу використовувати appendChild() і просто перемістити існуючий елемент. Припустимо, у нас наступна розмітка:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Example text

Я можу змінити розташування списку за допомогою наступного коду:

Var myList = document.getElementById("myList"), container = document.getElementById("c"); container.appendChild(myList);

Підсумковий DOM виглядатиме так:

Example text

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Зверніть увагу, що весь список був видалений зі свого місця (над параграфом) і потім вставлений після нього перед закриваючим body . І хоча зазвичай метод appendChild() використовується для додавання елементів, створених за допомогою createElement() , він також може використовуватися для переміщення існуючих елементів.

Я також можу повністю видалити дочірній елемент із DOM за допомогою removeChild() . Ось як видаляється наш список із попереднього прикладу:

Var myList = document.getElementById("myList"), container = document.getElementById("c"); container.removeChild(myList);

Тепер елемент видалено. Метод removeChild() повертає віддалений елемент і я можу його зберегти на випадок, якщо він знадобиться мені пізніше.

Var myOldChild = document.body.removeChild(myList); document.body.appendChild(myOldChild);

Таке існує метод ChildNode.remove() , відносно недавно доданий специфікацію:

Var myList = document.getElementById("myList"); myList.remove();

Цей метод не повертає віддалений об'єкт і не працює в IE (тільки в Edge). І обидва методи видаляють текстові вузли так само, як і вузли-елементи.

Заміна дочірніх елементів

Я можу замінити існуючий дочірній елемент на новий, незалежно від того, чи існує цей новий елемент або я створив його з нуля. Ось розмітка:

Example Text

Var myPar = document.getElementById("par"), myDiv = document.createElement("div"); myDiv.className = "example"; myDiv.appendChild(document.createTextNode("New element text")); document.body.replaceChild(myDiv, myPar);

New element text

Як бачите, метод replaceChild() приймає два аргументи: новий елемент і старий елемент, який він замінює.

Я також можу використовувати цей метод для переміщення наявного елемента. Погляньте на наступний HTML:

Example text 1

Example text 2

Example text 3

Я можу замінити третій пункт першим пунктом за допомогою наступного коду:

Var myPar1 = document.getElementById("par1"), myPar3 = document.getElementById("par3"); document.body.replaceChild(myPar1, myPar3);

Тепер згенерований DOM виглядає так:

Example text 2

Example text 1

Вибір конкретних дочірніх елементів

Існує декілька різних способіввибір конкретного елемента. Як показано раніше, я можу почати з використання колекції children або властивості childNodes . Але поглянемо інші варіанти:

Властивості firstElementChild і lastElementChild роблять саме те, чого від них можна очікувати за назвою: вибирають перший і останній дочірні елементи. Повернемося до нашої розмітки:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Я можу вибрати перший та останній елементи за допомогою цих властивостей:

Var myList = document.getElementById("myList"); console.log(myList.firstElementChild.innerHTML); // "Example one" console.log(myList.lastElementChild.innerHTML); // "Example six"

Я також можу використовувати властивості previousElementSibling і nextElementSibling , якщо я хочу вибрати дочірні елементи, відмінні від першого або останнього. Це робиться поєднанням властивостей першого elementChild і lastElementChild:

Var myList = document.getElementById("myList"); console.log(myList.firstElementChild.nextElementSibling.innerHTML); // "Example two" console.log(myList.lastElementChild.previousElementSibling.innerHTML); // "Example five"

Також є подібні властивості firstChild, lastChild, previousSibling, і nextSibling, але вони враховують всі типи вузлів, а не тільки елементи. Як правило, властивості, що враховують тільки вузли-елементи корисніші за ті, які вибирають всі вузли.

Вставка контенту в DOM

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

По-перше, є простий метод insertBefore() , він багато в чому схожий на replaceChild() , приймає два аргументи і при цьому працює як з новими елементами, так і з існуючими. Ось розмітка:

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Example Paragraph

Зверніть увагу на параграф, я збираюся спочатку прибрати його, а потім вставити перед списком, все одним махом:

Var myList = document.getElementById("myList"), container = document.getElementBy("c"), myPar = document.getElementById("par"); container.insertBefore(myPar, myList);

В отриманому HTML параграф буде перед списком і це ще один спосіб перенести елемент.

Example Paragraph

  • Example one
  • Example two
  • Example three
  • Example four
  • Example five
  • Example Six

Як і replaceChild() , insertBefore() приймає два аргументи: елемент, що додається, і елемент, перед яким ми хочемо його вставити.

Цей метод простий. Спробуємо тепер потужніший спосіб вставки: метод insertAdjacentHTML() .

Псевдокод

$(".rightArrow").click(function() ( rightArrowParents = this.dom(); //.dom(); is the pseudo function ... it should show the whole alert(rightArrowParents); ));

Повідомлення про тривогу буде:

body div.lol a.rightArrow

Як я можу отримати це за допомогою javascript / jquery?

Використання jQuery, як і це (за ним слідує рішення, яке не використовує jQuery, за винятком події, набагато менше викликів функцій, якщо це важливо):

Приклад у реальному часі:

$(".rightArrow").click(function() ( var rightArrowParents = ; $(this).parents().addBack().not("html").each(function() ( var entry = this.tagName) .toLowerCase(); if (this.className) ( entry += "." + this.className.replace(/ /g, "."); ) rightArrowParents.push(entry); )); alert(rightArrowParents.join (" ")); return false; )); Click here

(У живих прикладах я оновив атрибут classна div як lol multi до продемонструвати обробку кількох класів.)

Ось рішення для точної відповідності елемента.

Важливо розуміти, що селектор (не є реальним), які показують хром-інструменти, однозначно не ідентифікують елемент DOM. ( Наприклад, він не розрізнятиме список послідовних елементів span . Немає інформації про позиціонування / індексування)

$.fn.fullSelector = function () ( var path = this.parents().addBack(); var quickCss = path.get().map(function (item) ( var self = $(item), id = item .id ? "#" + item.id: "", clss = item.classList.length ? item.classList.toString().split(" ").map(function (c) ( return "." + c; )).join("") : "", name = item.nodeName.toLowerCase(), index = self.siblings(name).length ? ":nth-child(" + (self.index() + 1) + ")" : ""; if (name === "html" || name === "body") ( return name; ) return name + index + id + clss; )). ;return quickCss;);

І ви можете використовувати його так:

Console.log($("some-selector").fullSelector());

Я перемістив фрагмент із TJ. Кроудер до крихітного плагіну jQuery. Я використав версію jQuery, навіть якщо він має рацію, що це зовсім зайві накладні витрати, але я використовую його тільки для цілей налагодження, тому мені все одно.

Використання:

Nested span Simple span Pre

// result (array): ["body", "div.sampleClass"] $("span").getDomPath(false) // result (string): body > div.sampleClass $("span").getDomPath( ) // result (array): ["body", "div#test"] $("pre").getDomPath(false) // result (string): body > div#test $("pre").getDomPath ()

Var obj = $ ("# show-editor-button"), path = ""; while (typeof obj.prop("tagName") != "undefined")( if (obj.attr("class"))( path = "."+obj.attr("class").replace(/\s /g , ".") + path; ) if (obj.attr("id"))( path = "#"+obj.attr("id") + path; ) path = " " +obj.prop( "tagName").toLowerCase() + path;obj = obj.parent(); ) console.log(path);

hello ця функція дозволяє помилку, пов'язану з поточним елементом, що не відображається в дорозі

Перевірте це зараз

$j(".wrapper").click(function(event) ( selectedElement=$j(event.target); var rightArrowParents = ; $j(event.target).parents().not("html,body") .each(function() ( var entry = this.tagName.toLowerCase(); if (this.className) ( entry += "." + this.className.replace(/ /g, "."); )else if (this.id)( entry += "#" + this.id; ) entry=replaceAll(entry,"..","."); rightArrowParents.push(entry); )); rightArrowParents.reverse(); //if(event.target.nodeName.toLowerCase()=="a" || event.target.nodeName.toLowerCase()=="h1")( var entry = event.target.nodeName.toLowerCase(); if (event.target.className) ( entry += "." + event.target.className.replace(/ /g, "."); )else if(event.target.id)( entry += "#" + event.target.id; ) rightArrowParents.push(entry); // )

Де $j = jQuery Variable

також вирішити проблему с.. в класі ім'я

тут функція заміни:

Function escapeRegExp(str) ( return str.replace(/([.*+?^=!:$()()|\[\]\/\\])/g, "\\$1"); ) function replaceAll(str, find, replace) ( return str.replace(new RegExp(escapeRegExp(find), "g"), replace); )

Ось рідна версія JS, яка повертає шлях jQuery. Я також додаю ідентифікатори елементів, якщо вони є. Це дасть вам можливість зробити найкоротший шлях, якщо ви побачите ідентифікатор у масиві.

Var path = getDomPath(element); console.log(path.join(" > "));

Body > section:eq(0) > div:eq(3) > section#content > section#firehose > div#firehoselist > article#firehose-46813651 > header > h2 > span#title-46813651

Ось функція.

Function getDomPath(el) ( var stack = ; while (el.parentNode != null) ( console.log(el.nodeName); var sibCount = 0; var sibIndex = 0; for (var i = 0; i)< el.parentNode.childNodes.length; i++) { var sib = el.parentNode.childNodes[i]; if (sib.nodeName == el.nodeName) { if (sib === el) { sibIndex = sibCount; } sibCount++; } } if (el.hasAttribute("id") && el.id != "") { stack.unshift(el.nodeName.toLowerCase() + "#" + el.id); } else if (sibCount >1) ( stack.unshift(el.nodeName.toLowerCase() + ":eq(" + sibIndex + ")"); ) else ( stack.unshift(el.nodeName.toLowerCase()); ) el = el.parentNode ; ) return stack.slice (1); // removes the html element)



Розповісти друзям