Skip to content

Commit

Permalink
feat(core): add event listener options to renderer (#59092)
Browse files Browse the repository at this point in the history
Updates the `Renderer2.listen` signature to accept event options, as well as all adjacent types to it.

PR Close #59092
  • Loading branch information
crisbeto authored and alxhub committed Dec 10, 2024
1 parent 46f00f9 commit d010e11
Show file tree
Hide file tree
Showing 19 changed files with 143 additions and 36 deletions.
12 changes: 11 additions & 1 deletion goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,16 @@ export function linkedSignal<S, D>(options: {
equal?: ValueEqualityFn<NoInfer<D>>;
}): WritableSignal<D>;

// @public
export interface ListenerOptions {
// (undocumented)
capture?: boolean;
// (undocumented)
once?: boolean;
// (undocumented)
passive?: boolean;
}

// @public
export const LOCALE_ID: InjectionToken<string>;

Expand Down Expand Up @@ -1524,7 +1534,7 @@ export abstract class Renderer2 {
abstract destroy(): void;
destroyNode: ((node: any) => void) | null;
abstract insertBefore(parent: any, newChild: any, refChild: any, isMove?: boolean): void;
abstract listen(target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void): () => void;
abstract listen(target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void, options?: ListenerOptions): () => void;
abstract nextSibling(node: any): any;
abstract parentNode(node: any): any;
abstract removeAttribute(el: any, name: string, namespace?: string | null): void;
Expand Down
5 changes: 3 additions & 2 deletions goldens/public-api/platform-browser/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { HttpTransferCacheOptions } from '@angular/common/http';
import * as i0 from '@angular/core';
import * as i1 from '@angular/common';
import { InjectionToken } from '@angular/core';
import { ListenerOptions } from '@angular/core';
import { NgZone } from '@angular/core';
import { PlatformRef } from '@angular/core';
import { Predicate } from '@angular/core';
Expand Down Expand Up @@ -77,7 +78,7 @@ export const EVENT_MANAGER_PLUGINS: InjectionToken<EventManagerPlugin[]>;
// @public
export class EventManager {
constructor(plugins: EventManagerPlugin[], _zone: NgZone);
addEventListener(element: HTMLElement, eventName: string, handler: Function): Function;
addEventListener(element: HTMLElement, eventName: string, handler: Function, options?: ListenerOptions): Function;
getZone(): NgZone;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<EventManager, never>;
Expand All @@ -88,7 +89,7 @@ export class EventManager {
// @public
export abstract class EventManagerPlugin {
constructor(_doc: any);
abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function;
abstract addEventListener(element: HTMLElement, eventName: string, handler: Function, options?: ListenerOptions): Function;
// (undocumented)
manager: EventManager;
abstract supports(eventName: string): boolean;
Expand Down
13 changes: 10 additions & 3 deletions packages/animations/browser/src/render/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
RendererFactory2,
RendererStyleFlags2,
ɵAnimationRendererType as AnimationRendererType,
type ListenerOptions,
} from '@angular/core';
import type {AnimationEngine} from './animation_engine_next';

Expand Down Expand Up @@ -135,8 +136,13 @@ export class BaseAnimationRenderer implements Renderer2 {
this.delegate.setValue(node, value);
}

listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void {
return this.delegate.listen(target, eventName, callback);
listen(
target: any,
eventName: string,
callback: (event: any) => boolean | void,
options?: ListenerOptions,
): () => void {
return this.delegate.listen(target, eventName, callback, options);
}

protected disableAnimations(element: any, value: boolean) {
Expand Down Expand Up @@ -173,6 +179,7 @@ export class AnimationRenderer extends BaseAnimationRenderer implements Renderer
target: 'window' | 'document' | 'body' | any,
eventName: string,
callback: (event: any) => any,
options?: ListenerOptions,
): () => void {
if (eventName.charAt(0) == ANIMATION_PREFIX) {
const element = resolveElementFromTarget(target);
Expand All @@ -188,7 +195,7 @@ export class AnimationRenderer extends BaseAnimationRenderer implements Renderer
this.factory.scheduleListenerCallback(countId, callback, event);
});
}
return this.delegate.listen(target, eventName, callback);
return this.delegate.listen(target, eventName, callback, options);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/dom_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export abstract class DomAdapter {
abstract isShadowRoot(node: any): boolean;

// Used by KeyEventsPlugin
abstract onAndCancel(el: any, evt: any, listener: any): Function;
abstract onAndCancel(el: any, evt: any, listener: any, options?: any): Function;

// Used by PlatformLocation and ServerEventManagerPlugin
abstract getGlobalEventTarget(doc: Document, target: string): any;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
*/

// Public API for render
export {Renderer2, RendererFactory2} from './render/api';
export {Renderer2, RendererFactory2, ListenerOptions} from './render/api';
export {RendererStyleFlags2, RendererType2} from './render/api_flags';
12 changes: 12 additions & 0 deletions packages/core/src/render/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,14 @@ export abstract class Renderer2 {
* DOM element.
* @param eventName The event to listen for.
* @param callback A handler function to invoke when the event occurs.
* @param options Options that configure how the event listener is bound.
* @returns An "unlisten" function for disposing of this handler.
*/
abstract listen(
target: 'window' | 'document' | 'body' | any,
eventName: string,
callback: (event: any) => boolean | void,
options?: ListenerOptions,
): () => void;

/**
Expand Down Expand Up @@ -252,3 +254,13 @@ export const enum AnimationRendererType {
Regular = 0,
Delegated = 1,
}

/**
* Options that can be used to configure an event listener.
* @publicApi
*/
export interface ListenerOptions {
capture?: boolean;
once?: boolean;
passive?: boolean;
}
2 changes: 2 additions & 0 deletions packages/core/src/render3/interfaces/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {RendererStyleFlags2, RendererType2} from '../../render/api_flags';
import type {ListenerOptions} from '../../render/api';
import {TrustedHTML, TrustedScript, TrustedScriptURL} from '../../util/security/trusted_type_defs';

import {RComment, RElement, RNode, RText} from './renderer_dom';
Expand Down Expand Up @@ -68,6 +69,7 @@ export interface Renderer {
target: GlobalTargetName | RNode,
eventName: string,
callback: (event: any) => boolean | void,
options?: ListenerOptions,
): () => void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
Expand Down Expand Up @@ -263,6 +266,9 @@
{
"name": "InputFlags"
},
{
"name": "KeyEventsPlugin"
},
{
"name": "LContainerFlags"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
Expand Down Expand Up @@ -212,6 +215,9 @@
{
"name": "InputFlags"
},
{
"name": "KeyEventsPlugin"
},
{
"name": "LContainerFlags"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
Expand Down Expand Up @@ -290,6 +293,9 @@
{
"name": "ItemComponent"
},
{
"name": "KeyEventsPlugin"
},
{
"name": "LContainerFlags"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
Expand Down Expand Up @@ -176,6 +179,9 @@
{
"name": "InputFlags"
},
{
"name": "KeyEventsPlugin"
},
{
"name": "LContainerFlags"
},
Expand Down
20 changes: 15 additions & 5 deletions packages/core/test/render3/imported_renderer2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {PLATFORM_BROWSER_ID, PLATFORM_SERVER_ID} from '@angular/common/src/platform_id';
import {NgZone, RendererFactory2, RendererType2} from '@angular/core';
import {type ListenerOptions, NgZone, RendererFactory2, RendererType2} from '@angular/core';
import {NoopNgZone} from '@angular/core/src/zone/ng_zone';
import {EventManager, ɵDomRendererFactory2, ɵSharedStylesHost} from '@angular/platform-browser';
import {EventManagerPlugin} from '@angular/platform-browser/src/dom/events/event_manager';
Expand All @@ -21,13 +21,23 @@ export class SimpleDomEventsPlugin extends EventManagerPlugin {
return true;
}

override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
override addEventListener(
element: HTMLElement,
eventName: string,
handler: Function,
options?: ListenerOptions,
): Function {
let callback: EventListener = handler as EventListener;
element.addEventListener(eventName, callback, false);
return () => this.removeEventListener(element, eventName, callback);
element.addEventListener(eventName, callback, options);
return () => this.removeEventListener(element, eventName, callback, options);
}

removeEventListener(target: any, eventName: string, callback: Function): void {
removeEventListener(
target: any,
eventName: string,
callback: Function,
options?: ListenerOptions,
): void {
return target.removeEventListener.apply(target, [eventName, callback, false]);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
ɵChangeDetectionScheduler as ChangeDetectionScheduler,
ɵNotificationSource as NotificationSource,
ɵRuntimeError as RuntimeError,
Injector,
InjectionToken,
type ListenerOptions,
} from '@angular/core';
import {ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/platform-browser';

Expand Down Expand Up @@ -278,13 +278,20 @@ export class DynamicDelegationRenderer implements Renderer2 {
this.delegate.setValue(node, value);
}

listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void {
listen(
target: any,
eventName: string,
callback: (event: any) => boolean | void,
options?: ListenerOptions,
): () => void {
// We need to keep track of animation events registred by the default renderer
// So we can also register them against the animation renderer
if (this.shouldReplay(eventName)) {
this.replay!.push((renderer: Renderer2) => renderer.listen(target, eventName, callback));
this.replay!.push((renderer: Renderer2) =>
renderer.listen(target, eventName, callback, options),
);
}
return this.delegate.listen(target, eventName, callback);
return this.delegate.listen(target, eventName, callback, options);
}

private shouldReplay(propOrEventName: string): boolean {
Expand Down
7 changes: 3 additions & 4 deletions packages/platform-browser/src/browser/browser_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@ import {GenericBrowserDomAdapter} from './generic_browser_adapter';
* @security Tread carefully! Interacting with the DOM directly is dangerous and
* can introduce XSS risks.
*/
/* tslint:disable:requireParameterType no-console */
export class BrowserDomAdapter extends GenericBrowserDomAdapter {
static makeCurrent() {
setRootDomAdapter(new BrowserDomAdapter());
}

override onAndCancel(el: Node, evt: any, listener: any): Function {
el.addEventListener(evt, listener);
override onAndCancel(el: Node, evt: any, listener: any, options: any): Function {
el.addEventListener(evt, listener, options);
return () => {
el.removeEventListener(evt, listener);
el.removeEventListener(evt, listener, options);
};
}
override dispatchEvent(el: Node, evt: any) {
Expand Down
3 changes: 3 additions & 0 deletions packages/platform-browser/src/dom/dom_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
RendererType2,
ViewEncapsulation,
ɵRuntimeError as RuntimeError,
type ListenerOptions,
} from '@angular/core';

import {RuntimeErrorCode} from '../errors';
Expand Down Expand Up @@ -342,6 +343,7 @@ class DefaultDomRenderer2 implements Renderer2 {
target: 'window' | 'document' | 'body' | any,
event: string,
callback: (event: any) => boolean,
options?: ListenerOptions,
): () => void {
(typeof ngDevMode === 'undefined' || ngDevMode) &&
this.throwOnSyntheticProps &&
Expand All @@ -357,6 +359,7 @@ class DefaultDomRenderer2 implements Renderer2 {
target,
event,
this.decoratePreventDefault(callback),
options,
) as VoidFunction;
}

Expand Down
22 changes: 16 additions & 6 deletions packages/platform-browser/src/dom/events/dom_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {DOCUMENT} from '@angular/common';
import {Inject, Injectable} from '@angular/core';
import {Inject, Injectable, type ListenerOptions} from '@angular/core';

import {EventManagerPlugin} from './event_manager';

Expand All @@ -23,12 +23,22 @@ export class DomEventsPlugin extends EventManagerPlugin {
return true;
}

override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
element.addEventListener(eventName, handler as EventListener, false);
return () => this.removeEventListener(element, eventName, handler as EventListener);
override addEventListener(
element: HTMLElement,
eventName: string,
handler: Function,
options?: ListenerOptions,
): Function {
element.addEventListener(eventName, handler as EventListener, options);
return () => this.removeEventListener(element, eventName, handler as EventListener, options);
}

removeEventListener(target: any, eventName: string, callback: Function): void {
return target.removeEventListener(eventName, callback as EventListener);
removeEventListener(
target: any,
eventName: string,
callback: Function,
options?: ListenerOptions,
): void {
return target.removeEventListener(eventName, callback as EventListener, options);
}
}
Loading

0 comments on commit d010e11

Please sign in to comment.