Уявіть: ви створюєте власний UI-компонент — карусель слайдів, модальне вікно або dropdown-меню. Користувач взаємодіє з вашим компонентом, і вам потрібно повідомити інший код про це:
Браузерні події (click, input, submit) описують базові дії. Але як створити власні події для власної логіки?
// Створюємо подію
const event = new CustomEvent('user-login', {
detail: { username: 'John', role: 'admin' },
})
// Запускаємо подію
document.dispatchEvent(event)
🧩 Компонентність
Розділення відповідальності
Компонент повідомляє "що сталося", але не знає "хто і як" це обробить.
// Компонент генерує подію
modal.emit('close')
// Різний код може слухати
modal.on('close', saveData)
modal.on('close', analytics)
🔗 Слабке зчеплення
Незалежність модулів
Модулі спілкуються через події, не викликаючи функції один одного напряму.
// ❌ Сильне зчеплення
function closeModal() {
analyticsTrack()
saveToStorage()
}
// ✅ Слабке зчеплення
modal.emit('close')
🧪 Тестування
Імітація дій користувача
Програмно ініціюємо події для автотестів.
// Імітація кліку для тесту
button.dispatchEvent(new Event('click'))
📡 Publish-Subscribe
Архітектурний патерн
Реалізація EventBus/EventEmitter для глобальної комунікації.
// Глобальна шина подій
eventBus.on('navigation', update)
eventBus.emit('navigation', data)
EventНайпростіший спосіб створити подію — використати конструктор Event:
const event = new Event(eventType, options)
Параметри:
| Параметр | Тип | Опис |
|---|---|---|
eventType | string | Назва події: 'click', 'my-event', лю будь-яка |
options | object | Налаштування (необов'язково) |
Опції:
| Властивість | Тип | За замовчуванням | Опис |
|---|---|---|---|
bubbles | boolean | false | Чи спливає подія вгору по DOM |
cancelable | boolean | false | Чи можна скасувати через preventDefault() |
// 1. Створюємо подію
const event = new Event('hello')
// 2. Додаємо обробник
document.addEventListener('hello', function () {
console.log('👋 Привіт з події!')
})
// 3. Запускаємо подію
document.dispatchEvent(event)
// Консоль: 👋 Привіт з події!
dispatchEvent()Метод element.dispatchEvent(event) запускає подію на елементі.
element.dispatchEvent(event)
Повертає:
true — якщо подія не була скасована (не викликано preventDefault())false — якщо подія була скасована<button id="btn">Я кнопка</button>
<script>
const button = document.getElementById('btn')
// Додаємо обробник
button.addEventListener('click', function () {
alert('Клік!')
})
// Імітуємо клік програмно
const clickEvent = new Event('click')
button.dispatchEvent(clickEvent)
// Alert з'явиться автоматично!
</script>
Що відбувається:
clickdispatchEvent()За замовчуванням події НЕ спливають. Щоб включити спливання, встановіть bubbles: true:
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>Спливання користувацьких подій</title>
</head>
<body>
<div id="outer">
<div id="inner">
<button id="btn">Запустити подію</button>
</div>
</div>
<script>
const btn = document.getElementById('btn')
const inner = document.getElementById('inner')
const outer = document.getElementById('outer')
// Обробники на різних рівнях
btn.addEventListener('custom-event', () => {
console.log('📍 Кнопка: подія на button')
})
inner.addEventListener('custom-event', () => {
console.log('📍 Внутрішній: подія на inner')
})
outer.addEventListener('custom-event', () => {
console.log('📍 Зовнішній: подія на outer')
})
document.addEventListener('custom-event', () => {
console.log('📍 Документ: подія на document')
})
// Запускаємо події
btn.addEventListener('click', () => {
console.log('\n--- БЕЗ спливання (bubbles: false) ---')
const event1 = new Event('custom-event') // bubbles = false за замовчуванням
btn.dispatchEvent(event1)
setTimeout(() => {
console.log('\n--- ЗІ спливанням (bubbles: true) ---')
const event2 = new Event('custom-event', { bubbles: true })
btn.dispatchEvent(event2)
}, 1000)
})
</script>
</body>
</html>
Результат:
--- БЕЗ спливання (bubbles: false) ---
📍 Кнопка: подія на button
--- ЗІ спливанням (bubbles: true) ---
📍 Кнопка: подія на button
📍 Внутрішній: подія на inner
📍 Зовнішній: подія на outer
📍 Документ: подія на document
isTrusted: реальна чи синтетична подіяВластивість event.isTrusted показує, чи подія від реального користувача чи створена кодом:
element.addEventListener('click', (event) => {
if (event.isTrusted) {
console.log('✅ Реальний клік користувача')
} else {
console.log('🤖 Синтетична подія з кода')
}
})
// Реальний клік → isTrusted = true
// element.dispatchEvent(new Event('click')) → isTrusted = false
// Захист від автоматизації
button.addEventListener('click', (event) => {
if (!event.isTrusted) {
console.warn('⚠️ Спроба ботом обійти захист!')
return // Блокуємо
}
// Обробляємо тільки реальні кліки
processPayment()
})
isTrusted для критичної безпеки! Зловмисник може змінити це значення через DevTools або модифікацію браузера. Використовуйте серверну валідацію.Для браузерних типів подій (миша, клавіатура, фокус) використовуйте спеціалізовані конструктори:
const mouseEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
clientX: 150, // Координата X
clientY: 200, // Координата Y
button: 0, // Ліва кнопка миші
ctrlKey: false, // Ctrl не натиснуто
shiftKey: false, // Shift не натиснуто
})
element.dispatchEvent(mouseEvent)
console.log(mouseEvent.clientX) // 150
console.log(mouseEvent.clientY) // 200
const keyEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13, // Старий API
ctrlKey: false,
shiftKey: false,
bubbles: true,
})
input.dispatchEvent(keyEvent)
const focusEvent = new FocusEvent('focus', {
bubbles: true, // focus не спливає, але focusin спливає
relatedTarget: null, // Попередній елемент з фокусом
})
input.dispatchEvent(focusEvent)
// Спроба встановити clientX через Event
const event = new Event('click', {
bubbles: true,
cancelable: true,
clientX: 100, // ❌ Ігнорується!
})
console.log(event.clientX) // undefined
Проблема: Event не знає про властивості миші
// Правильне використання MouseEvent
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
clientX: 100, // ✅ Працює!
})
console.log(event.clientX) // 100
Рішення: Використовуємо спеціалізований конструктор
// Обхідний шлях (не рекомендується)
const event = new Event('click', {
bubbles: true,
cancelable: true,
})
event.clientX = 100 // Присвоюємо після створення
console.log(event.clientX) // 100 (працює, але погана практика)
Недолік: Порушення типізації, нестандартно
Для власних подій використовуйте CustomEvent з властивістю detail:
const event = new CustomEvent(eventType, {
detail: {
/* будь-які дані */
},
bubbles: true,
cancelable: true,
})
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>Система сповіщень</title>
<style>
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 8px;
color: white;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
animation: slideIn 0.3s;
}
.notification.success {
background: #10b981;
}
.notification.error {
background: #ef4444;
}
.notification.info {
background: #3b82f6;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
</head>
<body>
<h1>Система сповіщень</h1>
<button onclick="notify('success', 'Збережено успішно!')">✅ Показати успіх</button>
<button onclick="notify('error', 'Помилка з\'єднання!')">❌ Показати помилку</button>
<button onclick="notify('info', 'Нова інформація')">ℹ️ Показати інфо</button>
<script>
// Глобальний обробник сповіщень
document.addEventListener('notification', (event) => {
const { type, message, duration } = event.detail
// Створюємо елемент сповіщення
const notification = document.createElement('div')
notification.className = `notification ${type}`
notification.textContent = message
document.body.appendChild(notification)
// Автоматичне видалення
setTimeout(() => {
notification.remove()
}, duration)
console.log(`📢 Сповіщення [${type}]: ${message}`)
})
// Функція для ініціювання сповіщення
function notify(type, message, duration = 3000) {
const event = new CustomEvent('notification', {
detail: { type, message, duration },
bubbles: true,
})
document.dispatchEvent(event)
}
</script>
</body>
</html>
Переваги підходу:
notify()event.detailclass Modal {
constructor(element) {
this.element = element
}
open() {
// Генеруємо подію ПЕРЕД відкриттям
const beforeOpenEvent = new CustomEvent('modal:before-open', {
detail: { modalId: this.element.id },
cancelable: true, // Можна скасувати
bubbles: true,
})
// Якщо подію скасовано — не відкриваємо
if (!this.element.dispatchEvent(beforeOpenEvent)) {
console.log('Відкриття модального вікна скасовано')
return
}
// Відкриваємо
this.element.style.display = 'block'
// Генеруємо подію ПІСЛЯ відкриття
const afterOpenEvent = new CustomEvent('modal:opened', {
detail: { modalId: this.element.id, timestamp: Date.now() },
bubbles: true,
})
this.element.dispatchEvent(afterOpenEvent)
}
close() {
this.element.style.display = 'none'
const event = new CustomEvent('modal:closed', {
detail: { modalId: this.element.id },
bubbles: true,
})
this.element.dispatchEvent(event)
}
}
// Використання
const modal = new Modal(document.getElementById('myModal'))
// Слухаємо події
modal.element.addEventListener('modal:before-open', (e) => {
console.log('Модальне вікно відкривається:', e.detail)
// Можна скасувати
if (!confirm('Відкрити модальне вікно?')) {
e.preventDefault()
}
})
modal.element.addEventListener('modal:opened', (e) => {
console.log('Модальне вікно відкрито:', e.detail)
analytics.track('modal-opened', e.detail)
})
modal.element.addEventListener('modal:closed', (e) => {
console.log('Модальне вікно закрито:', e.detail)
})
modal.open()
preventDefault()Для користувацьких подій можна використовувати preventDefault(), але потрібно встановити cancelable: true:
const event = new CustomEvent('my-event', {
cancelable: true, // ✅ Обов'язково!
})
element.addEventListener('my-event', (e) => {
e.preventDefault() // Тепер працює
})
const wasNotCancelled = element.dispatchEvent(event)
if (!wasNotCancelled) {
console.log('Подію скасовано!')
}
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8" />
<title>Кролик, що ховається</title>
<style>
#rabbit {
font-size: 20px;
font-family: monospace;
white-space: pre;
background: #f0f0f0;
padding: 20px;
border-radius: 10px;
display: inline-block;
transition: opacity 0.5s;
}
#rabbit.hidden {
opacity: 0;
}
</style>
</head>
<body>
<h1>Кролик, що ховається</h1>
<pre id="rabbit">
|\\ /|
\\|_|/
/. .\\
=\\_Y_/=
{>o<}
</pre
>
<button onclick="hide()">Сховати кролика</button>
<button onclick="show()">Показати кролика</button>
<script>
const rabbit = document.getElementById('rabbit')
function hide() {
// Генеруємо подію "hide" з можливістю скасування
const event = new CustomEvent('hide', {
cancelable: true, // ✅ Можна скасувати через preventDefault
})
const wasNotCancelled = rabbit.dispatchEvent(event)
if (!wasNotCancelled) {
alert('🐰 Обробник запобіг приховуванню! Кролик залишається.')
} else {
rabbit.classList.add('hidden')
console.log('🐰 Кролик сховався')
}
}
function show() {
rabbit.classList.remove('hidden')
rabbit.dispatchEvent(
new CustomEvent('show', {
bubbles: true,
}),
)
}
// Обробник, що може скасувати приховування
rabbit.addEventListener('hide', (event) => {
const shouldPrevent = confirm('🐰 Кролик хоче сховатися. Дозволити?')
if (!shouldPrevent) {
event.preventDefault() // ❌ Скасовуємо приховування
console.log('❌ Приховування скасовано користувачем')
}
})
// Аналітика (завжди спрацює)
rabbit.addEventListener('hide', () => {
console.log('📊 [Аналітика] Спроба сховати кролика')
})
rabbit.addEventListener('show', () => {
console.log('📊 [Аналітика] Кролик показаний')
})
</script>
</body>
</html>
Як це працює:
hideconfirm() — користувач обираєpreventDefault() → dispatchEvent() повертає falseЯкщо під час обробки події запускається інша подія (через dispatchEvent), вона обробляється синхронно (одразу):
<button id="menu">Клікни мене</button>
<script>
const menu = document.getElementById('menu')
menu.addEventListener('click', () => {
console.log('1️⃣ Початок обробки click')
// Запускаємо вкладену подію
menu.dispatchEvent(
new CustomEvent('menu-open', {
bubbles: true,
}),
)
console.log('3️⃣ Кінець обробки click')
})
document.addEventListener('menu-open', () => {
console.log('2️⃣ Обробка menu-open (вкладена)')
})
// Результат:
// 1️⃣ Початок обробки click
// 2️⃣ Обробка menu-open (вкладена)
// 3️⃣ Кінець обробки click
</script>
Порядок виконання:
click handler (початок)
↓
dispatchEvent('menu-open')
↓
menu-open handler (СИНХРОННО!)
↓
menu-open завершено
↓
click handler (продовження)
Якщо потрібно відкласти подію до завершення поточного обробника:
menu.addEventListener('click', () => {
console.log('1️⃣ Початок')
// Відкладаємо подію
setTimeout(() => {
menu.dispatchEvent(
new CustomEvent('menu-open', {
bubbles: true,
}),
)
}, 0) // Навіть 0ms створює асинхронність!
console.log('2️⃣ Кінець')
})
document.addEventListener('menu-open', () => {
console.log('3️⃣ menu-open (асинхронно)')
})
// Результат:
// 1️⃣ Початок
// 2️⃣ Кінець
// 3️⃣ menu-open (асинхронно)
Різниця:
setTimeout: 1 → 2 (вкладена) → 3setTimeout: 1 → 3 → 2 (пізніше)Створимо глобальну шину подій для комунікації між модулями:
// EventBus — централізована система подій
class EventBus {
constructor() {
this.events = new EventTarget() // Використовуємо вбудований EventTarget
}
// Підписка на подію
on(eventName, callback) {
this.events.addEventListener(eventName, callback)
console.log(`✅ Підписка на "${eventName}"`)
}
// Відписка від події
off(eventName, callback) {
this.events.removeEventListener(eventName, callback)
console.log(`❌ Відписка від "${eventName}"`)
}
// Ініціювання події
emit(eventName, data = {}) {
const event = new CustomEvent(eventName, {
detail: data,
bubbles: false, // Не спливає (локальна шина)
})
this.events.dispatchEvent(event)
console.log(`📢 Ініційовано "${eventName}":`, data)
}
// Одноразова підписка
once(eventName, callback) {
const wrapper = (e) => {
callback(e)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
// Створюємо глобальну шину
const eventBus = new EventBus()
// === ПРИКЛАД ВИКОРИСТАННЯ ===
// Модуль аналітики
eventBus.on('user:login', (e) => {
console.log('📊 Аналітика: Користувач увійшов', e.detail)
// analytics.track('login', e.detail);
})
// Модуль UI
eventBus.on('user:login', (e) => {
console.log(`🎨 UI: Показуємо привітання для ${e.detail.username}`)
// showWelcomeMessage(e.detail.username);
})
// Модуль локального сховища
eventBus.on('user:login', (e) => {
console.log('💾 Storage: Зберігаємо токен', e.detail.token)
// localStorage.setItem('token', e.detail.token);
})
// Симуляція входу користувача
console.log('\n--- Симуляція входу ---')
eventBus.emit('user:login', {
username: 'John',
role: 'admin',
token: 'abc123xyz',
})
// Одноразова подія
eventBus.once('app:ready', () => {
console.log('🚀 Додаток готовий! (спрацює лише раз)')
})
eventBus.emit('app:ready')
eventBus.emit('app:ready') // Не спрацює
Результат:
✅ Підписка на "user:login"
✅ Підписка на "user:login"
✅ Підписка на "user:login"
✅ Підписка на "app:ready"
--- Симуляція входу ---
📢 Ініційовано "user:login": {username: "John", role: "admin", token: "abc123xyz"}
📊 Аналітика: Користувач увійшов {username: "John", ...}
🎨 UI: Показуємо привітання для John
💾 Storage: Зберігаємо токен abc123xyz
📢 Ініційовано "app:ready": {}
🚀 Додаток готовий! (спрацює лише раз)
❌ Відписка від "app:ready"
📢 Ініційовано "app:ready": {}
(нічого не відбувається — обробник видалено)
// Погана практика — імітація кліку
function triggerSave() {
saveButton.dispatchEvent(new Event('click'))
}
// Хороша практика — виклик функції
function triggerSave() {
handleSaveClick() // Викликаємо функцію напряму
}
// E2E тести
test('button click triggers action', () => {
button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(result).toBe('success')
})
// Бібліотека очікує реальний клік
legacyLibrary.init() // Слухає тільки click події
triggerButton.dispatchEvent(new Event('click')) // Запускаємо
// Демо/туторіали
function showDemo() {
setTimeout(() => {
menuButton.dispatchEvent(new Event('click'))
}, 1000)
}
Event() — базовий для будь-яких подійMouseEvent, KeyboardEvent — для специфічних типівCustomEvent() — для власних подій з данимиelement.dispatchEvent(event)
// Повертає: true (не скасовано) / false (скасовано)
bubbles: true — подія спливаєcancelable: true — можна скасувати через preventDefault()new CustomEvent('my-event', {
detail: { key: 'value' },
})
event.isTrusted = true — реальна дія користувачаevent.isTrusted = false — створена кодомВикористовуйте setTimeout(() => dispatchEvent(...), 0) для асинхронності
Використовуйте для компонентів, модулів, слабкого зчеплення
Викликайте функції напряму, а не через події
tab:before-change — перед зміною (можна скасувати)tab:changed — після зміни (з даними про старий/новий таб)CustomEvent з detailcancelable: true для tab:before-changeEventBus:clear(eventName) — видалити всі обробники подіїdrag:start — початок перетягуванняdrag:move — рух (з координатами)drag:end — кінець (з даними про drop-зону)MouseEvent для передачі координатbubbles: true)mousedown, mousemove, mouseup📚 MDN Web Docs
CustomEvent — повна документація
EventTarget.dispatchEvent() — запуск подій
🎓 Специфікація W3C
Вітаємо! 🎉 Ви завершили серію статей про події JavaScript. Тепер ви володієте повним арсеналом знань про події браузера!