Как да манипулирате HTML (Въведение в JS DOM)

Ако някога сте се чудили как уебсайтовете са създали навигационни ленти или бутони, които „създават“ съдържание от нищото, през повечето време си имате работа с DOM манипулация.

DOM означава Document Object Model и това, което виждате в браузъра, е самият DOM.

Може да изглежда, че DOM е HTML, но имайте предвид, че манипулирането на DOM, както ще знаете как да направите тук, не променя HTML. По-скоро мислете за DOM като за HTML, който браузърът показва на потребителя.

Следователно промяната на HTML засяга DOM, но промяната на DOM не засяга HTML.

Има два начина за манипулиране на DOM. Има лесен начин: jQuery. Аз обаче ще ви науча на по-трудния начин: чист JavaScript.¹

Така че може би се питате защо да учим по трудния начин? Защо просто не използвате jQuery?

jQuery е хубава библиотека, но съдържа твърде много неща, които няма да ви трябват. Дори ако имате нужда само от три реда код, за да работи вашият проект, той пак ще изтегли целия jQuery файл.

Да, jQuery е с размер под 50 kB, но защо да имате много, когато имате нужда само от няколко?

Освен това е хубаво да знаете как jQuery го прави зад кулисите.

P.S. Ако все още настоявате да използвате jQuery, препоръчвам ви да прочетете техния собствен ресурс.

Някои досадни предположения

Преди да продължа, първо трябва да направя някои предположения за вашите умения.

Въпреки че не са необходими много познания за JavaScript, трябва да разберете концепцията за променливи, функции, цикли и обекти.

Ако имате нужда от преглед, ето ресурсът на Mozilla за „променливи“, „цикли“, „функции“ и „обекти“.

Условия на DOM

Преди да премина към кода, нека първо научим няколко езика. Ще ви трябва.

Първо, имаме документа. Документът е вид HTML, който вашият браузър показва на потребителя.

Следователно, ако все още не сте манипулирали DOM, документът е HTML, който пишете.

Така че, ако вашият сайт „Hello World“ е този:

<!doctype html>
<html>
  <head>
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

Това само по себе си е документът.

Сега, нека влезем в някакъв семеен жаргон. (Между другото, кълна се, че не се шегувам, когато казвам тези неща).

Както можете да видите, между два HTML тагова понякога има други тагове вътре в тях.

Например в тага <html> има <head> и <body> тага вътре в него.

<head> и <body> се считат за деца на маркера <html>. И тъй като децата трябва да имат родител, ние наричаме <html> родител.

И тъй като <head> и <body> имат един и същ родител, всеки от тях е братя и сестри.

Ако все още мислите, че се шегувам с това, няма да ви съдя. Но така ние и JS наричаме връзките между HTML таговете.

И накрая, JS нарича всеки таг и всичко вътре в него „възел“. Но вместо това ще го наричам елемент в тази статия. (Защо? Е, аз искам. И е по-лесно за разбиране от начинаещи.)

Манипулиране на DOM

Сега нека да преминем към вълнуващите части: сега ще манипулираме DOM!

Обърнете внимание отново, че манипулирането на DOM не манипулира HTML. Изглежда така, но това, което манипулира, е това, което потребителят вижда в браузъра.

Избор на елементи

Преди да можем да манипулираме DOM, първо трябва да кажем на JS кои елементи да манипулира.

За да направите това, нека преминем през синтаксиса за момент. Не се притеснявайте, ако все още не го разбирате.

const body = document.querySelector('body');

В горния пример избираме етикета <body> от документа и го присвояваме на променлива с име body.

Следователно синтаксисът за избор на елемент е:

PARENT.querySelector('ELEMENT');

Забележете, че не избираме тялото с помощта на '<body>'. Вместо това използваме 'body'. Това е така, защото избирането на елементи е до голяма степен начинът, по който избирате елементи в CSS.

Следователно, ако искаме да изберем таговете <head> и <body>, можем да направим следното:

const head_and_body = document.querySelector('head, body');

Въпреки това, ако сте се опитали да направите горното и сте използвали console.log(), може да забележите, че издава само главата.

Какво дава? Е, querySelector() избира само първата инстанция на елемента, който искате да изберете. С други думи, тъй като повечето уебсайтове имат етикет <head> преди <body>, използването на querySelector('head, body') дава само етикет <head>.

Но не се притеснявайте! Има и друг начин! querySelectorAll() е.

Когато използва querySelectorAll(), той преследва всички тагове <head> и <body> и ги съхранява в „списък с възли“. Въпреки че изглежда и действа като масив, той НЕ е масив.

За повече информация относно разликата, тази статия я обяснява по-добре.

И така, връщайки се към горния пример, ако искаме да изберем таговете <head> и <body>, ще използваме това:

const head_and_body = document.querySelectorAll('head, body');

Сега, ако използваме console.log() на head_and_body, ето какво получаваме:

> console.log(head_and_body);
NodeList(2) [head, body]
  > 0: head
  > 1: body
  > length: 2
  > __proto__: NodeList

Ааа! Сладка!

Списъците с възли действат като масиви, така че ако искаме да направим нещо с етикета head, просто използваме head_and_body[0].

Сега има друг начин за избиране на елементи: getElementById().

Тъй като атрибутът id е уникален за един елемент, можете да забравите за йерархиите и просто да увеличите мащаба покрай елемента.

Например, да кажем, че имаме този HTML файл и искаме да изберем елемента <p id='lorem'>...</p>:

<body>
  <h1>Lorem ipsum dolor sit amet.</h1>
  <p>Exercitationem blanditiis unde quas debitis enim quisquam facere quaerat autem placeat, repellendus dolor temporibus, tempora molestias suscipit pariatur atque officia consequatur voluptatum nemo recusandae consectetur fugiat itaque! Quae, praesentium reprehenderit.</p>
  <p id='lorem'>Ullam nisi atque nesciunt aliquid quos a porro quis eum, maiores aspernatur placeat error numquam iusto qui dolorum culpa laboriosam repellendus consequatur. Quae temporibus non doloremque quos est voluptate beatae!</p>
  <p>Commodi molestiae velit fuga earum, dolorem atque perspiciatis veritatis enim consectetur ratione praesentium.</p>
</body>

Ако използвахме метода querySelectorAll(), щяхме да имаме този код:

const lorem = document.querySelectorAll('p')[1];

Това не е необходимо. Можем да го опростим до:

const lorem = document.getElementById('lorem');

Докато можехме да използваме querySelector('#lorem') вместо това, използването на getElementById() е по-семантично.

Сега, ако има начин да изберете елементи по техния id, има и начин да изберете елементи по техния клас. Това е getElementsByClassName().

Няма да навлизам повече в това как да използвам това. Ако сте чели дотук, знаете как да го използвате.

Подобно на querySelectorAll(), използването на getElementsByClassName() дава списък с възли.

Има и друг начин за избор на елементи: чрез техните връзки с другите възли.

Например, нека вземем следния HTML:

<div>
  <p>Text</p>
  <ul>
    <li>Unordered List 1</li>
    <li id='2'>Unordered List 2</li>
    <li>Unordered List 3</li>
  </ul>
</div>

Ако приемем, че вече сме избрали div и сме го присвоили на променлива с име div, можем да изберем маркера <p>, като направим следното:

const p = div.firstChild;

По същия начин можем да изберем <ul> по div.lastChild (няма secondChild в JS).

Ето няколко релационни селектора, които можете да използвате:

  1. деца › Извежда HTML колекция²
  2. firstChild › Първото дете на родителския елемент
  3. lastChild › Последното дете на родителския елемент
  4. nextSibling › Следващият брат на елемента
  5. previousSibling › Предишният брат или сестра на елемента
  6. parentElement › Родител на елемента

уф! Това беше много. За да направите бърз преглед, ето HTML файл:

<body>
  <h1>Hello World!</h1>
  <p class='some-element'>Eligendi cupiditate corrupti temporibus vitae magnam laudantium quaerat a, voluptas earum. Odio illum enim ipsa illo beatae? Necessitatibus, temporibus adipisci!</p>
  <p id='select-this' class='some-element'>Aspernatur corporis eum, qui dolorum illo rem harum pariatur esse voluptatibus fuga, incidunt deserunt officiis illum molestias voluptate. <span>Corporis,</span> illo.</p>
  <p>Neque in quasi at architecto illum, suscipit ab incidunt tenetur, quo, rerum soluta earum aliquam dolore voluptatibus quod vel placeat!</p>
  <p>Delectus fugit culpa iusto nesciunt, blanditiis aspernatur amet perspiciatis quaerat vel distinctio atque mollitia odio ex ratione quia? Tenetur, laudantium!</p>
  <p>Eaque beatae placeat in quia dicta voluptatibus doloribus porro laudantium earum aliquam, perferendis laborum est alias debitis incidunt ratione delectus?</p>
</body>

За да изберете абзаца с id „select-this“, ето почти всички начини, по които можете да го направите:

let p;
// Using querySelector()
p = document.querySelector('#select-this');
// Using querySelectorAll()
p = document.querySelectorAll('p')[1];
// Using getElementById()
p = document.getElementById('select-this');
// Using getElementsByClassName()
p = document.getElementsByClassName('some-element')[1];
// Using Relationships (I'll just stick to two for now)
const body = document.querySelector('body');
p = body.children[2];
const span = document.querySelector('span');
p = span.parentElement;

Създаване на елементи

Синтаксисът за създаване на елементи е прост:

document.createElement('ELEMENT');

Така че, ако искате да създадете <p> елемент и да го присвоите на променлива, използвате:

const p = document.createElement('p');
// The above will create <p></p>

Това е. И докато createElement() създава елемент, имайте предвид, че той не го добавя към самия DOM.

Мислете за това като за липса на информация. Вече сте създали елемента, но не знаете къде да го поставите.

Така че следващото нещо, което трябва да научите, е как да ги добавите към самия DOM.

Добавяне на елементи

Има два начина, по които можем да добавим създадения от нас елемент към DOM. Първото е това:

PARENT.appendChild(ELEMENT)

Тъй като новото дете винаги ще бъде най-младото, добавянето на елемент, използващ горния синтаксис, винаги ще се добавя точно преди затварящия таг на елемента.

Просто за забавление: Какво се случва, ако се опитаме да добавим елемент към незатварящ елемент в JS (напр. <img>? Е, получавате това:

const img = document.createElement('img');
const p = document.createElement('p');
img.appendChild(p);
/*
<img>
  <p></p>
</img>
*/

Но не правете това. Това е лоша семантика.

Сега, ето втория начин за добавяне на елемент към DOM:

PARENT.insertBefore(ELEMENT, REFERENCE)

Горният скрипт ще вмъкне елемента преди препратката вътре в родителския възел.

объркани? Ето един пример за по-лесно разбиране:

<div>
  <h1>Title</h1>
  <p>Body</p>
</div> 

JavaScript:

const div = document.querySelector('div');
const p = document.querySelector('p');
const blockquote = document.createElement('blockquote');
div.insertBefore(blockquote, p);

След стартиране на скрипта, DOM сега ще стане:

<div>
  <h1>Title</h1>
  <blockquote></blockquote>
  <p>Body</p>
</div>

Какво ще стане, ако се опитаме да вмъкнем елемент в родителски елемент, но препратката не е в родителския?

Получавате грешка:

Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.

Освен това не можете да използвате document като родител. Не и този път.

Изтриване на елементи

Сега понякога може да искаме да премахнем някои елементи от DOM. В тези случаи ние просто използваме следния синтаксис:

PARENT.removeChild(ELEMENT)

И да. Не можете да използвате document като родител. Отново.

Манипулиране на елементи

Сега добавянето на нови елементи не е достатъчно. В крайна сметка createElement() просто създава празен елемент. Ами ако искаме да добавим атрибути като класове или да добавим текст вътре в него?

JS има четири начина за манипулиране на елементи.

Първо, можем да манипулираме атрибутите на даден елемент.

От една страна, можем да получим атрибутите на даден елемент, като използваме следния синтаксис:

ELEMENT.getAttribute('ATTRIBUTE NAME');

Горното до голяма степен се обяснява само по себе си, така че ще го оставя така.

Ако искаме да премахнем атрибут (напр. alt таг) от елемент, можем да използваме следния синтаксис:

ELEMENT.removeAttribute('ATTRIBUTE NAME');

И накрая, можем да зададем атрибути в елемент, като използваме следния синтаксис:

ELEMENT.setAttribute('ATTRIBUTE', 'STUFF YOU WANT IN ATTRIBUTE');

Така че, ако искате да превърнете <p> в <p class='paragraph'>, използвайте този код:

const p = document.createElement('p');
p.setAttribute('class', 'paragraph');

Сега, вторият начин, по който можем да манипулираме DOM, е чрез манипулиране на неговите класове.

Докато можем да добавяме класове чрез метода setAttribute(), той ни позволява само да добавяме класове. Не ги премахвайте.

По-семантичен начин за добавяне на класове е да използвате следния синтаксис:

ELEMENT.classList.add('CLASS NAME');

Но можем също така да манипулираме класовете на елемент, като ги премахнем, използвайки следния синтаксис:

ELEMENT.classList.remove('CLASS NAME');

И на свой ред можем да превключваме клас (напр. премахване, ако е там, добавяне, ако не е), използвайки следния синтаксис:

ELEMENT.classList.toggle('CLASS NAME');

И така, вторият (и по-добър) начин за превръщане на <p> в <p class='paragraph'> е:

p.classList.add('paragraph');

Третият начин, по който можем да манипулираме елементи, е чрез добавяне на стилови правила.

Тези стилови правила ще бъдат „вградени стилове“, така че не препоръчвам да ги използвате. Ако някога искате да промените нещо например, може да стане объркващо дали сте го поставили в CSS или в JS.

Но ако все пак искате да добавите стилови правила с помощта на JS, има два начина да го направите.

Ако искате да добавите само едно стилово правило (напр. color: blue), трябва да използвате само следния синтаксис:

ELEMENT.style.RULE = 'WHAT YOU WANT IN THE RULE';

Следователно, ако искаме да зададем елемент на абзац да оцветява в жълто, ще направим следното:

const p = document.createElement('p');
p.style.color = 'yellow';

Между другото, докато CSS използва тирета за интервали (напр. margin-left), JS използва камилски главни букви (напр. marginLeft).

Въпреки това, ако искате да добавите няколко стилови правила, е много по-добре да използвате следния синтаксис:

ELEMENT.style.cssText = 'WRITE HOW YOU WOULD WRITE INLINE CSS';

Така че, ако искаме да настроим абзаца да оцветява в жълто, да има подложка от 10px и черен фон, ще направим следното:

p.style.cssText = 'color: yellow; padding: 10px; background: black';

Последният начин, по който манипулираме елементи в dom, е чрез добавяне на съдържание в елемента.

Ако искате да добавите само текстово съдържание, можете да използвате следния синтаксис:

ELEMENT.textContent = 'CONTENT';

Въпреки това, ако искаме да добавим HTML съдържание, бихме използвали следния синтаксис:

ELEMENT.innerHTML = 'CONTENT';

Така че, ако искаме да добавим „Здравей свят!“ в параграфа, ще направим следното:

p.textContent = 'Hello World!';
// ALTERNATIVE WAY
p.innerHTML = 'Hello World';

DOM събития

Да кажем, че имате бутон и искате фонът да стане зелен, когато се щракне върху него. Как правиш това?

Е, тъй като правите нещо само (оцветявате фона в зелено), когато се случи събитие (щраквате), трябва да присвоим това, което наричаме „Слушател на събития“.

Слушателите на събития са това: те слушат събития. Ако му кажете да слуша щракване, то ще го направи.

Въпреки че има три метода за прилагане на слушатели на събития, аз ще се съсредоточа само върху най-добрия начин.

Синтаксисът е доста ясен:

ELEMENT.addEventListener('EVENT', () => {
  // Stuff you want to happen
});

За пълния списък със събития, които JavaScript може да слуша, ви препоръчвам да отидете на Ресурсите на мрежата за разработчици на Mozilla. Има твърде много за покриване.

Връщайки се назад, ако искаме фонът да стане зелен, когато се щракне върху бутон, ще направим следното:

// Assume there's a button in HTML and I assigned it the button variable. Assume the same for the body variable.
button.addEventListener('click', () => {
  body.style.background = 'green';
});

Сега понякога искаме елементът сам да се промени. Например, вместо фонът да стане зелен, какво ще стане, ако искаме самият бутон да стане зелен? Е, можем да направим това:

button.addEventListener('click', () => {
  button.style.background = 'green';
});

Има обаче друг начин да го направите. Използваме e.

За да демонстрираме употребата му, ако искаме бутонът да стане зелен, когато се щракне върху него, бихме могли вместо това да използваме следния синтаксис:

button.addEventListener('click', (e) => {
  e.target.style.background = 'green';
});

Защо e.target? Е, нека първо направим това и да видим какво ще получим:

button.addEventListener('click', (e) => {
  console.log(e);
});

Ако щракнете върху бутона и отидете на конзолата, ще получите нещо подобно:

MouseEvent {isTrusted: true, screenX: 846, screenY: 609, clientX: 846, clientY: 516, ...}

Както виждате, e е обект. Ако разгледаме една от неговите стойности, получаваме следното:

target: button

Следователно, за да манипулираме самия бутон, първо трябва да изберем цел, преди да продължим. В противен случай ще манипулирате самия обект.

Може би се питате обаче какъв е смисълът от използването на e? Е, target е само една стойност от e. Ако погледнем другите му стойности, можем да правим други неща със самия DOM.

Например една от стойностите на e е altKey (която връща true или false). Използвайки e, можем да кажем на JS да направи нещо друго, когато потребителят натисне клавиша Alt, докато щраква върху елемента.

Някои DOM проблеми и решения

Ето два проблема с DOM, които повечето хора срещат, когато манипулират DOM.

Добавяне на множество създадени елементи

Да приемем, че искате да създадете този HTML в чист JavaScript:

<ul>
  <li>Hey!</li>
  <li>Hey!</li>
  <li>Hey!</li>
</ul>

Ако сте начинаещ в манипулирането на DOM, може да сте направили нещо подобно:

const ul = document.createElement('ul');
const li = document.createElement('li');
li.textContent = 'Hey!';
for (let i = 1; i <= 3; i += 1) {
  ul.appendChild(li);
}

Сега, ако го добавите към тялото, може да останете разочаровани и да завършите с този HTML:

<ul>
  <li>Hey!</li>
</ul>

Какво дава? Е, ако сте забелязали, променливата li е извън for цикъла. JS го третира само като един елемент.

Следователно, ако го добавите отново в същия елемент (ul), JS смята, че това е същият елемент, който се опитвате да добавите, и вместо това само го премества.

Така че коригирането му изисква само една малка промяна.

const ul = document.createElement('ul');
for (let i = 1; i <= 3; i += 1) {
  const li = document.createElement('li');
  li.textContent = 'Hey!';
  ul.appendChild(li);
}

Така ще получите това, което искате. Тъй като променливата li се създава отново на всяка итерация, JS я третира като нова <li>, а не като същата.

Манипулиране на списък с възли

Тъй като списъците с възли не са възли (тук ги нарекох елементи), не можете просто да добавите атрибути към тях, тъй като ще добавите атрибут върху списъка с възли.

Списъците с възли действат като масиви, така че просто използвайте цикъл, за да го манипулирате.

Има обаче друг начин за манипулиране на списъци с възли: forEach(). Използването му изглежда така:

const p = document.querySelectorAll('p');
p.forEach((e) => {
  // code
  // to do something with each element, use e as a placeholder. In other words:
  e.style.background = "blue";
});

уф! Това са много неща, които покрихме! И дори не е всичко!

Но не се безпокойте. Не е необходимо да знаете всичко, за да сте компетентни. Това са като 20%, които трябва да научите в принципа на Парето. Достатъчно е да ви позволи да изпълнявате 80% от проектите, по които ще работите.

Бележки под линия

  1. Излъгах. JavaScript не е единственият начин за манипулиране на DOM. Повечето езици като Python също могат да манипулират DOM. Въпреки това, JS е най-лесният и най-малко ангажиран начин за това.
  2. Също така изглежда и действа като масив, но не е масив.