Як правило, коли потрібно виконати якісь дії з 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)