diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts index 7b561c341e4..eb2fb98ecc9 100644 --- a/packages/core/src/render3/instructions/listener.ts +++ b/packages/core/src/render3/instructions/listener.ts @@ -11,7 +11,6 @@ import {GlobalTargetResolver, Renderer} from '../interfaces/renderer'; import {LView, RENDERER, TView} from '../interfaces/view'; import {assertTNodeType} from '../node_assert'; import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state'; -import {getOrCreateLViewCleanup, getOrCreateTViewCleanup} from '../util/view_utils'; import {listenToOutput} from '../view/directive_outputs'; import {listenToDomEvent, wrapListener} from '../view/listeners'; @@ -93,13 +92,6 @@ export function listenerInternal( listenerFn: (e?: any) => any, eventTargetResolver?: GlobalTargetResolver, ): void { - const tCleanup = tView.firstCreatePass ? getOrCreateTViewCleanup(tView) : null; - - // When the ɵɵlistener instruction was generated and is executed we know that there is either a - // native listener or a directive output on this element. As such we we know that we will have to - // register a listener and store its cleanup function on LView. - const lCleanup = getOrCreateLViewCleanup(lView); - ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer); let processOutputs = true; @@ -113,17 +105,16 @@ export function listenerInternal( wrappedListener ??= wrapListener(tNode, lView, listenerFn); const hasCoalescedDomEvent = listenToDomEvent( tNode, + tView, lView, eventTargetResolver, - lCleanup, - tView, - eventName, - wrappedListener, - listenerFn, renderer, - tCleanup, + eventName, + listenerFn, + wrappedListener, ); + // Context: https://github.com/angular/angular/pull/30144 if (hasCoalescedDomEvent) { processOutputs = false; } diff --git a/packages/core/src/render3/view/listeners.ts b/packages/core/src/render3/view/listeners.ts index 0314c87b4ae..f1ab787d963 100644 --- a/packages/core/src/render3/view/listeners.ts +++ b/packages/core/src/render3/view/listeners.ts @@ -12,13 +12,20 @@ import {NotificationSource} from '../../change_detection/scheduling/zoneless_sch import {TNode} from '../interfaces/node'; import {isComponentHost, isDirectiveHost} from '../interfaces/type_checks'; import {CLEANUP, CONTEXT, INJECTOR, LView, TView} from '../interfaces/view'; -import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils'; +import { + getComponentLViewByIndex, + getNativeByTNode, + getOrCreateLViewCleanup, + getOrCreateTViewCleanup, + unwrapRNode, +} from '../util/view_utils'; import {profiler} from '../profiler'; import {ProfilerEvent} from '../profiler_types'; import {ErrorHandler} from '../../error_handler'; import {markViewDirty} from '../instructions/mark_view_dirty'; import {RElement, RNode} from '../interfaces/renderer_dom'; import {GlobalTargetResolver, Renderer} from '../interfaces/renderer'; +import {assertNotSame} from '../../util/assert'; /** * Contains a reference to a function that disables event replay feature @@ -103,22 +110,41 @@ function handleError(lView: LView, error: any): void { injector.get(ErrorHandler, null)?.handleError(error); } +/** + * Listen to a DOM event on a specific node. + * @param tNode TNode on which to listen. + * @param tView TView in which the node is placed. + * @param lView LView in which the node instance is placed. + * @param eventTargetResolver Resolver for global event targets. + * @param renderer Renderer to use for listening to the event. + * @param eventName Name of the event. + * @param originalListener Original listener as it was created by the compiler. Necessary for event + * coalescing. + * @param wrappedListener Listener wrapped with additional logic like marking for check and error + * handling. + * @returns Boolean indicating whether the event was bound or was coalesced into an existing + * listener. + */ export function listenToDomEvent( tNode: TNode, + tView: TView, lView: LView<{} | null>, eventTargetResolver: GlobalTargetResolver | undefined, - lCleanup: any[], - tView: TView, - eventName: string, - wrappedListener: (e?: any) => any, - originalListener: (e?: any) => any, renderer: Renderer, - tCleanup: any[] | null, -) { + eventName: string, + originalListener: (e?: any) => any, + wrappedListener: (e?: any) => any, +): boolean { + ngDevMode && + assertNotSame( + wrappedListener, + originalListener, + 'Expected wrapped and original listeners to be different.', + ); + const isTNodeDirectiveHost = isDirectiveHost(tNode); const native = getNativeByTNode(tNode, lView) as RElement; const target = eventTargetResolver ? eventTargetResolver(native) : native; - const lCleanupIndex = lCleanup.length; const idxOrTargetGetter = eventTargetResolver ? (_lView: LView) => eventTargetResolver(unwrapRNode(_lView[tNode.index])) : tNode.index; @@ -158,7 +184,9 @@ export function listenToDomEvent( } else { stashEventListener(target as RElement, eventName, wrappedListener); const cleanupFn = renderer.listen(target as RElement, eventName, wrappedListener); - + const tCleanup = tView.firstCreatePass ? getOrCreateTViewCleanup(tView) : null; + const lCleanup = getOrCreateLViewCleanup(lView); + const lCleanupIndex = lCleanup.length; lCleanup.push(wrappedListener, cleanupFn); tCleanup && tCleanup.push(eventName, idxOrTargetGetter, lCleanupIndex, lCleanupIndex + 1); }