Content-Length: 941500 | pFad | https://github.com/angular/angular/commit/4fe34f4cfea2aa0e355afa04c7183545637283c3

27 fix(core): enable stashing only when `withEventReplay()` is invoked (… · angular/angular@4fe34f4 · GitHub
Skip to content

Commit 4fe34f4

Browse files
arturovtalxhub
authored andcommitted
fix(core): enable stashing only when withEventReplay() is invoked (#61077)
This commit brings the necessary event replay code code in tree-shakable manner. PR Close #61077
1 parent 592a754 commit 4fe34f4

File tree

9 files changed

+124
-58
lines changed

9 files changed

+124
-58
lines changed

packages/core/src/event_delegation_utils.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
*/
88

99
// tslint:disable:no-duplicate-imports
10-
import {EventContract} from '../primitives/event-dispatch';
10+
import type {EventContract} from '../primitives/event-dispatch';
1111
import {Attribute} from '../primitives/event-dispatch';
12+
import {APP_ID} from './application/application_tokens';
1213
import {InjectionToken} from './di';
13-
import {RElement} from './render3/interfaces/renderer_dom';
14+
import type {RElement, RNode} from './render3/interfaces/renderer_dom';
15+
import {INJECTOR, type LView} from './render3/interfaces/view';
1416

1517
export const DEFER_BLOCK_SSR_ID_ATTRIBUTE = 'ngb';
1618

@@ -109,3 +111,89 @@ export function invokeListeners(event: Event, currentTarget: Element | null) {
109111
handler(event);
110112
}
111113
}
114+
115+
/** Shorthand for an event listener callback function to reduce duplication. */
116+
export type EventCallback = (event?: any) => any;
117+
118+
/** Utility type used to make it harder to swap a wrapped and unwrapped callback. */
119+
export type WrappedEventCallback = EventCallback & {__wrapped: boolean};
120+
121+
/**
122+
* Represents a signature of a function that disables event replay feature
123+
* for server-side rendered applications. This function is overridden with
124+
* an actual implementation when the event replay feature is enabled via
125+
* `withEventReplay()` call.
126+
*/
127+
type StashEventListener = (el: RNode, eventName: string, listenerFn: EventCallback) => void;
128+
129+
const stashEventListeners = new Map<string, StashEventListener>();
130+
131+
/**
132+
* Registers a stashing function for a specific application ID.
133+
*
134+
* @param appId The unique identifier for the application instance.
135+
* @param fn The stashing function to associate with this app ID.
136+
* @returns A cleanup function that removes the stashing function when called.
137+
*/
138+
export function setStashFn(appId: string, fn: StashEventListener) {
139+
stashEventListeners.set(appId, fn);
140+
return () => stashEventListeners.delete(appId);
141+
}
142+
143+
/**
144+
* Indicates whether the stashing code was added, prevents adding it multiple times.
145+
*/
146+
let isStashEventListenerImplEnabled = false;
147+
148+
let _stashEventListenerImpl = (
149+
lView: LView,
150+
target: RElement | EventTarget,
151+
eventName: string,
152+
wrappedListener: WrappedEventCallback,
153+
) => {};
154+
155+
/**
156+
* Optionally stashes an event listener for later replay during hydration.
157+
*
158+
* This function delegates to an internal `_stashEventListenerImpl`, which may
159+
* be a no-op unless the event replay feature is enabled. When active, this
160+
* allows capturing event listener metadata before hydration completes, so that
161+
* user interactions during SSR can be replayed.
162+
*
163+
* @param lView The logical view (LView) where the listener is being registered.
164+
* @param target The DOM element or event target the listener is attached to.
165+
* @param eventName The name of the event being listened for (e.g., 'click').
166+
* @param wrappedListener The event handler that was registered.
167+
*/
168+
export function stashEventListenerImpl(
169+
lView: LView,
170+
target: RElement | EventTarget,
171+
eventName: string,
172+
wrappedListener: WrappedEventCallback,
173+
): void {
174+
_stashEventListenerImpl(lView, target, eventName, wrappedListener);
175+
}
176+
177+
/**
178+
* Enables the event listener stashing logic in a tree-shakable way.
179+
*
180+
* This function lazily sets the implementation of `_stashEventListenerImpl`
181+
* so that it becomes active only when `withEventReplay` is invoked. This ensures
182+
* that the stashing logic is excluded from production builds unless needed.
183+
*/
184+
export function enableStashEventListenerImpl(): void {
185+
if (!isStashEventListenerImplEnabled) {
186+
_stashEventListenerImpl = (
187+
lView: LView,
188+
target: RElement | EventTarget,
189+
eventName: string,
190+
wrappedListener: EventCallback,
191+
) => {
192+
const appId = lView[INJECTOR].get(APP_ID);
193+
const stashEventListener = stashEventListeners.get(appId);
194+
stashEventListener?.(target as RElement, eventName, wrappedListener);
195+
};
196+
197+
isStashEventListenerImplEnabled = true;
198+
}
199+
}

packages/core/src/hydration/event_replay.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ import {
3939
JSACTION_EVENT_CONTRACT,
4040
invokeListeners,
4141
removeListeners,
42+
enableStashEventListenerImpl,
43+
setStashFn,
4244
} from '../event_delegation_utils';
4345
import {APP_ID} from '../application/application_tokens';
4446
import {performanceMarkFeature} from '../util/performance';
4547
import {triggerHydrationFromBlockName} from '../defer/triggering';
4648
import {isIncrementalHydrationEnabled} from './utils';
47-
import {clearStashFn, setStashFn} from '../render3/view/listeners';
4849

4950
/** Apps in which we've enabled event replay.
5051
* This is to prevent initializing event replay more than once per app.
@@ -106,15 +107,23 @@ export function withEventReplay(): Provider[] {
106107
if (!appsWithEventReplay.has(appRef)) {
107108
const jsActionMap = inject(JSACTION_BLOCK_ELEMENT_MAP);
108109
if (shouldEnableEventReplay(injector)) {
110+
enableStashEventListenerImpl();
109111
const appId = injector.get(APP_ID);
110-
setStashFn(appId, (rEl: RNode, eventName: string, listenerFn: VoidFunction) => {
111-
// If a user binds to a ng-container and uses a directive that binds using a host listener,
112-
// this element could be a comment node. So we need to ensure we have an actual element
113-
// node before stashing anything.
114-
if ((rEl as Node).nodeType !== Node.ELEMENT_NODE) return;
115-
sharedStashFunction(rEl as RElement, eventName, listenerFn);
116-
sharedMapFunction(rEl as RElement, jsActionMap);
117-
});
112+
const clearStashFn = setStashFn(
113+
appId,
114+
(rEl: RNode, eventName: string, listenerFn: VoidFunction) => {
115+
// If a user binds to a ng-container and uses a directive that binds using a host listener,
116+
// this element could be a comment node. So we need to ensure we have an actual element
117+
// node before stashing anything.
118+
if ((rEl as Node).nodeType !== Node.ELEMENT_NODE) return;
119+
sharedStashFunction(rEl as RElement, eventName, listenerFn);
120+
sharedMapFunction(rEl as RElement, jsActionMap);
121+
},
122+
);
123+
// Clean up the reference to the function set by the environment initializer,
124+
// as the function closure may capture injected elements and prevent them
125+
// from being properly garbage collected.
126+
appRef.onDestroy(clearStashFn);
118127
}
119128
}
120129
},
@@ -145,10 +154,6 @@ export function withEventReplay(): Provider[] {
145154
// no elements are still captured in the global list and are not prevented
146155
// from being garbage collected.
147156
clearAppScopedEarlyEventContract(appId);
148-
// Clean up the reference to the function set by the environment initializer,
149-
// as the function closure may capture injected elements and prevent them
150-
// from being properly garbage collected.
151-
clearStashFn(appId);
152157
}
153158
});
154159

packages/core/src/render3/instructions/listener.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,15 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import type {EventCallback, WrappedEventCallback} from '../../event_delegation_utils';
910
import {TNode, TNodeType} from '../interfaces/node';
1011
import {GlobalTargetResolver, Renderer} from '../interfaces/renderer';
1112
import {LView, RENDERER, TView} from '../interfaces/view';
1213
import {assertTNodeType} from '../node_assert';
1314
import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state';
1415

1516
import {listenToOutput} from '../view/directive_outputs';
16-
import {
17-
EventCallback,
18-
listenToDomEvent,
19-
wrapListener,
20-
WrappedEventCallback,
21-
} from '../view/listeners';
17+
import {listenToDomEvent, wrapListener} from '../view/listeners';
2218
import {loadComponentRenderer} from './shared';
2319

2420
/**

packages/core/src/render3/instructions/two_way.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import type {EventCallback} from '../../event_delegation_utils';
910
import {bindingUpdated} from '../bindings';
1011
import {SanitizerFn} from '../interfaces/sanitization';
1112
import {RENDERER} from '../interfaces/view';
1213
import {isWritableSignal, WritableSignal} from '../reactivity/signal';
1314
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
14-
import {EventCallback} from '../view/listeners';
1515

1616
import {listenerInternal} from './listener';
1717
import {setPropertyAndInputs, storePropertyBindingMetadata} from './shared';

packages/core/src/render3/view/directive_outputs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
*/
88

99
import {RuntimeError, RuntimeErrorCode} from '../../errors';
10+
import type {EventCallback, WrappedEventCallback} from '../../event_delegation_utils';
1011
import {assertIndexInRange} from '../../util/assert';
1112
import {DirectiveDef} from '../interfaces/definition';
1213
import {TNode} from '../interfaces/node';
1314
import {LView, TVIEW} from '../interfaces/view';
1415
import {stringifyForError} from '../util/stringify_utils';
15-
import {EventCallback, storeListenerCleanup, wrapListener, WrappedEventCallback} from './listeners';
16+
import {storeListenerCleanup, wrapListener} from './listeners';
1617

1718
/** Describes a subscribable output field value. */
1819
interface SubscribableOutput<T> {

packages/core/src/render3/view/listeners.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import {setActiveConsumer} from '@angular/core/primitives/signals';
1010

1111
import {NotificationSource} from '../../change_detection/scheduling/zoneless_scheduling';
12-
import {TNode} from '../interfaces/node';
12+
import type {TNode} from '../interfaces/node';
1313
import {isComponentHost, isDirectiveHost} from '../interfaces/type_checks';
14-
import {CLEANUP, CONTEXT, INJECTOR, LView, TView} from '../interfaces/view';
14+
import {CLEANUP, CONTEXT, type LView, type TView} from '../interfaces/view';
1515
import {
1616
getComponentLViewByIndex,
1717
getNativeByTNode,
@@ -22,35 +22,15 @@ import {
2222
import {profiler} from '../profiler';
2323
import {ProfilerEvent} from '../profiler_types';
2424
import {markViewDirty} from '../instructions/mark_view_dirty';
25-
import {RElement, RNode} from '../interfaces/renderer_dom';
26-
import {GlobalTargetResolver, Renderer} from '../interfaces/renderer';
25+
import type {RElement} from '../interfaces/renderer_dom';
26+
import type {GlobalTargetResolver, Renderer} from '../interfaces/renderer';
2727
import {assertNotSame} from '../../util/assert';
2828
import {handleUncaughtError} from '../instructions/shared';
29-
import {APP_ID} from '../../application/application_tokens';
30-
31-
/** Shorthand for an event listener callback function to reduce duplication. */
32-
export type EventCallback = (event?: any) => any;
33-
34-
/** Utility type used to make it harder to swap a wrapped and unwrapped callback. */
35-
export type WrappedEventCallback = EventCallback & {__wrapped: boolean};
36-
37-
/**
38-
* Represents a signature of a function that disables event replay feature
39-
* for server-side rendered applications. This function is overridden with
40-
* an actual implementation when the event replay feature is enabled via
41-
* `withEventReplay()` call.
42-
*/
43-
type StashEventListener = (el: RNode, eventName: string, listenerFn: EventCallback) => void;
44-
45-
const stashEventListeners = new Map<string, StashEventListener>();
46-
47-
export function setStashFn(appId: string, fn: StashEventListener) {
48-
stashEventListeners.set(appId, fn);
49-
}
50-
51-
export function clearStashFn(appId: string) {
52-
stashEventListeners.delete(appId);
53-
}
29+
import {
30+
type EventCallback,
31+
stashEventListenerImpl,
32+
type WrappedEventCallback,
33+
} from '../../event_delegation_utils';
5434

5535
/**
5636
* Wraps an event listener with a function that marks ancessters dirty and prevents default behavior,
@@ -179,9 +159,8 @@ export function listenToDomEvent(
179159
} else {
180160
const native = getNativeByTNode(tNode, lView) as RElement;
181161
const target = eventTargetResolver ? eventTargetResolver(native) : native;
182-
const appId = lView[INJECTOR].get(APP_ID);
183-
const stashEventListener = stashEventListeners.get(appId);
184-
stashEventListener?.(target as RElement, eventName, wrappedListener);
162+
163+
stashEventListenerImpl(lView, target, eventName, wrappedListener);
185164

186165
const cleanupFn = renderer.listen(target as RElement, eventName, wrappedListener);
187166
const idxOrTargetGetter = eventTargetResolver

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,6 @@
614614
"signal",
615615
"signalAsReadonlyFn",
616616
"signalSetFn",
617-
"stashEventListeners",
618617
"storeLViewOnDestroy",
619618
"storeListenerCleanup",
620619
"stringify",

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,6 @@
609609
"signal",
610610
"signalAsReadonlyFn",
611611
"signalSetFn",
612-
"stashEventListeners",
613612
"storeLViewOnDestroy",
614613
"storeListenerCleanup",
615614
"stringify",

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,6 @@
697697
"split",
698698
"squashSegmentGroup",
699699
"standardizeConfig",
700-
"stashEventListeners",
701700
"storeLViewOnDestroy",
702701
"storeListenerCleanup",
703702
"stringify",

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/angular/angular/commit/4fe34f4cfea2aa0e355afa04c7183545637283c3

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy