Фундамент Ф1 · урок 11 з 12
DOM і події
DOM — це живе дерево сторінки, яке JavaScript може читати й змінювати. В Angular руками з ним майже не працюють: шаблон оновлює сторінку сам. Але щоб розуміти, що робить Angular «під капотом», писати директиви й дебажити події, DOM треба знати. А якщо ти писав скрипти для DAP-платформ чи розширень браузера, — це твоя рідна територія, і урок покаже, як вона стикується з Angular.
Сторінка — це дерево обʼєктів
Браузер читає HTML і будує з нього дерево: кожен тег стає обʼєктом-вузлом з батьком, дітьми й властивостями. Це дерево й називають DOM (Document Object Model). Змінив вузол — сторінка одразу перемалювалась.
Знайти елемент
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роблять решту.