8000 fix(core): do not activate event replay when no events are registered by AndrewKushnir · Pull Request #56509 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 39 additions & 13 deletions packages/core/src/hydration/event_replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,19 @@ import {CLEANUP, LView, TView} from '../render3/interfaces/view';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {unwrapRNode} from '../render3/util/view_utils';

import {IS_EVENT_REPLAY_ENABLED, IS_GLOBAL_EVENT_DELEGATION_ENABLED} from './tokens';
import {
EVENT_REPLAY_ENABLED_DEFAULT,
IS_EVENT_REPLAY_ENABLED,
IS_GLOBAL_EVENT_DELEGATION_ENABLED,
} from './tokens';
import {
GlobalEventDelegation,
sharedStashFunction,
removeListeners,
invokeRegisteredListeners,
} from '../event_delegation_utils';
import {APP_ID} from '../application/application_tokens';
import {performanceMarkFeature} from '../util/performance';

declare global {
var ngContracts: {[key: string]: EarlyJsactionDataContainer};
Expand All @@ -50,6 +55,16 @@ function isGlobalEventDelegationEnabled(injector: Injector) {
return injector.get(IS_GLOBAL_EVENT_DELEGATION_ENABLED, false);
}

/**
* Determines whether Event Replay feature should be activated on the client.
*/
function shouldEnableEventReplay(injector: Injector) {
return (
injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT) &&
!isGlobalEventDelegationEnabled(injector)
);
}

/**
* Returns a set of providers required to setup support for event replay.
* Requires hydration to be enabled separately.
Expand All @@ -58,19 +73,31 @@ export function withEventReplay(): Provider[] {
return [
{
provide: IS_EVENT_REPLAY_ENABLED,
useValue: true,
useFactory: () => {
let isEnabled = true;
if (isPlatformBrowser()) {
// Note: globalThis[CONTRACT_PROPERTY] may be undefined in case Event Replay feature
// is enabled, but there are no events configured in this application, in which case
// we don't activate this feature, since there are no events to replay.
const appId = inject(APP_ID);
isEnabled = !!globalThis[CONTRACT_PROPERTY]?.[appId];
}
if (isEnabled) {
performanceMarkFeature('NgEventReplay');
}
return isEnabled;
},
},
{
provide: ENVIRONMENT_INITIALIZER,
useValue: () => {
const injector = inject(Injector);
if (isGlobalEventDelegationEnabled(injector)) {
return;
if (isPlatformBrowser(injector) && shouldEnableEventReplay(injector)) {
setStashFn((rEl: RElement, eventName: string, listenerFn: VoidFunction) => {
sharedStashFunction(rEl, eventName, listenerFn);
jsactionSet.add(rEl as unknown as Element);
});
}
setStashFn((rEl: RElement, eventName: string, listenerFn: VoidFunction) => {
sharedStashFunction(rEl, eventName, listenerFn);
jsactionSet.add(rEl as unknown as Element);
});
},
multi: true,
},
Expand All @@ -81,13 +108,14 @@ export function withEventReplay(): Provider[] {
const injector = inject(Injector);
const appRef = inject(ApplicationRef);
return () => {
if (!shouldEnableEventReplay(injector)) {
return;
}

// Kick off event replay logic once hydration for the initial part
// of the application is completed. This timing is similar to the unclaimed
// dehydrated views cleanup timing.
whenStable(appRef).then(() => {
if (isGlobalEventDelegationEnabled(injector)) {
return;
}
const globalEventDelegation = injector.get(GlobalEventDelegation);
initEventReplay(globalEventDelegation, injector);
jsactionSet.forEach(removeListeners);
Expand All @@ -112,8 +140,6 @@ function getJsactionData(container: EarlyJsactionDataContainer) {
const initEventReplay = (eventDelegation: GlobalEventDelegation, injector: Injector) => {
const appId = injector.get(APP_ID);
// This is set in packages/platform-server/src/utils.ts
// Note: globalThis[CONTRACT_PROPERTY] may be undefined in case Event Replay feature
// is enabled, but there are no events configured in an application.
const container = globalThis[CONTRACT_PROPERTY]?.[appId];
const earlyJsactionData = getJsactionData(container)!;
const eventContract = (eventDelegation.eventContract = new EventContract(
Expand Down
48 changes: 46 additions & 2 deletions packages/platform-server/test/event_replay_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@
*/

import {DOCUMENT} from '@angular/common';
import {Component, destroyPlatform, getPlatform, Type} from '@angular/core';
import {
Component,
destroyPlatform,
ErrorHandler,
getPlatform,
PLATFORM_ID,
Type,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {
withEventReplay,
Expand All @@ -19,7 +26,14 @@ import {provideServerRendering} from '../public_api';
import {EVENT_DISPATCH_SCRIPT_ID, renderApplication} from '../src/utils';
import {EventPhase} from '@angular/core/primitives/event-dispatch';

import {getAppContents, hydrate, render as renderHtml, resetTViewsFor} from './dom_utils';
import {
getAppContents,
hydrate,
renderAndHydrate,
render as renderHtml,
resetTViewsFor,
} from './dom_utils';
import {CONTRACT_PROPERTY} from '@angular/core/src/hydration/event_replay';

/**
* Represents the <script> tag added by the build process to inject
Expand All @@ -38,6 +52,24 @@ function hasJSActionAttrs(content: string) {
return content.includes('jsaction="');
}

/**
* Enables strict error handler that fails a test
* if there was an error reported to the ErrorHandler.
*/
function withStrictErrorHandler() {
class StrictErrorHandler extends ErrorHandler {
override handleError(error: any): void {
fail(error);
}
}
return [
{
provide: ErrorHandler,
useClass: StrictErrorHandler,
},
];
}

describe('event replay', () => {
let doc: Document;
const originalDocument = globalThis.document;
Expand All @@ -61,6 +93,7 @@ describe('event replay', () => {

afterEach(() => {
doc.body.outerHTML = '<body></body>';
globalThis[CONTRACT_PROPERTY] = {};
});

/**
Expand Down Expand Up @@ -402,6 +435,17 @@ describe('event replay', () => {

expect(hasJSActionAttrs(ssrContents)).toBeFalse();
expect(hasEventDispatchScript(ssrContents)).toBeFalse();

resetTViewsFor(SimpleComponent);
await renderAndHydrate(doc, ssrContents, SimpleComponent, {
envProviders: [
{provide: PLATFORM_ID, useValue: 'browser'},
// This ensures that there are no errors while bootstrapping an application
// that has no events, but enables Event Replay feature.
withStrictErrorHandler(),
],
hydrationFeatures: [withEventReplay()],
});
});

it('should not replay mouse events', async () => {
Expand Down
0