Делегування подій (Event Delegation)
Делегування подій (Event Delegation)
Проблема: коли обробників занадто багато
Уявіть, що ви створюєте інтерактивну програму управління завданнями (TODO-list). У списку може бути 1000+ завдань, і кожне має:
- Кнопку "Виконано" ✅
- Кнопку "Видалити" ❌
- Кнопку "Редагувати" ✏️
Наївний підхід — призначити обробник кожній кнопці:
// ⚠️ НЕЕФЕКТИВНИЙ КОД!
const tasks = document.querySelectorAll('.task')
tasks.forEach((task) => {
const completeBtn = task.querySelector('.complete')
const deleteBtn = task.querySelector('.delete')
const editBtn = task.querySelector('.edit')
completeBtn.addEventListener('click', handleComplete)
deleteBtn.addEventListener('click', handleDelete)
editBtn.addEventListener('click', handleEdit)
})
// Результат: 3000+ обробників подій в пам'яті! 😱
Проблеми:
- 💾 Витік пам'яті — тисячі функцій-обробників
- 🐌 Повільна ініціалізація — треба пройтися по всіх елементах
- 🔄 Не працює для динамічних елементів — нові завдання не матимуть обробників
- 🗑️ Складна очистка — треба видаляти обробники при видаленні завдань
Критична проблема масштабування
Уявіть:- 1000 завдань × 3 кнопки = 3000 обробників
- Кожен обробник займає ~100 байт = 300 КБ пам'яті
- При скролінгу списку з 10,000 елементів браузер зависає
Рятівне рішення: делегування подій
💡 Ключова ідея
Замість призначення обробника кожному елементу, призначте один обробник батьківському елементу. Він перехопить всі події від дочірніх елементів завдяки механізму спливання.Результат: 3000 обробників → 1 обробник = економія 99.97% пам'яті!Ефективний підхід:
// ✅ ОДИН обробник замість тисяч!
const taskList = document.querySelector('.task-list')
taskList.addEventListener('click', function (event) {
const target = event.target
// Перевіряємо, на яку кнопку клікнули
if (target.classList.contains('complete')) {
handleComplete(event)
} else if (target.classList.contains('delete')) {
handleDelete(event)
} else if (target.classList.contains('edit')) {
handleEdit(event)
}
})
Візуалізація делегування
Навіщо використовувати делегування?
🚀 Продуктивність
Один обробник замість тисяч. Менше пам'яті, швидша ініціалізація.
// 1 обробник vs 1000 обробників
// Економія: 99.9% пам'яті
🔄 Динамічність
Працює для майбутніх елементів. Додали новий елемент? Обробник вже працює!
// Новий елемент автоматично обробляється
taskList.innerHTML += '<li>New task</li>'
📝 Простота коду
Менше коду для підтримки. Вся логіка в одному місці.
// Одна функція керує всім
function handleClick(e) {
// Вся логіка тут
}
🗑️ Автоматична очистка
Не треба видаляти обробники. Видалили елемент? Обробник залишається на батьку.
// Просто видаляємо елемент
element.remove() // Готово!
Практичний приклад 1: Інтерактивна таблиця
Створимо таблицю, де можна виділяти комірки кліком. Це класичний приклад делегування.
HTML структура
<table id="data-table">
<thead>
<tr>
<th>Ім'я</th>
<th>Вік</th>
<th>Місто</th>
</tr>
</thead>
<tbody>
<tr>
<td>Олександр</td>
<td>25</td>
<td>Київ</td>
</tr>
<tr>
<td>Марія</td>
<td>30</td>
<td>Львів</td>
</tr>
<tr>
<td>Іван</td>
<td>22</td>
<td>Одеса</td>
</tr>
</tbody>
</table>
Проблема: вкладені елементи
Якщо клік відбувається на тексті всередині <td>, event.target не буде <td>!
<td>
<strong>Олександр</strong>
<!-- event.target буде <strong>! -->
</td>
Рішення: Використовуємо метод closest():
const table = document.getElementById('data-table')
let selectedCell = null
table.addEventListener('click', function (event) {
// 1. Знаходимо найближчий <td> (навіть якщо клік на вкладеному елементі)
const td = event.target.closest('td')
// 2. Якщо клік не в <td> — ігноруємо
if (!td) return
// 3. Перевіряємо, чи <td> належить нашій таблиці (не вкладеній!)
if (!table.contains(td)) return
// 4. Виділяємо комірку
highlight(td)
})
function highlight(td) {
// Знімаємо попереднє виділення
if (selectedCell) {
selectedCell.classList.remove('highlighted')
}
// Додаємо нове виділення
selectedCell = td
selectedCell.classList.add('highlighted')
}
Розбір коду:
event.target.closest('td')— шукає найближчий елемент<td>вгору по дереву DOM- Якщо клік на тексті всередині
<td>, метод знайде батьківський<td> - Якщо клік на
<th>або поза таблицею, повернеnull
- Якщо клік на тексті всередині
if (!td) return— рання перевірка, якщо клік не на комірціtable.contains(td)— важлива перевірка для вкладених таблиць- Якщо всередині є ще одна таблиця, її комірки нас не цікавлять
- Перевіряємо, чи
tdналежить саме нашій таблиці
highlight(td)— виділяємо знайдену комірку
Повний приклад з CSS
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>Делегування подій — Таблиця</title>
<style>
table {
border-collapse: collapse;
width: 100%;
font-family: Arial, sans-serif;
}
th,
td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
th {
background-color: #3b82f6;
color: white;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f9fafb;
}
tr:hover {
background-color: #f3f4f6;
}
.highlighted {
background-color: #fef08a !important;
border: 2px solid #eab308;
font-weight: bold;
transform: scale(1.05);
transition: all 0.2s;
}
</style>
</head>
<body>
<h1>Клікніть на комірку для виділення</h1>
<table id="data-table">
<thead>
<tr>
<th>Ім'я</th>
<th>Вік</th>
<th>Місто</th>
<th>Професія</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Олександр</strong></td>
<td>25</td>
<td><em>Київ</em></td>
<td>Розробник</td>
</tr>
<tr>
<td><strong>Марія</strong></td>
<td>30</td>
<td><em>Львів</em></td>
<td>Дизайнер</td>
</tr>
<tr>
<td><strong>Іван</strong></td>
<td>22</td>
<td><em>Одеса</em></td>
<td>Менеджер</td>
</tr>
</tbody>
</table>
<script>
const table = document.getElementById('data-table')
let selectedCell = null
table.addEventListener('click', function (event) {
const td = event.target.closest('td')
if (!td) return
if (!table.contains(td)) return
highlight(td)
})
function highlight(td) {
if (selectedCell) {
selectedCell.classList.remove('highlighted')
}
selectedCell = td
selectedCell.classList.add('highlighted')
}
</script>
</body>
</html>
Практичний приклад 2: Меню з data-атрибутами
Делегування особливо потужне в поєднанні з data-атрибутами. Ви можете зберігати дії прямо в HTML!
Патерн "Дії в розмітці"
<div id="menu">
<button data-action="save">💾 Зберегти</button>
<button data-action="load">📂 Завантажити</button>
<button data-action="print">🖨️ Друкувати</button>
<button data-action="export">📤 Експортувати</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem
// Прив'язуємо this, щоб методи працювали правильно
elem.addEventListener('click', this.onClick.bind(this))
}
save() {
alert('💾 Збереження даних...')
console.log('Виклик API для збереження')
}
load() {
alert('📂 Завантаження даних...')
console.log('Виклик API для завантаження')
}
print() {
alert('🖨️ Друк документа...')
window.print()
}
export() {
alert('📤 Експорт даних...')
console.log('Генерація CSV файлу')
}
onClick(event) {
// Отримуємо data-action з клікнутого елемента
const action = event.target.dataset.action
// Якщо action існує і є метод з такою назвою
if (action && typeof this[action] === 'function') {
this[action]() // Викликаємо метод
}
}
}
const menu = new Menu(document.getElementById('menu'))
</script>
Переваги підходу:
🎯 Чому data-атрибути + делегування — ідеальна пара?
1. Декларативність — дія описана прямо в HTML<button data-action="delete">Видалити</button>
<!-- Зразу зрозуміло, що робить кнопка! -->
<!-- Просто додаємо нову кнопку -->
<button data-action="newFeature">Нова функція</button>
- HTML описує що (структура + наміри)
- JavaScript реалізує як (логіка)
- Не важливо, 3 кнопки чи 300
- Працює для динамічно доданих кнопок
Розширена версія з параметрами
<div id="advanced-menu">
<button data-action="notify" data-message="Успішно!" data-type="success">✅ Успіх</button>
<button data-action="notify" data-message="Помилка!" data-type="error">❌ Помилка</button>
<button data-action="notify" data-message="Попередження" data-type="warning">⚠️ Попередження</button>
</div>
<script>
class AdvancedMenu {
constructor(elem) {
this._elem = elem
elem.addEventListener('click', this.onClick.bind(this))
}
notify(params) {
const { message, type } = params
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
}
const notification = document.createElement('div')
notification.textContent = message
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
background: ${colors[type]};
color: white;
border-radius: 8px;
font-weight: bold;
animation: slideIn 0.3s;
`
document.body.appendChild(notification)
setTimeout(() => {
notification.remove()
}, 3000)
}
onClick(event) {
const action = event.target.dataset.action
if (!action) return
// Збираємо всі data-атрибути як параметри
const params = { ...event.target.dataset }
delete params.action // Видаляємо сам action
if (typeof this[action] === 'function') {
this[action](params)
}
}
}
const advMenu = new AdvancedMenu(document.getElementById('advanced-menu'))
</script>
Патерн "Поведінки" (Behaviors)
Делегування дозволяє створювати переповикористовувані поведінки через атрибути.
Приклад 1: Лічильник
<style>
[data-counter] {
padding: 10px 20px;
font-size: 24px;
font-weight: bold;
cursor: pointer;
border: 2px solid #3b82f6;
background: white;
border-radius: 8px;
transition: all 0.2s;
}
[data-counter]:hover {
background: #3b82f6;
color: white;
transform: scale(1.1);
}
</style>
<p>Лічильники (клікніть для збільшення):</p>
<button data-counter>0</button>
<button data-counter>0</button>
<button data-counter>0</button>
<script>
// ОДИН обробник на весь документ!
document.addEventListener('click', function (event) {
// Перевіряємо наявність атрибута data-counter
if (event.target.dataset.counter !== undefined) {
// Збільшуємо значення
const currentValue = parseInt(event.target.textContent) || 0
event.target.textContent = currentValue + 1
}
})
</script>
Магія: Додайте data-counter до будь-якого елемента — він автоматично стає лічильником!
Приклад 2: Перемикач видимості
<button data-toggle-id="secret-content">🔓 Показати секретний вміст</button>
<div id="secret-content" hidden>
<h3>🎉 Секретний вміст!</h3>
<p>Ви відкрили приховану інформацію.</p>
</div>
<button data-toggle-id="bonus-info">📋 Додаткова інформація</button>
<div id="bonus-info" hidden>
<p>Тут розміщена додаткова інформація.</p>
</div>
<script>
document.addEventListener('click', function (event) {
const toggleId = event.target.dataset.toggleId
if (toggleId) {
const elem = document.getElementById(toggleId)
if (elem) {
elem.hidden = !elem.hidden
// Змінюємо текст кнопки
event.target.textContent = elem.hidden ? '🔓 Показати' : '🔒 Сховати'
}
}
})
</script>
Приклад 3: Підтвердження дій
<style>
.danger-zone {
padding: 20px;
background: #fee;
border: 2px solid #f00;
border-radius: 8px;
}
[data-confirm] {
background: #ef4444;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
}
[data-confirm]:hover {
background: #dc2626;
}
</style>
<div class="danger-zone">
<h3>⚠️ Небезпечна зона</h3>
<button data-confirm="Ви впевнені, що хочете видалити ВСІ дані?" data-action="deleteAll">
🗑️ Видалити всі дані
</button>
<button data-confirm="Скинути налаштування до заводських?" data-action="reset">🔄 Скинути налаштування</button>
</div>
<script>
document.addEventListener('click', function (event) {
const confirmMessage = event.target.dataset.confirm
if (confirmMessage) {
// Показуємо діалог підтвердження
if (confirm(confirmMessage)) {
const action = event.target.dataset.action
console.log(`Виконую дію: ${action}`)
alert(`✅ Дія "${action}" виконана!`)
} else {
console.log('Дію скасовано користувачем')
}
}
})
</script>
Комбінування кількох поведінок
Один елемент може мати кілька поведінок одночасно:
<button data-counter data-confirm="Збільшити лічильник?" data-sound="click">Лічильник з підтвердженням</button>
<script>
document.addEventListener('click', function (event) {
const target = event.target
// Поведінка 1: Підтвердження
if (target.dataset.confirm) {
if (!confirm(target.dataset.confirm)) {
return // Зупиняємо, якщо не підтвердили
}
}
// Поведінка 2: Звук
if (target.dataset.sound) {
playSound(target.dataset.sound)
}
// Поведінка 3: Лічильник
if (target.dataset.counter !== undefined) {
const value = parseInt(target.textContent) || 0
target.textContent = value + 1
}
})
function playSound(soundName) {
console.log(`🔊 Відтворення звуку: ${soundName}`)
// new Audio(`/sounds/${soundName}.mp3`).play();
}
</script>
Складний приклад: TODO List з делегуванням
Створимо повноцінний додаток для управління завданнями з мінімальним кодом:
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>TODO List з делегуванням</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 32px;
margin-bottom: 10px;
}
.add-task {
padding: 20px;
background: #f8f9fa;
border-bottom: 2px solid #e9ecef;
}
.add-task input {
width: 100%;
padding: 15px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
}
.add-task input:focus {
border-color: #667eea;
}
#task-list {
list-style: none;
padding: 0;
}
.task {
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
gap: 15px;
transition: background 0.2s;
}
.task:hover {
background: #f8f9fa;
}
.task.completed {
background: #d1fae5;
}
.task.completed .task-text {
text-decoration: line-through;
color: #6b7280;
}
.task-text {
flex: 1;
font-size: 16px;
}
.task button {
padding: 8px 12px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: transform 0.2s;
}
.task button:hover {
transform: scale(1.1);
}
.task button:active {
transform: scale(0.95);
}
[data-action='toggle'] {
background: #10b981;
}
[data-action='delete'] {
background: #ef4444;
}
.stats {
padding: 20px;
background: #f8f9fa;
text-align: center;
color: #6b7280;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📝 Мої завдання</h1>
<p>Делегування подій у дії</p>
</div>
<div class="add-task">
<input
type="text"
id="new-task-input"
placeholder="Додати нове завдання..."
data-action="add-on-enter"
/>
</div>
<ul id="task-list"></ul>
<div class="stats" id="stats">Завдань: 0 | Виконано: 0</div>
</div>
<script>
const taskList = document.getElementById('task-list')
const input = document.getElementById('new-task-input')
const stats = document.getElementById('stats')
let taskId = 0
let tasks = []
// ЄДИНИЙ обробник для ВСЬОГО документа!
document.addEventListener('click', function (event) {
const action = event.target.dataset.action
if (action === 'toggle') {
toggleTask(event.target.dataset.id)
} else if (action === 'delete') {
deleteTask(event.target.dataset.id)
}
})
// Обробка Enter для додавання
document.addEventListener('keypress', function (event) {
if (event.target.dataset.action === 'add-on-enter' && event.key === 'Enter') {
addTask()
}
})
function addTask() {
const text = input.value.trim()
if (!text) return
const task = {
id: taskId++,
text: text,
completed: false,
}
tasks.push(task)
render()
input.value = ''
input.focus()
}
function toggleTask(id) {
const task = tasks.find((t) => t.id == id)
if (task) {
task.completed = !task.completed
render()
}
}
function deleteTask(id) {
tasks = tasks.filter((t) => t.id != id)
render()
}
function render() {
taskList.innerHTML = tasks
.map(
(task) => `
<li class="task ${task.completed ? 'completed' : ''}">
<span class="task-text">${task.text}</span>
<button data-action="toggle" data-id="${task.id}">
${task.completed ? '↩️' : '✅'}
</button>
<button data-action="delete" data-id="${task.id}">
🗑️
</button>
</li>
`,
)
.join('')
updateStats()
}
function updateStats() {
const total = tasks.length
const completed = tasks.filter((t) => t.completed).length
stats.textContent = `Завдань: ${total} | Виконано: ${completed}`
}
// Початкові дані
tasks = [
{ id: taskId++, text: 'Вивчити делегування подій', completed: true },
{ id: taskId++, text: 'Створити TODO додаток', completed: false },
{ id: taskId++, text: 'Практикуватись з JavaScript', completed: false },
]
render()
</script>
</body>
</html>
Що робить цей код:
- ✅ Один обробник
clickдля всього документа - 🔄 Динамічне додавання/видалення завдань
- ⚡ Миттєва реакція без переприв'язки обробників
- 📊 Автоматичне оновлення статистики
- 🎨 Красивий дизайн з анімаціями
Алгоритм делегування подій
Крок 1: Визначте контейнер
Знайдіть спільного батька для всіх елементів, з якими треба працювати.
const container = document.querySelector('.task-list')
Крок 2: Додайте обробник на контейнер
Призначте один обробник події (зазвичай click).
container.addEventListener('click', handleClick)
Крок 3: Визначте цільовий елемент
Використовуйте event.target або event.target.closest().
function handleClick(event) {
const button = event.target.closest('button')
if (!button) return
// ...
}
Крок 4: Перевірте належність
Переконайтеся, що елемент належить вашому контейнеру.
if (!container.contains(button)) return
Крок 5: Виконайте дію
Обробіть подію відповідно до логіки.
const action = button.dataset.action
if (action === 'delete') {
deleteItem(button.dataset.id)
}
Методи для роботи з DOM
// Клік на <span> всередині <button>
event.target.closest('button') // Знайде батьківський button
nullif (event.target.matches('.delete-btn')) {
// Це кнопка видалення
}
true або falsechild нащадком element.if (container.contains(event.target)) {
// Клік всередині контейнера
}
true або falseПорівняння методів
const container = document.querySelector('.container')
container.addEventListener('click', (e) => {
const target = e.target
// 1. matches() — перевірка самого елемента
if (target.matches('.delete-btn')) {
console.log('Клік на кнопці видалення')
}
// 2. closest() — пошук найближчого предка
const card = target.closest('.card')
if (card) {
console.log('Клік всередині картки')
}
// 3. contains() — перевірка належності
if (container.contains(target)) {
console.log('Клік всередині контейнера')
}
})
Обмеження делегування
⚠️ Коли делегування НЕ працює
1. Події, що не спливаютьДеякі події не спливають, тому делегування не спрацює:focus/blur(використовуйтеfocusin/focusout)mouseenter/mouseleave(використовуйтеmouseover/mouseout)scroll(прокрутка не спливає)load/unload
// ❌ Не спрацює
container.addEventListener('focus', handler)
// ✅ Спрацює
container.addEventListener('focusin', handler)
event.stopPropagation(), подія не дійде до контейнера:button.addEventListener('click', (e) => {
e.stopPropagation() // Делегування НЕ спрацює!
})
container.addEventListener('click', () => {
console.log('Не виконається!')
})
Переваги vs Недоліки
| Перевага | Опис |
|---|---|
| 🚀 Продуктивність | Один обробник замість тисяч |
| 💾 Пам'ять | Менше об'єктів-обробників |
| 🔄 Динамічність | Працює для майбутніх елементів |
| 📝 Простота | Менше коду для підтримки |
| 🗑️ Очистка | Не треба видаляти обробники при видаленні елементів |
| 🎨 Гнучкість | Легко змінювати логіку в одному місці |
| Недолік | Рішення |
|---|---|
| ⚠️ Не для несплываючих подій | Використовуйте альтернативи (focusin замість focus) |
| 🛑 Чутливість до stopPropagation | Навчіть команду не зловживати цим методом |
| 🐌 Потенційне навантаження | Використовуйте специфічніші контейнери |
| 🔍 Складність debugging | Використовуйте data-атрибути для чіткої ідентифікації |
Best Practices (Найкращі практики)
🎯 Рекомендації для ефективного делегування
1. Використовуйте data-атрибути<button data-action="delete" data-id="123">Видалити</button>
const item = event.target.closest('.item')
if (!item || !container.contains(item)) return
if (!event.target.closest('.button')) return
// Вся логіка тут
// ❌ Погано — все в одному обробнику
document.addEventListener('click', handleEverything)
// ✅ Добре — розділені обробники
document.addEventListener('click', handleButtons)
menu.addEventListener('click', handleMenuItems)
// ✅ Для глобальних поведінок
document.addEventListener('click', handler)
// ⚠️ Не використовуйте document.onclick
document.onclick = handler // Може конфліктувати!
// ✅ Використовуйте addEventListener
document.addEventListener('click', handler)
Порівняння: з делегуванням vs без
// Призначаємо обробник кожній кнопці
const buttons = document.querySelectorAll('.delete-btn')
buttons.forEach((button) => {
button.addEventListener('click', function () {
const id = this.dataset.id
deleteItem(id)
})
})
// Проблеми:
// 1. Нові кнопки не матимуть обробника
// 2. Треба видаляти обробники
// 3. Багато обробників у пам'яті
// Додаємо нову кнопку
const newBtn = document.createElement('button')
newBtn.className = 'delete-btn'
newBtn.dataset.id = '999'
// ❌ Обробника немає!
// Один обробник на контейнер
const container = document.querySelector('.container')
container.addEventListener('click', function (event) {
const deleteBtn = event.target.closest('.delete-btn')
if (deleteBtn && container.contains(deleteBtn)) {
const id = deleteBtn.dataset.id
deleteItem(id)
}
})
// Переваги:
// 1. Нові кнопки працюють автоматично ✅
// 2. Не треба видаляти обробники ✅
// 3. Один обробник ✅
// Додаємо нову кнопку
const newBtn = document.createElement('button')
newBtn.className = 'delete-btn'
newBtn.dataset.id = '999'
// ✅ Обробник вже працює!
Підсумки
Делегування = один обробник для всіх
Замість призначення обробника кожному елементу, призначте один обробник батьківському елементу.
Використовуйте event.target
event.target показує, на якому елементі насправді відбулася подія.event.target.closest(selector) знаходить найближчий елемент.
Працює для динамічних елементів
Нові елементи автоматично обробляються без додаткового коду.
Переваги: продуктивність + простота
- Менше пам'яті (один обробник)
- Менше коду (одна функція)
- Працює для майбутніх елементів
Обмеження: події повинні спливати
Не працює для focus, blur, scroll та інших несплываючих подій.
Використовуйте альтернативи: focusin/focusout.
Best practice: data-атрибути
Зберігайте дії та параметри в data-* атрибутах для чистого коду.
Практичні завдання
Завдання 1: Фільтрована галерея
Створіть галерею зображень з фільтрами:- Кнопки фільтрів: "Всі", "Природа", "Міста", "Люди"
- При кліку на фільтр показуються лише відповідні зображення
- Використайте делегування для кнопок фільтрів
- Додайте клас
.activeдля активного фільтра
Завдання 2: Кошик покупок
Реалізуйте кошик покупок:- Кнопки: "Додати", "Видалити", "+1", "-1"
- Автоматичний підрахунок загальної суми
- Збереження в
localStorage - Використайте делегування для всіх кнопок
<ul>, різні data-action для кнопок.Завдання 3: Конструктор форм
Створіть конструктор, де можна:- Додавати поля форми (текст, email, checkbox)
- Видаляти поля
- Змінювати порядок (вгору/вниз)
- Використайте делегування + data-атрибути
data-action для дій, data-field-id для ідентифікації.Додаткові ресурси
Наступна стаття: Типові дії браузера — як контролювати стандартну поведінку браузера