Shadow DOM создаёт изолированную область внутри веб-компонента, предоставляя локальный DOM, который защищён от внешних стилей и скриптов. Это создаёт особенности при работе с событиями, особенно когда используется библиотека Haunted для построения компонентов на основе хуков. Понимание механизма всплытия событий в Shadow DOM является критическим для корректной архитектуры компонентов.
В обычном DOM событие, инициированное на элементе, проходит через три стадии:
document к целевому элементу.В Shadow DOM фаза всплытия работает с особенностями: события,
инициированные внутри теневого корня, не всплывают наружу
автоматически, если не указано свойство
composed: true при создании события.
composed: trueВ Haunted для передачи пользовательских событий из внутреннего
состояния компонента наружу используется CustomEvent.
Пример:
import { html, component, useState } from 'haunted';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
const event = new CustomEvent('counter-incremented', {
detail: { count: count + 1 },
bubbles: true,
composed: true // позволяет событию выйти из Shadow DOM
});
this.dispatchEvent(event);
};
return html`
<div>
<p>Счётчик: ${count}</p>
<button @click=${increment}>Увеличить</button>
</div>
`;
}
customElements.define('x-counter', component(Counter));
Ключевой момент: без composed: true
событие останется внутри Shadow DOM, и внешний код не сможет его
отловить через addEventListener на компоненте.
После того как событие становится composed, его можно слушать так же, как обычное событие на пользовательском компоненте:
const counter = document.querySelector('x-counter');
counter.addEventListener('counter-incremented', (e) => {
console.log('Новое значение счётчика:', e.detail.count);
});
Haunted не изменяет стандартное поведение событий, поэтому все методы
addEventListener работают корректно. Отличие только в том,
что события внутри компонента нужно создавать с
bubbles: true и composed: true.
Многие стандартные события, например click,
input, уже настроены как composed, поэтому
они автоматически всплывают за пределы Shadow DOM. Пример с Haunted:
function TextInput() {
return html`<input @input=${e => console.log(e.target.value)} />`;
}
customElements.define('x-text-input', component(TextInput));
Событие input будет всплывать наружу, что позволяет
родительскому компоненту реагировать на него без дополнительных
настроек.
Когда компонент Haunted содержит внутри другие компоненты с Shadow DOM, важно помнить:
Пример повторной генерации события:
function ChildComponent() {
const notifyParent = (e) => {
const event = new CustomEvent('child-event', {
detail: e.detail,
bubbles: true,
composed: true
});
this.dispatchEvent(event);
};
return html`<button @click=${(e) => notifyParent(e)}>Нажать</button>`;
}
customElements.define('x-child', component(ChildComponent));
Такой подход гарантирует корректную коммуникацию между компонентами даже через несколько уровней Shadow DOM.
composed: true для всех событий,
которые должны достигать родительских компонентов.bubbles: true, если
требуется всплытие через дерево компонентов.click,
input, change).Использование хуков, таких как useState и
useEffect, не меняет правила всплытия. Однако хуки
позволяют создавать события реактивно при изменении
состояния:
function Toggle() {
const [on, setOn] = useState(false);
useEffect(() => {
const event = new CustomEvent('toggle-changed', {
detail: { on },
bubbles: true,
composed: true
});
this.dispatchEvent(event);
}, [on]);
return html`<button @click=${() => setOn(!on)}>${on ? 'Вкл' : 'Выкл'}</button>`;
}
customElements.define('x-toggle', component(Toggle));
Такой подход позволяет компонентах Haunted автоматически уведомлять внешнее окружение об изменениях состояния без ручного управления событиями в каждом обработчике.
В совокупности эти механизмы создают мощную модель взаимодействия компонентов внутри Shadow DOM, сохраняя изоляцию стилей и поведения, но позволяя гибко передавать события наружу.