Angularшлях до Strong Junior
🔥 0

Фундамент Ф1 · урок 11 з 12

DOM і події

DOM — це живе дерево сторінки, яке JavaScript може читати й змінювати. В Angular руками з ним майже не працюють: шаблон оновлює сторінку сам. Але щоб розуміти, що робить Angular «під капотом», писати директиви й дебажити події, DOM треба знати. А якщо ти писав скрипти для DAP-платформ чи розширень браузера, — це твоя рідна територія, і урок покаже, як вона стикується з Angular.

Сторінка — це дерево обʼєктів

Браузер читає HTML і будує з нього дерево: кожен тег стає обʼєктом-вузлом з батьком, дітьми й властивостями. Це дерево й називають DOM (Document Object Model). Змінив вузол — сторінка одразу перемалювалась.

HTML <ul id="list"> <li>Звіт</li> <li class="done">Дизайн</li> <li>Реліз</li> </ul> ul#list li li.done li 'Звіт' 'Дизайн' 'Реліз'
Кожен тег — вузол дерева, текст усередині — окремий текстовий вузол. JavaScript працює саме з цим деревом, а не з текстом HTML.

Знайти елемент

document.querySelector('#list');          // перший елемент за CSS-селектором (тут — за id)
document.querySelector('.done');          // перший з класом done
document.querySelectorAll('#list li');    // усі такі — колекція, яку перебирають for...of

Селектори — ті самі, що в CSS з модуля 1. Якщо нічого не знайшлось, querySelector повертає null — і наступний рядок упаде з Cannot read properties of null.

Змінити: текст, класи, атрибути

el.textContent = 'Новий текст';     // замінити текст — безпечно
el.classList.add('done');           // додати клас
el.classList.remove('done');        // прибрати
el.classList.toggle('done');        // перемкнути
el.setAttribute('aria-pressed', 'true');

const li = document.createElement('li');   // створити новий елемент
li.textContent = 'Нова задача';
list.append(li);                           // вставити в кінець списку
li.remove();                               // прибрати зі сторінки

Є ще innerHTML — він вставляє рядок як HTML. З ним обережно: якщо в рядку опиниться те, що ввів користувач, туди можна підсунути <img onerror="…"> зі шкідливим кодом. Це атака XSS. Правило: текст — завжди через textContent. Angular, до речі, захищає від цього сам — модуль 4.

Запусти й подивись, як змінилась сторінка в рамці. Потім клікни «Додати задачу» кілька разів — обробник спрацює вже після запуску коду.

Познач класом done усі задачі, у назві яких є слово «Звіт», і виведи, скільки елементів із класом done стало на сторінці.

Перебери querySelectorAll('#list li') через for...of, перевір li.textContent.includes('Звіт'), додай клас. Потім querySelectorAll('#list li.done').length.

Події

Подія — сигнал, що щось сталось: клік, натискання клавіші, відправка форми. Підписуються на неї через addEventListener — передаючи функцію-колбек (урок 3), яку браузер викличе, коли подія станеться.

button.addEventListener('click', (event) => {
  event.target;            // елемент, по якому клікнули
});

form.addEventListener('submit', (event) => {
  event.preventDefault();  // скасувати дію браузера за замовчуванням: тут — перезавантаження сторінки
});

input.addEventListener('keydown', (event) => {
  if (event.key === 'Enter') { … }
});

Спливання: подія йде вгору

Клік по кнопці — це клік і по рядку, в якому вона лежить, і по картці, і по всій сторінці. Подія спочатку спрацьовує на самому елементі, а потім спливає вгору — до батька, до його батька і так до кореня. Кожен обробник на цьому шляху отримає її.

Запусти й клікни по кнопці, потім по рядку, потім по картці. Подивись, які обробники спрацюють. Потім розкоментуй останній рядок, запусти знову й клікни по кнопці.

Саме тому в таблиці клік по кнопці «Видалити» в рядку спрацьовує ще й як клік по рядку — і відкриває деталі. event.stopPropagation() в обробнику кнопки зупиняє спливання. Це питання з квізу модуля 5 — тепер ти знаєш, звідки воно.

Делегування: один обробник замість сотні

Спливання — не лише пастка, а й інструмент. Замість обробника на кожному з сотні рядків можна повісити один — на список, а в ньому зʼясувати, по якому рядку клікнули:

list.addEventListener('click', (event) => {
  const li = event.target.closest('li');   // найближчий li від місця кліку вгору
  if (!li) return;                         // клікнули повз рядки
  li.classList.toggle('done');
});

Плюс бонус: рядки, додані пізніше, теж працюють — обробник висить на батьку, а не на них.

Як це робить Angular

Руками в DOMВ Angular-шаблоні
el.textContent = task.title{{ task.title }}
el.classList.toggle('done', task.done)[class.done]="task.done"
el.addEventListener('click', …)(click)="toggle(task)"
createElement у циклі@for (task of tasks(); track task.id) { … }

Головна різниця не в синтаксисі, а в підході. Руками ти щоразу кажеш, що змінити на сторінці. В Angular ти змінюєш дані (сигнал), а сторінку під них оновлює фреймворк. Тому в Angular-коді DOM руками майже не чіпають: те, що змінив сам, Angular при наступному оновленні може перезаписати. А обробники він прибирає сам, коли компонент зникає.

Типові помилки
  • querySelector нічого не знайшов (опечатка в селекторі, елемент ще не на сторінці) — далі Cannot read properties of null.
  • innerHTML з даними від користувача — пряма дорога до XSS. Текст — через textContent.
  • Забутий preventDefault() у submit — сторінка перезавантажується, дані зникають.
  • Обробник додають при кожному оновленні — і один клік спрацьовує двічі, тричі…
  • В Angular: змінювати DOM руками замість даних.

Підсумок

  • DOM — дерево обʼєктів сторінки. querySelector шукає, textContent і classList змінюють.
  • addEventListener(тип, колбек) підписує на подію. preventDefault скасовує дію браузера.
  • Події спливають від елемента до батьків. stopPropagation зупиняє, делегування — використовує.
  • В Angular змінюють дані, а не DOM: {{ }}, [class.x], (click) і @for роблять решту.