Към съдържанието
wannacode

DOM: страницата от JavaScript

Накратко: DOM е дървото от обекти, през което JavaScript чете и мени страницата: querySelector намира елемент, textContent сменя текста, а createElement и append добавят нови.

Проблем

Цената остава „—“

Цената и наличността на продукта идват от JavaScript. Колега е сложил скрипта най-горе, „за да тръгне по-рано“. Страницата показва „—“ вместо цена.

HTML, CSS, JS

Конзолата казва две неща. console.log(price) е отпечатал null, а следващият ред пада с TypeError: Cannot set properties of null. Елементът <strong id="price"> го има в HTML-а. Защо JavaScript не го намира?

Идеята

Документът като обект

В модула за HTML видя, че браузърът строи от текста дърво. JavaScript работи със същото дърво: за него страницата е обект с име document. Всеки елемент в дървото е обект с полета и методи. Затова дървото се казва DOM: Document Object Model.

Елемент се намира със селектор, както в CSS:

const price = document.querySelector('#price'); // по id
const title = document.querySelector('h1'); // по таг
const first = document.querySelector('.product'); // по клас: първият
const links = document.querySelectorAll('nav a'); // всички връзки в nav
  • querySelector обхожда дървото отгоре надолу и връща първия елемент, който пасва. Ако няма такъв, връща null.
  • querySelectorAll връща всички като NodeList: списък, който има length и forEach, но няма map и filter. За тях го превърни в масив: [...links]. Масивите и forEach са от курса по JavaScript: масиви и ламбди.
  • NodeList-ът е снимка: елемент, добавен по-късно, не влиза в него.

null значи „няма такъв елемент“, а не „празен елемент“. Грешката идва после: null няма textContent, и кодът пада на следващия ред.

Идеята

Кога върви скриптът

Парсерът чете HTML отгоре надолу. Когато стигне до <script>, той спира, пуска скрипта и чак после продължава. В проблема скриптът е преди <article>. Когато върви, в дървото има само <body>, а цената още я няма. Затова querySelector връща null.

Поправката е скриптът да чака дървото:

<head>
<meta charset="utf-8">
<script src="script.js" defer></script>
</head>
  • defer казва: изтегли скрипта веднага, но го пусни, когато целият HTML е прочетен. Тогава всички елементи са в дървото.
  • По-старият начин е <script> най-долу, точно преди </body>. Работи по същата причина.
  • defer важи само за скрипт с src. На скрипт с код между таговете няма ефект.

Пробвай

Поправи цената

Поправи страницата така, че да показва цената и наличността. Не пипай script.js: проблемът е кога върви, а не какво прави.

HTML, CSS, JS

Идеята

Текстът: textContent и innerHTML

Намереният елемент се променя през полетата му. За текста има две:

price.textContent = '1,80 €'; // текстът, както е
note.innerHTML = 'Цена: <strong>1,80 €</strong>'; // текстът се чете като HTML
  • textContent слага текст. Ако в него има <strong>, той се показва като знаци, а не като таг.
  • innerHTML дава текста на парсера и той строи от него нови елементи. Старите деца на елемента изчезват.

innerHTML с данни от потребител (име, отзив, търсене) е опасен. Ако в тях има HTML с код, той тръгва в браузъра на всеки, който отвори страницата: това е XSS атака, за която е урокът „Недоверен вход“. Затова текстът е с textContent, а нови елементи се правят с createElement (по-долу).

След промяната браузърът сам рисува наново. Не рисува цялата страница, а само каквото се е променило.

Виж го

Виж как се променя страницата

Вляво е дървото, вдясно е страницата, която браузърът рисува от него. Кодът намира кошницата, сменя ѝ текста и класа и добавя нов ред в списъка.

const cart = document.querySelector('#cart');
cart.textContent = 'Кошница: 1';
cart.classList.add('full');
const li = document.createElement('li');
li.textContent = 'Мляко';
document.querySelector('ul').append(li);
Стъпка по стъпка, с думи
  1. Парсерът вече е построил дървото от HTML. Вдясно е страницата, която браузърът рисува от него. JavaScript вижда същото дърво през document.
  2. querySelector('#cart') обхожда дървото отгоре и сравнява всеки елемент със селектора. Връща първия, който пасва: cart вече сочи този възел. Ако няма такъв, връща null.
  3. textContent сменя текста на възела. Браузърът забелязва промяната и рисува наново само тази част от страницата. Останалото си стои.
  4. classList.add слага клас на елемента. CSS правило за .full му дава нов цвят и пак се рисува наново само кошницата.
  5. createElement прави нов <li>, а textContent му дава текст. Той още не е в дървото, затова страницата не се променя.
  6. append го слага като последно дете на <ul>. Сега е в дървото и списъкът на страницата расте с един ред.
  7. JavaScript не рисува страницата сам. Той променя дървото, а браузърът рисува каквото се е променило. Страницата не се зарежда наново.

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

Пробвай

Остават 3 бр.

Покажи наличността в #stock: „Остават 3 бр.“. Ако са под 5, сложи на елемента класа low (CSS-ът го оцветява в червено). Удебеляването от класа stock трябва да остане.

HTML, CSS, JS

Идеята

Атрибути, свойства и класове

В HTML елементът има атрибути: value="1", disabled, data-id="7". В JavaScript той има свойства: полета на обекта. Повечето атрибути имат свойство със същото име, но двете не са едно и също нещо.

  • input.value е това, което е в полето сега. Атрибутът value е началната стойност и не се мени, докато купувачът пише. Атрибутът се чете с getAttribute('value').
  • button.disabled = true изключва бутона. Свойството е true или false.
  • Атрибутите data-… са за твои данни. Четат се през dataset: data-stock е dataset.stock, а data-product-id е dataset.productId. Стойността винаги е низ.
  • Класовете се пипат през classList: add, remove, toggle (слага класа, ако го няма, и го маха, ако го има) и contains.
HTML, CSS, JS

Конзолата показва, че свойството е 3, а атрибутът си е 1. dataset.stock е "12", низ: за сметка първо го превърни в число с Number(...).

Пробвай

Намери грешката

Намалението трябва да оцвети всички цени, но конзолата казва Cannot read properties of undefined (reading 'add').

Избери реда с грешката.

Идеята

Нови елементи

Нов елемент се прави в три стъпки: създаваш го, даваш му съдържание и го слагаш в дървото.

const li = document.createElement('li'); // нов <li>, още не е на страницата
li.textContent = 'Мляко';
li.dataset.id = '2';
document.querySelector('ul').append(li); // вече е последното дете на <ul>

Докато не го сложиш с append, елементът го няма на страницата. append приема и няколко елемента наведнъж: li.append(title, button). prepend слага в началото, а remove() маха елемента от дървото.

По-точно, за любопитните

Още начини за търсене. getElementById('price') е като querySelector('#price'). getElementsByClassName и getElementsByTagName връщат „жив“ списък: той се мени, когато се мени дървото. querySelectorAll връща снимка. След append на нов .price живият списък има един елемент повече, а снимката не.

Търсене вътре в елемент. querySelector го има и всеки елемент, не само document: card.querySelector('.price') търси само в картата.

setAttribute('disabled', 'false') изключва бутона. При disabled има значение само дали атрибутът е там, а не стойността му. Затова се пише button.disabled = false или removeAttribute('disabled').

innerHTML и <script>. Скрипт, вкаран с innerHTML, не тръгва. Атрибут като onerror на картинка обаче тръгва. Затова innerHTML с чужди данни е опасен и без <script>.

className срещу classList. className е целият атрибут class като един низ. Присвояването му сменя всички класове наведнъж.

Упражнение

Картата на продукт

В script.js има обект product. Попълни картата от него: името е заглавието (#name), цената е „8,90 €“ (две цифри след запетаята), а наличността е „Изчерпан“, когато stock е 0. Тогава картата получава класа sold-out, а бутонът е изключен. На картата сложи data-id с id-то на продукта. За всеки етикет от badges добави <li> в #badges.

HTML, CSS, JS

Тест

Провери се

1. Какво връща document.querySelector('#total'), ако на страницата няма елемент с id="total"?
2. Скриптът е в <head> без defer и търси бутон от <body>. Какво намира?
3. Име на продукт идва от потребител. С кое го слагаш на страницата?
4. Купувачът е написал 3 в полето <input value="1">. Какво дават input.value и input.getAttribute('value')?
5. Кое добавя класа sale към всички елементи .price?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·