Пользовательские события: CustomEvent

CustomEvent — API браузера для создания и диспетчеризации произвольных DOM-событий с пользовательскими данными, позволяющее выстраивать слабосвязанную событийную коммуникацию между компонентами.

Зачем нужно

В сложных приложениях компонентам нужно общаться, не зная друг о друге. CustomEvent + dispatchEvent реализует паттерн Observer на уровне DOM: один компонент генерирует событие, другие подписываются через addEventListener — никакой прямой связи между ними.

Где используется

  • Коммуникация между Web Components
  • Vanilla JS приложения без фреймворков
  • Уведомления об изменении состояния (корзина, авторизация)
  • Кросс-компонентные события (form submit → notification)

Создание и диспетчеризация

// Создаём пользовательское событие
const event = new CustomEvent('user:login', {
  detail: {          // пользовательские данные
    userId: 42,
    name: 'Иван',
    role: 'admin'
  },
  bubbles: true,     // всплывает по DOM
  cancelable: true,  // можно отменить через preventDefault
  composed: false    // не пересекает Shadow DOM (для Web Components)
});

// Диспетчеризуем на элемент
document.dispatchEvent(event);

// Или на конкретный элемент
const btn = document.getElementById('login-btn');
btn.dispatchEvent(event);

Подписка на пользовательское событие

// Слушатель
document.addEventListener('user:login', (event) => {
  const { userId, name, role } = event.detail;
  console.log(`Вошёл пользователь: ${name} (${role}), id=${userId}`);
  showWelcomeMessage(name);
});

// Удаление слушателя
const handler = (e) => console.log(e.detail);
document.addEventListener('cart:update', handler);
document.removeEventListener('cart:update', handler);

Паттерн EventBus

Глобальная шина событий для коммуникации между любыми компонентами:

const EventBus = {
  _bus: document.createElement('div'),

  on(event, callback) {
    this._bus.addEventListener(event, (e) => callback(e.detail));
    return this; // для цепочки
  },

  off(event, callback) {
    this._bus.removeEventListener(event, callback);
  },

  emit(event, data = {}) {
    this._bus.dispatchEvent(new CustomEvent(event, { detail: data }));
  }
};

// Использование
EventBus.on('cart:add', ({ productId, qty }) => {
  updateCartUI(productId, qty);
  saveToLocalStorage(productId, qty);
});

EventBus.on('cart:add', ({ productId }) => {
  analytics.track('add_to_cart', { productId });
});

// Из любого места приложения:
EventBus.emit('cart:add', { productId: 101, qty: 2 });

Отмена события (cancelable)

document.addEventListener('form:submit', (event) => {
  if (!validateForm) {
    event.preventDefault(); // отменяем если валидация не прошла
  }
});

const submitEvent = new CustomEvent('form:submit', {
  cancelable: true,
  bubbles: true
});

const wasCancelled = !element.dispatchEvent(submitEvent);
if (!wasCancelled) {
  // продолжить отправку
  sendData;
}

Web Components и composed

class MyComponent extends HTMLElement {
  connectedCallback {
    this.shadowRoot.querySelector('button').addEventListener('click', () => {
      // composed: true — событие пересекает Shadow DOM boundary
      this.dispatchEvent(new CustomEvent('my-click', {
        detail: { timestamp: Date.now() },
        bubbles: true,
        composed: true // без этого родитель не получит событие!
      }));
    });
  }
}

Частые ошибки

1. Отсутствие bubbles: true при подписке на document

const event = new CustomEvent('my-event'); // bubbles: false по умолчанию

div.dispatchEvent(event);
document.addEventListener('my-event', handler); // НЕ сработает!

// Исправление:
new CustomEvent('my-event', { bubbles: true });

2. Передача данных не через detail

// Нельзя добавлять свойства напрямую к событию
const bad = new CustomEvent('test');
bad.myData = 'value'; // игнорируется или не доступно

// Правильно:
const good = new CustomEvent('test', { detail: { myData: 'value' } });

3. Утечка памяти — не удалён слушатель

// Если компонент удалён из DOM, но слушатель остался — утечка
function init() {
  const handler = (e) => console.log(e.detail);
  document.addEventListener('app:update', handler);
  // Нужно сохранить ссылку на handler и удалить при cleanup
}

Связанные темы

Ресурсы