Skip to content

Commit 72b107b

Browse files
iterianiatscott
authored andcommitted
refactor(core): Use early event contract instead of the event contract in bootstrap. (angular#55587)
This also fixes an existing bug where we erase the jsaction attribute too early. Now the event contract binary is 608 bytes :D. PR Close angular#55587
1 parent d00f9e8 commit 72b107b

File tree

10 files changed

+90
-105
lines changed

10 files changed

+90
-105
lines changed

goldens/public-api/core/primitives/event-dispatch/index.md

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,18 @@
55
```ts
66

77
// @public
8-
export function bootstrapEarlyEventContract(field: string, container: HTMLElement, appId: string, eventTypes: string[], captureEventTypes: string[], earlyJsactionTracker?: EventContractTracker<EarlyJsactionDataContainer>): void;
9-
10-
// @public
11-
export function bootstrapEventContract(field: string, container: Element, appId: string, events: string[], earlyJsactionTracker?: EventContractTracker<EventContract>): void;
12-
13-
// @public
14-
export class Dispatcher {
15-
constructor(getHandler?: ((eventInfoWrapper: EventInfoWrapper) => EventInfoWrapperHandler | void) | undefined, { stopPropagation, eventReplayer, }?: {
16-
stopPropagation?: boolean;
8+
export class BaseDispatcher {
9+
constructor(dispatchDelegate: (eventInfoWrapper: EventInfoWrapper) => void, { eventReplayer }?: {
1710
eventReplayer?: Replayer;
1811
});
19-
canDispatch(eventInfoWrapper: EventInfoWrapper): boolean;
20-
dispatch(eventInfo: EventInfo, isGlobalDispatch?: boolean): void;
21-
hasAction(name: string): boolean;
22-
registerEventInfoHandlers<T>(namespace: string, instance: T | null, methods: {
23-
[key: string]: EventInfoWrapperHandler;
24-
}): void;
25-
registerGlobalHandler(eventType: string, handler: GlobalHandler): void;
26-
setEventReplayer(eventReplayer: Replayer): void;
27-
unregisterGlobalHandler(eventType: string, handler: GlobalHandler): void;
28-
unregisterHandler(namespace: string, name: string): void;
12+
dispatch(eventInfo: EventInfo): void;
13+
queueEventInfoWrapper(eventInfoWrapper: EventInfoWrapper): void;
14+
scheduleEventReplay(): void;
2915
}
3016

17+
// @public
18+
export function bootstrapEarlyEventContract(field: string, container: HTMLElement, appId: string, eventTypes?: string[], captureEventTypes?: string[], earlyJsactionTracker?: EventContractTracker<EarlyJsactionDataContainer>): void;
19+
3120
// @public (undocumented)
3221
export interface EarlyJsactionDataContainer {
3322
// (undocumented)
@@ -46,12 +35,12 @@ export class EventContract implements UnrenamedEventContract {
4635
static CUSTOM_EVENT_SUPPORT: boolean;
4736
// (undocumented)
4837
ecaacs?: (updateEventInfoForA11yClick: typeof a11yClickLib.updateEventInfoForA11yClick, preventDefaultForA11yClick: typeof a11yClickLib.preventDefaultForA11yClick, populateClickOnlyAction: typeof a11yClickLib.populateClickOnlyAction) => void;
49-
ecrd(dispatcher: Dispatcher_2, restriction: Restriction): void;
38+
ecrd(dispatcher: Dispatcher, restriction: Restriction): void;
5039
exportAddA11yClickSupport(): void;
5140
handler(eventType: string): EventHandler | undefined;
5241
// (undocumented)
5342
static MOUSE_SPECIAL_SUPPORT: boolean;
54-
registerDispatcher(dispatcher: Dispatcher_2, restriction: Restriction): void;
43+
registerDispatcher(dispatcher: Dispatcher, restriction: Restriction): void;
5544
replayEarlyEvents(earlyJsactionContainer?: EarlyJsactionDataContainer): void;
5645
}
5746

@@ -112,7 +101,7 @@ export class EventInfoWrapper {
112101
}
113102

114103
// @public
115-
export function registerDispatcher(eventContract: UnrenamedEventContract, dispatcher: Dispatcher): void;
104+
export function registerDispatcher(eventContract: UnrenamedEventContract, dispatcher: BaseDispatcher): void;
116105

117106
// (No @packageDocumentation comment for this package)
118107

goldens/size-tracking/integration-payloads.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
},
5555
"platform-server-hydration/browser": {
5656
"uncompressed": {
57-
"main": 200584,
57+
"main": 207890,
5858
"polyfills": 33807,
59-
"event-dispatch-contract.min": 11293
59+
"event-dispatch-contract.min": 704
6060
}
6161
}
6262
}

packages/core/primitives/event-dispatch/contract_binary.ts

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

9-
import {bootstrapEventContract} from './src/register_events';
9+
import {bootstrapEarlyEventContract} from './src/register_events';
1010

11-
(window as any)['__jsaction_bootstrap'] = bootstrapEventContract;
11+
(window as any)['__jsaction_bootstrap'] = bootstrapEarlyEventContract;

packages/core/primitives/event-dispatch/index.ts

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

9-
export {Dispatcher, registerDispatcher} from './src/dispatcher';
9+
export {BaseDispatcher, registerDispatcher} from './src/base_dispatcher';
1010
export {EventContractContainer} from './src/event_contract_container';
1111
export type {EarlyJsactionDataContainer} from './src/earlyeventcontract';
1212
export {EventContract} from './src/eventcontract';
13-
export {bootstrapEventContract, bootstrapEarlyEventContract} from './src/register_events';
13+
export {bootstrapEarlyEventContract} from './src/register_events';
1414

1515
export type {EventContractTracker} from './src/register_events';
1616
export {EventInfoWrapper} from './src/event_info';

packages/core/primitives/event-dispatch/src/register_events.ts

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,9 @@
77
*/
88

99
import {EarlyEventContract, EarlyJsactionDataContainer} from './earlyeventcontract';
10-
import {EventContractContainer} from './event_contract_container';
11-
import {EventContract} from './eventcontract';
1210

1311
export type EventContractTracker<T> = {[key: string]: {[appId: string]: T}};
1412

15-
/**
16-
* Provides a factory function for bootstrapping an event contract on a
17-
* specified object (by default, exposed on the `window`).
18-
* @param field The property on the object that the event contract will be placed on.
19-
* @param container The container that listens to events
20-
* @param appId A given identifier for an application. If there are multiple apps on the page
21-
* then this is how contracts can be initialized for each one.
22-
* @param events An array of event names that should be listened to.
23-
* @param earlyJsactionTracker The object that should receive the event contract.
24-
*/
25-
export function bootstrapEventContract(
26-
field: string,
27-
container: Element,
28-
appId: string,
29-
events: string[],
30-
earlyJsactionTracker: EventContractTracker<EventContract> = window as unknown as EventContractTracker<EventContract>,
31-
) {
32-
if (!earlyJsactionTracker[field]) {
33-
earlyJsactionTracker[field] = {};
34-
}
35-
const eventContract = new EventContract(new EventContractContainer(container));
36-
earlyJsactionTracker[field][appId] = eventContract;
37-
for (const ev of events) {
38-
eventContract.addEvent(ev);
39-
}
40-
}
41-
4213
/**
4314
* Provides a factory function for bootstrapping an event contract on a
4415
* specified object (by default, exposed on the `window`).
@@ -63,6 +34,6 @@ export function bootstrapEarlyEventContract(
6334
}
6435
earlyJsactionTracker[field][appId] = {};
6536
const eventContract = new EarlyEventContract(earlyJsactionTracker[field][appId], container);
66-
eventTypes && eventContract.addEvents(eventTypes);
67-
captureEventTypes && eventContract.addEvents(captureEventTypes, true);
37+
if (eventTypes) eventContract.addEvents(eventTypes);
38+
if (captureEventTypes) eventContract.addEvents(captureEventTypes, true);
6839
}

packages/core/src/hydration/event_replay.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
*/
88

99
import {
10-
Dispatcher,
10+
BaseDispatcher,
11+
EarlyJsactionDataContainer,
1112
EventContract,
13+
EventContractContainer,
1214
EventInfoWrapper,
1315
registerDispatcher,
1416
} from '@angular/core/primitives/event-dispatch';
@@ -32,7 +34,12 @@ export const EVENT_REPLAY_ENABLED_DEFAULT = false;
3234
export const CONTRACT_PROPERTY = 'ngContracts';
3335

3436
declare global {
35-
var ngContracts: {[key: string]: EventContract};
37+
var ngContracts: {[key: string]: EarlyJsactionDataContainer};
38+
}
39+
40+
// TODO: Upstream this back into event-dispatch.
41+
function getJsactionData(container: EarlyJsactionDataContainer) {
42+
return container._ejsa;
3643
}
3744

3845
const JSACTION_ATTRIBUTE = 'jsaction';
@@ -76,11 +83,27 @@ export function withEventReplay(): Provider[] {
7683
// This is set in packages/platform-server/src/utils.ts
7784
// Note: globalThis[CONTRACT_PROPERTY] may be undefined in case Event Replay feature
7885
// is enabled, but there are no events configured in an application.
79-
const eventContract = globalThis[CONTRACT_PROPERTY]?.[appId] as EventContract;
80-
if (eventContract) {
81-
const dispatcher = new Dispatcher();
82-
setEventReplayer(dispatcher);
83-
// Event replay is kicked off as a side-effect of executing this function.
86+
const container = globalThis[CONTRACT_PROPERTY]?.[appId];
87+
const earlyJsactionData = getJsactionData(container);
88+
if (earlyJsactionData) {
89+
const eventContract = new EventContract(
90+
new EventContractContainer(earlyJsactionData.c),
91+
);
92+
for (const et of earlyJsactionData.et) {
93+
eventContract.addEvent(et);
94+
}
95+
for (const et of earlyJsactionData.etc) {
96+
eventContract.addEvent(et);
97+
}
98+
eventContract.replayEarlyEvents(container);
99+
const dispatcher = new BaseDispatcher(() => {}, {
100+
eventReplayer: (queue) => {
101+
for (const event of queue) {
102+
handleEvent(event);
103+
}
104+
queue.length = 0;
105+
},
106+
});
84107
registerDispatcher(eventContract, dispatcher);
85108
for (const el of removeJsactionQueue) {
86109
el.removeAttribute(JSACTION_ATTRIBUTE);
@@ -154,17 +177,6 @@ export function setJSActionAttribute(
154177
}
155178
}
156179

157-
/**
158-
* Registers a function that should be invoked to replay events.
159-
*/
160-
function setEventReplayer(dispatcher: Dispatcher) {
161-
dispatcher.setEventReplayer((queue) => {
162-
for (const event of queue) {
163-
handleEvent(event);
164-
}
165-
});
166-
}
167-
168180
/**
169181
* Finds an LView that a given DOM element belongs to.
170182
*/

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,9 +1328,6 @@
13281328
{
13291329
"name": "init_discovery_utils"
13301330
},
1331-
{
1332-
"name": "init_dispatcher"
1333-
},
13341331
{
13351332
"name": "init_document"
13361333
},

packages/platform-server/src/utils.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,33 @@ function insertEventRecordScript(
142142
const eventDispatchScript = findEventDispatchScript(doc);
143143
if (eventDispatchScript) {
144144
const events = Array.from(eventTypesToBeReplayed);
145+
const captureEventTypes = [];
146+
const eventTypes = [];
147+
for (const eventType of events) {
148+
if (
149+
eventType === 'mouseenter' ||
150+
eventType === 'mouseleave' ||
151+
eventType === 'pointerenter' ||
152+
eventType === 'pointerleave'
153+
) {
154+
continue;
155+
}
156+
if (
157+
eventType === 'focus' ||
158+
eventType === 'blur' ||
159+
eventType === 'error' ||
160+
eventType === 'load' ||
161+
eventType === 'toggle'
162+
) {
163+
captureEventTypes.push(eventType);
164+
} else {
165+
eventTypes.push(eventType);
166+
}
167+
}
145168
// This is defined in packages/core/primitives/event-dispatch/contract_binary.ts
146169
const replayScriptContents = `window.__jsaction_bootstrap('ngContracts', document.body, ${JSON.stringify(
147170
appId,
148-
)}, ${JSON.stringify(events)});`;
171+
)}, ${JSON.stringify(eventTypes)}${captureEventTypes.length ? ',' + JSON.stringify(captureEventTypes) : ''});`;
149172

150173
const replayScript = createScript(doc, replayScriptContents, nonce);
151174

packages/platform-server/test/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ circular_dependency_test(
1616
ts_library(
1717
name = "test_lib",
1818
testonly = True,
19-
srcs = glob(["**/*.ts"]),
19+
srcs = glob(["*.ts"]),
2020
deps = [
2121
"//packages:types",
2222
"//packages/animations",

packages/platform-server/test/event_replay_spec.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import {DOCUMENT} from '@angular/common';
1010
import {Component, destroyPlatform, getPlatform, Type} from '@angular/core';
11-
import {EventContract} from '@angular/core/primitives/event-dispatch';
1211
import {TestBed} from '@angular/core/testing';
1312
import {
1413
withEventReplay,
@@ -83,7 +82,6 @@ describe('event replay', () => {
8382

8483
describe('server rendering', () => {
8584
let doc: Document;
86-
let eventContract: EventContract | undefined = undefined;
8785
const originalDocument = globalThis.document;
8886
const originalWindow = globalThis.window;
8987

@@ -96,8 +94,6 @@ describe('event replay', () => {
9694
eval(script.textContent);
9795
}
9896
}
99-
eventContract = globalThis.window['ngContracts']['ng'];
100-
expect(eventContract).toBeDefined();
10197
}
10298

10399
beforeAll(async () => {
@@ -111,51 +107,55 @@ describe('event replay', () => {
111107

112108
afterEach(() => {
113109
doc.body.textContent = '';
114-
eventContract?.cleanUp();
115-
eventContract = undefined;
116110
});
117111
afterAll(() => {
118112
globalThis.window = originalWindow;
119113
globalThis.document = originalDocument;
120114
});
121-
it('should serialize event types to be listened to and jsaction', async () => {
115+
it('should serialize event types to be listened to and jsaction attribute', async () => {
122116
const clickSpy = jasmine.createSpy('onClick');
123-
const blurSpy = jasmine.createSpy('onBlur');
117+
const focusSpy = jasmine.createSpy('onFocus');
124118
@Component({
125119
standalone: true,
126120
selector: 'app',
127121
template: `
128-
<div (click)="onClick()" id="1">
129-
<div (blur)="onClick()" id="2"></div>
122+
<div (click)="onClick()" id="click-element">
123+
<div id="focus-container">
124+
<div id="focus-action-element" (focus)="onFocus()">
125+
<button id="focus-target-element">Focus Button</button>
126+
</div>
127+
</div>
130128
</div>
131129
`,
132130
})
133131
class SimpleComponent {
134132
onClick = clickSpy;
135-
onBlur = blurSpy;
133+
onFocus = focusSpy;
136134
}
137135

138136
const docContents = `<html><head></head><body>${EVENT_DISPATCH_SCRIPT}<app></app></body></html>`;
139137
const html = await ssr(SimpleComponent, {doc: docContents});
140138
const ssrContents = getAppContents(html);
141139
expect(ssrContents).toContain(
142-
`<script>window.__jsaction_bootstrap('ngContracts', document.body, "ng", ["click","blur"]);</script>`,
143-
);
144-
expect(ssrContents).toContain(
145-
'<div id="1" jsaction="click:"><div id="2" jsaction="blur:"></div></div>',
140+
`<script>window.__jsaction_bootstrap('ngContracts', document.body, "ng", ["click"],["focus"]);</script>`,
146141
);
147142

148143
render(doc, ssrContents);
149-
const el = doc.getElementById('1')!;
144+
const el = doc.getElementById('click-element')!;
145+
const button = doc.getElementById('focus-target-element')!;
150146
const clickEvent = new CustomEvent('click', {bubbles: true});
151147
el.dispatchEvent(clickEvent);
148+
const focusEvent = new CustomEvent('focus');
149+
button.dispatchEvent(focusEvent);
152150
expect(clickSpy).not.toHaveBeenCalled();
151+
expect(focusSpy).not.toHaveBeenCalled();
153152
resetTViewsFor(SimpleComponent);
154153
const appRef = await hydrate(doc, SimpleComponent, {
155154
hydrationFeatures: [withEventReplay()],
156155
});
157156
appRef.tick();
158157
expect(clickSpy).toHaveBeenCalled();
158+
expect(focusSpy).toHaveBeenCalled();
159159
});
160160

161161
it('should remove jsaction attributes, but continue listening to events.', async () => {
@@ -175,24 +175,17 @@ describe('event replay', () => {
175175
const docContents = `<html><head></head><body>${EVENT_DISPATCH_SCRIPT}<app></app></body></html>`;
176176
const html = await ssr(SimpleComponent, {doc: docContents});
177177
const ssrContents = getAppContents(html);
178-
const removeEventListenerSpy = spyOn(
179-
document.body,
180-
'removeEventListener',
181-
).and.callThrough();
182178
render(doc, ssrContents);
183179
const el = doc.getElementById('1')!;
184180
expect(el.hasAttribute('jsaction')).toBeTrue();
185181
expect((el.firstChild as Element).hasAttribute('jsaction')).toBeTrue();
186182
resetTViewsFor(SimpleComponent);
187-
expect(removeEventListenerSpy).not.toHaveBeenCalled();
188183
const appRef = await hydrate(doc, SimpleComponent, {
189184
hydrationFeatures: [withEventReplay()],
190185
});
191186
appRef.tick();
192187
expect(el.hasAttribute('jsaction')).toBeFalse();
193188
expect((el.firstChild as Element).hasAttribute('jsaction')).toBeFalse();
194-
// Event contract is still listening even if jsaction attributes are removed.
195-
expect(removeEventListenerSpy).not.toHaveBeenCalled();
196189
});
197190

198191
it(`should add 'nonce' attribute to event record script when 'ngCspNonce' is provided`, async () => {

0 commit comments

Comments
 (0)