Which @angular/* package(s) are relevant/related to the feature request?
core
Description
@HostListener supports global event targets such as document:, window: and body::
@HostListener('document:click', ['$event'])
onClick(event: MouseEvent): void {
// ...
}
There is currently no equivalent target for the root node of the directive/component host element (hostElement.getRootNode()).
This becomes important for directives and components that can run both in the regular document and inside Shadow DOM. Listening on document is not equivalent to listening on the current root: when an element is rendered inside a ShadowRoot, the natural event scope can be that shadow root rather than the owner document.
Today this requires dropping the declarative @HostListener API and manually resolving the root and managing the listener lifecycle, for example:
private readonly element = inject(ElementRef<HTMLElement>);
private readonly destroyRef = inject(DestroyRef);
constructor() {
const root = this.element.nativeElement.getRootNode();
const listener = (event: Event) => this.onClick(event);
root.addEventListener('click', listener);
this.destroyRef.onDestroy(() => root.removeEventListener('click', listener));
}
private onClick(event: Event): void {
// ...
}
Proposed solution
Add a root: event target which resolves to getRootNode() of the host element:
@HostListener('root:click', ['$event'])
onClick(event: MouseEvent): void {
// ...
}
Conceptually, Angular would resolve root: at directive/component creation time as:
hostElement.getRootNode()
This means:
- for a regular element in the document, the target is
Document;
- for an element inside Shadow DOM, the target is its
ShadowRoot.
It would make listeners naturally follow the DOM tree in which the host is actually rendered and would provide a declarative counterpart to manually calling getRootNode().addEventListener(...).
If the same event-target syntax is shared with template event bindings, supporting (root:click) there as well would also be useful and consistent.
Alternatives considered
1. @HostListener('document:click')
This does not express the same semantics. A reusable directive should not have to know whether its host lives directly in Document or in a ShadowRoot.
2. Manual addEventListener / removeEventListener
This works, but requires resolving getRootNode(), retaining the listener reference, and handling destruction manually. It loses the concise declarative lifecycle management provided by @HostListener.
3. RxJS fromEvent(hostElement.getRootNode(), 'click')
This also works, but similarly requires imperative setup and teardown (takeUntilDestroyed, etc.) for something that otherwise maps naturally to @HostListener.
4. A custom EventManagerPlugin
Applications can implement their own event target syntax, but this is disproportionate for a standard DOM concept (Node.getRootNode()) and makes reusable libraries depend on application-level event manager configuration.
Additional
If this approach is approved, I’d be happy to submit a PR with the implementation and tests.
Which @angular/* package(s) are relevant/related to the feature request?
core
Description
@HostListenersupports global event targets such asdocument:,window:andbody::There is currently no equivalent target for the root node of the directive/component host element (
hostElement.getRootNode()).This becomes important for directives and components that can run both in the regular document and inside Shadow DOM. Listening on
documentis not equivalent to listening on the current root: when an element is rendered inside aShadowRoot, the natural event scope can be that shadow root rather than the owner document.Today this requires dropping the declarative
@HostListenerAPI and manually resolving the root and managing the listener lifecycle, for example:Proposed solution
Add a
root:event target which resolves togetRootNode()of the host element:Conceptually, Angular would resolve
root:at directive/component creation time as:This means:
Document;ShadowRoot.It would make listeners naturally follow the DOM tree in which the host is actually rendered and would provide a declarative counterpart to manually calling
getRootNode().addEventListener(...).If the same event-target syntax is shared with template event bindings, supporting
(root:click)there as well would also be useful and consistent.Alternatives considered
1.
@HostListener('document:click')This does not express the same semantics. A reusable directive should not have to know whether its host lives directly in
Documentor in aShadowRoot.2. Manual
addEventListener/removeEventListenerThis works, but requires resolving
getRootNode(), retaining the listener reference, and handling destruction manually. It loses the concise declarative lifecycle management provided by@HostListener.3. RxJS
fromEvent(hostElement.getRootNode(), 'click')This also works, but similarly requires imperative setup and teardown (
takeUntilDestroyed, etc.) for something that otherwise maps naturally to@HostListener.4. A custom
EventManagerPluginApplications can implement their own event target syntax, but this is disproportionate for a standard DOM concept (
Node.getRootNode()) and makes reusable libraries depend on application-level event manager configuration.Additional
If this approach is approved, I’d be happy to submit a PR with the implementation and tests.