Диспатч пользовательских событий

Haunted — это библиотека для создания веб-компонентов с реактивностью, вдохновлённая концепциями React, но использующая нативные Web Components. Одной из ключевых возможностей при работе с веб-компонентами является обработка и генерация пользовательских событий, что позволяет компонентам взаимодействовать друг с другом и с родительскими элементами без прямого связывания через свойства или методы.


Основы пользовательских событий

В DOM существует встроенный механизм событий. Пользовательские события создаются с помощью конструктора CustomEvent:

const event = new CustomEvent('my-event', {
  detail: { message: 'Hello' },
  bubbles: true,
  composed: true
});
element.dispatchEvent(event);

Пояснение параметров:

  • detail — объект с данными, которые передаются слушателю.
  • bubbles — указывает, будет ли событие всплывать через DOM.
  • composed — позволяет событию пройти через shadow DOM границы компонента.

В Haunted этот подход используется в сочетании с хуками и функциональными компонентами для создания реактивных и интерактивных компонентов.


Использование dispatchEvent внутри Haunted-компонента

Функциональный компонент в Haunted — это обычная функция, которая возвращает шаблон с помощью html из haunted. Для диспатча события внутри компонента нужно получить ссылку на корневой элемент. Обычно используют useRef или стандартный this в классовых Web Components. Пример с функциональным компонентом:

import { html, component, useRef } from 'haunted';

function Button({ label }) {
  const buttonRef = useRef();

  const handleClick = () => {
    const event = new CustomEvent('button-click', {
      detail: { label },
      bubbles: true,
      composed: true
    });
    buttonRef.current.dispatchEvent(event);
  };

  return html`
    <button @click=${handleClick} ref=${buttonRef}>${label}</button>
  `;
}

customElements.define('my-button', component(Button));

Ключевые моменты:

  • Событие создаётся при клике на кнопку.
  • Использование bubbles: true позволяет родительскому компоненту реагировать на событие.
  • composed: true обеспечивает доступ события из-за теневого DOM, что важно для скрытых компонентов.

Связывание событий между компонентами

Обычно пользовательские события используют для взаимодействия между компонентами:

function Parent() {
  const handleButtonClick = (e) => {
    console.log('Нажата кнопка с меткой:', e.detail.label);
  };

  return html`
    <my-button @button-click=${handleButtonClick} label="Click me"></my-button>
  `;
}

customElements.define('my-parent', component(Parent));
  • Событие button-click всплывает от дочернего компонента к родителю.
  • Данные из detail доступны обработчику.
  • Этот подход позволяет компонентам оставаться инкапсулированными, не вызывая методы напрямую.

Продвинутые сценарии: событие с асинхронной логикой

События могут использоваться для передачи результатов асинхронных операций:

function AsyncButton({ label }) {
  const buttonRef = useRef();

  const handleClick = async () => {
    const result = await fetchData();
    const event = new CustomEvent('data-loaded', {
      detail: { result },
      bubbles: true,
      composed: true
    });
    buttonRef.current.dispatchEvent(event);
  };

  return html`<button @click=${handleClick} ref=${buttonRef}>${label}</button>`;
}

Пояснения:

  • Асинхронные операции не нарушают работу событийной модели.
  • Родитель может подписаться на data-loaded и обработать результат без прямого вызова функции дочернего компонента.
  • Такой подход помогает создавать компоненты, полностью изолированные от логики родителя.

Настройка событий с разными уровнями инкапсуляции

Haunted позволяет комбинировать shadow DOM с событиями. Понимание параметров bubbles и composed критично:

Параметр Значение Эффект
bubbles true Событие поднимается вверх по DOM до родителя
bubbles false Событие ограничено элементом, который его диспатчит
composed true Событие проходит через shadow DOM границы
composed false Событие ограничено shadow DOM, не достигает внешнего DOM

Комбинации этих параметров позволяют контролировать видимость событий и предотвращать нежелательные утечки данных между компонентами.


Практические рекомендации

  1. Всегда использовать detail для передачи данных вместо изменения глобальных состояний.
  2. Использовать bubbles: true и composed: true для событий, которые должны быть доступны родителю.
  3. Инкапсулировать логику в компоненте и не полагаться на прямое обращение к методам родителя.
  4. Обрабатывать события на верхнем уровне с помощью @event-name=${handler}, что соответствует привычной реактивной модели Haunted.

Пример комплексного взаимодействия

function TodoItem({ task }) {
  const itemRef = useRef();

  const toggleDone = () => {
    const event = new CustomEvent('toggle-task', {
      detail: { taskId: task.id },
      bubbles: true,
      composed: true
    });
    itemRef.current.dispatchEvent(event);
  };

  return html`
    <div ref=${itemRef}>
      <input type="checkbox" @change=${toggleDone} ?checked=${task.done} />
      <span>${task.name}</span>
    </div>
  `;
}

function TodoList({ tasks }) {
  const handleToggle = (e) => {
    const id = e.detail.taskId;
    console.log('Переключить задачу с id:', id);
    // Логика обновления состояния списка задач
  };

  return html`
    <div @toggle-task=${handleToggle}>
      ${tasks.map(task => html`<todo-item .task=${task}></todo-item>`)}
    </div>
  `;
}

customElements.define('todo-item', component(TodoItem));
customElements.define('todo-list', component(TodoList));
  • Дочерний компонент TodoItem диспатчит событие при изменении состояния.
  • Родительский TodoList обрабатывает событие и обновляет список.
  • События позволяют компонентам оставаться независимыми, сохраняя реактивность и инкапсуляцию.

Пользовательские события в Haunted создают мощный механизм взаимодействия между компонентами, позволяя реализовать чистую реактивную архитектуру, где данные и события проходят через явно определённые каналы, а компоненты остаются автономными.