Skip to content

Commit 8f273ce

Browse files
iterianiAndrewKushnir
authored andcommitted
refactor(core): Allow the container and the listenable element to be configurable for early event contract. (angular#55586)
This will allow a multi-app application to listen to early events from different elements and place them on a separate field on the window. PR Close angular#55586
1 parent 1872fcd commit 8f273ce

File tree

7 files changed

+152
-35
lines changed

7 files changed

+152
-35
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
```ts
66

77
// @public
8-
export function bootstrapEventContract(field: string, container: Element, appId: string, events: string[], anyWindow?: any): EventContract;
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;
912

1013
// @public
1114
export class Dispatcher {
@@ -45,7 +48,7 @@ export class EventContract implements UnrenamedEventContract {
4548
// (undocumented)
4649
static MOUSE_SPECIAL_SUPPORT: boolean;
4750
registerDispatcher(dispatcher: Dispatcher_2, restriction: Restriction): void;
48-
replayEarlyEvents(): void;
51+
replayEarlyEvents(earlyJsactionContainer?: EarlyJsactionDataContainer): void;
4952
}
5053

5154
// @public
@@ -57,6 +60,13 @@ export class EventContractContainer implements EventContractContainerManager {
5760
readonly element: Element;
5861
}
5962

63+
// @public (undocumented)
64+
export type EventContractTracker<T> = {
65+
[key: string]: {
66+
[appId: string]: T;
67+
};
68+
};
69+
6070
// @public
6171
export class EventInfoWrapper {
6272
constructor(eventInfo: EventInfo);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@
99
export {Dispatcher, registerDispatcher} from './src/dispatcher';
1010
export {EventContractContainer} from './src/event_contract_container';
1111
export {EventContract} from './src/eventcontract';
12-
export {bootstrapEventContract} from './src/register_events';
12+
export {bootstrapEventContract, bootstrapEarlyEventContract} from './src/register_events';
13+
14+
export type {EventContractTracker} from './src/register_events';
1315
export {EventInfoWrapper} from './src/event_info';

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88

99
import {createEventInfoFromParameters, EventInfo} from './event_info';
1010

11-
declare global {
12-
interface Window {
13-
_ejsa?: EarlyJsactionData;
14-
}
11+
export declare interface EarlyJsactionDataContainer {
12+
_ejsa?: EarlyJsactionData;
1513
}
1614

1715
/**
@@ -21,11 +19,17 @@ export declare interface EarlyJsactionData {
2119
// List used to keep track of the early JSAction event types.
2220
et: string[];
2321

22+
// List used to keep track of capture event types.
23+
etc: string[];
24+
2425
// List used to keep track of the JSAction events if using earlyeventcontract.
2526
q: EventInfo[];
2627

2728
// Early Jsaction handler
2829
h: (event: Event) => void;
30+
31+
// Container for listening to events
32+
c: HTMLElement;
2933
}
3034

3135
/**
@@ -34,10 +38,15 @@ export declare interface EarlyJsactionData {
3438
* late-loaded EventContract.
3539
*/
3640
export class EarlyEventContract {
37-
constructor() {
38-
window._ejsa = {
41+
constructor(
42+
private readonly replaySink: EarlyJsactionDataContainer = window as EarlyJsactionDataContainer,
43+
private readonly container = window.document.documentElement,
44+
) {
45+
this.replaySink._ejsa = {
46+
c: container,
3947
q: [],
4048
et: [],
49+
etc: [],
4150
h: (event: Event) => {
4251
const eventInfo = createEventInfoFromParameters(
4352
event.type,
@@ -46,19 +55,21 @@ export class EarlyEventContract {
4655
window.document.documentElement,
4756
Date.now(),
4857
);
49-
window._ejsa!.q.push(eventInfo);
58+
this.replaySink._ejsa!.q.push(eventInfo);
5059
},
5160
};
5261
}
5362

5463
/**
55-
* Installs a list of event types for window.document.documentElement.
64+
* Installs a list of event types for container .
5665
*/
57-
addEvents(types: string[]) {
66+
addEvents(types: string[], capture?: boolean) {
67+
const replaySink = this.replaySink._ejsa!;
5868
for (let idx = 0; idx < types.length; idx++) {
5969
const eventType = types[idx];
60-
window._ejsa!.et.push(eventType);
61-
window.document.documentElement.addEventListener(eventType, window._ejsa!.h);
70+
const eventTypes = capture ? replaySink.etc : replaySink.et;
71+
eventTypes.push(eventType);
72+
this.container.addEventListener(eventType, replaySink.h, capture);
6273
}
6374
}
6475
}

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
import * as a11yClickLib from './a11y_click';
3434
import {ActionResolver} from './action_resolver';
35-
import {EarlyJsactionData} from './earlyeventcontract';
35+
import {EarlyJsactionData, EarlyJsactionDataContainer} from './earlyeventcontract';
3636
import * as eventLib from './event';
3737
import {EventContractContainerManager} from './event_contract_container';
3838
import {
@@ -255,10 +255,12 @@ export class EventContract implements UnrenamedEventContract {
255255
* in the provided event contract. Once all the events are replayed, it cleans
256256
* up the early contract.
257257
*/
258-
replayEarlyEvents() {
258+
replayEarlyEvents(
259+
earlyJsactionContainer: EarlyJsactionDataContainer = window as EarlyJsactionDataContainer,
260+
) {
259261
// Check if the early contract is present and prevent calling this function
260262
// more than once.
261-
const earlyJsactionData: EarlyJsactionData | undefined = window._ejsa;
263+
const earlyJsactionData: EarlyJsactionData | undefined = earlyJsactionContainer._ejsa;
262264
if (!earlyJsactionData) {
263265
return;
264266
}
@@ -278,13 +280,10 @@ export class EventContract implements UnrenamedEventContract {
278280
}
279281

280282
// Clean up the early contract.
281-
const earlyEventTypes: string[] = earlyJsactionData.et;
282283
const earlyEventHandler: (event: Event) => void = earlyJsactionData.h;
283-
for (let idx = 0; idx < earlyEventTypes.length; idx++) {
284-
const eventType: string = earlyEventTypes[idx];
285-
window.document.documentElement.removeEventListener(eventType, earlyEventHandler);
286-
}
287-
delete window._ejsa;
284+
removeEventListeners(earlyJsactionData.c, earlyJsactionData.et, earlyEventHandler);
285+
removeEventListeners(earlyJsactionData.c, earlyJsactionData.etc, earlyEventHandler, true);
286+
delete earlyJsactionContainer._ejsa;
288287
}
289288

290289
/**
@@ -390,6 +389,17 @@ export class EventContract implements UnrenamedEventContract {
390389
}
391390
}
392391

392+
function removeEventListeners(
393+
container: HTMLElement,
394+
eventTypes: string[],
395+
earlyEventHandler: (e: Event) => void,
396+
capture?: boolean,
397+
) {
398+
for (let idx = 0; idx < eventTypes.length; idx++) {
399+
container.removeEventListener(eventTypes[idx], earlyEventHandler, /* useCapture */ capture);
400+
}
401+
}
402+
393403
/**
394404
* Adds a11y click support to the given `EventContract`. Meant to be called
395405
* in a different compilation unit from the `EventContract`. The `EventContract`

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

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

9+
import {EarlyEventContract, EarlyJsactionDataContainer} from './earlyeventcontract';
910
import {EventContractContainer} from './event_contract_container';
1011
import {EventContract} from './eventcontract';
1112

13+
export type EventContractTracker<T> = {[key: string]: {[appId: string]: T}};
14+
1215
/**
1316
* Provides a factory function for bootstrapping an event contract on a
14-
* window object.
15-
* @param field The property on the window that the event contract will be placed on.
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.
1619
* @param container The container that listens to events
1720
* @param appId A given identifier for an application. If there are multiple apps on the page
1821
* then this is how contracts can be initialized for each one.
1922
* @param events An array of event names that should be listened to.
20-
* @param anyWindow The global window object that should receive the event contract.
21-
* @returns The `event` contract. This is both assigned to `anyWindow` and returned for testing.
23+
* @param earlyJsactionTracker The object that should receive the event contract.
2224
*/
2325
export function bootstrapEventContract(
2426
field: string,
2527
container: Element,
2628
appId: string,
2729
events: string[],
28-
anyWindow: any = window,
30+
earlyJsactionTracker: EventContractTracker<EventContract> = window as unknown as EventContractTracker<EventContract>,
2931
) {
30-
if (!anyWindow[field]) {
31-
anyWindow[field] = {};
32+
if (!earlyJsactionTracker[field]) {
33+
earlyJsactionTracker[field] = {};
3234
}
3335
const eventContract = new EventContract(new EventContractContainer(container));
34-
anyWindow[field][appId] = eventContract;
36+
earlyJsactionTracker[field][appId] = eventContract;
3537
for (const ev of events) {
3638
eventContract.addEvent(ev);
3739
}
38-
return eventContract;
40+
}
41+
42+
/**
43+
* Provides a factory function for bootstrapping an event contract on a
44+
* specified object (by default, exposed on the `window`).
45+
* @param field The property on the object that the event contract will be placed on.
46+
* @param container The container that listens to events
47+
* @param appId A given identifier for an application. If there are multiple apps on the page
48+
* then this is how contracts can be initialized for each one.
49+
* @param eventTypes An array of event names that should be listened to.
50+
* @param captureEventTypes An array of event names that should be listened to with capture.
51+
* @param earlyJsactionTracker The object that should receive the event contract.
52+
*/
53+
export function bootstrapEarlyEventContract(
54+
field: string,
55+
container: HTMLElement,
56+
appId: string,
57+
eventTypes: string[],
58+
captureEventTypes: string[],
59+
earlyJsactionTracker: EventContractTracker<EarlyJsactionDataContainer> = window as unknown as EventContractTracker<EarlyJsactionDataContainer>,
60+
) {
61+
if (!earlyJsactionTracker[field]) {
62+
earlyJsactionTracker[field] = {};
63+
}
64+
earlyJsactionTracker[field][appId] = {};
65+
const eventContract = new EarlyEventContract(earlyJsactionTracker[field][appId], container);
66+
eventContract.addEvents(eventTypes);
67+
eventContract.addEvents(captureEventTypes, true);
3968
}

packages/core/primitives/event-dispatch/test/eventcontract_test.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import * as cache from '../src/cache';
1010
import {bootstrapCustomEventSupport, fireCustomEvent} from '../src/custom_events';
1111
import {stopPropagation} from '../src/dispatcher';
12-
import {EarlyEventContract, EarlyJsactionData} from '../src/earlyeventcontract';
12+
import {
13+
EarlyEventContract,
14+
EarlyJsactionData,
15+
EarlyJsactionDataContainer,
16+
} from '../src/earlyeventcontract';
1317
import {
1418
EventContractContainer,
1519
EventContractContainerManager,
@@ -23,6 +27,10 @@ import {Restriction} from '../src/restriction';
2327

2428
import {safeElement, testonlyHtml} from './html';
2529

30+
declare global {
31+
interface Window extends EarlyJsactionDataContainer {}
32+
}
33+
2634
const domContent = `
2735
<div id="container"></div>
2836
@@ -182,6 +190,11 @@ const domContent = `
182190
</div>
183191
</div>
184192
</div>
193+
<div id="focus-container">
194+
<div id="focus-action-element" jsaction="focus:handleFocus">
195+
<button id="focus-target-element">Focus Button</button>
196+
</div>
197+
</div>
185198
`;
186199

187200
function getRequiredElementById(id: string) {
@@ -1920,6 +1933,43 @@ describe('EventContract', () => {
19201933
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
19211934
});
19221935

1936+
it('early capture events are dispatched', () => {
1937+
const container = getRequiredElementById('focus-container');
1938+
const actionElement = getRequiredElementById('focus-action-element');
1939+
const targetElement = getRequiredElementById('focus-target-element');
1940+
const replaySink = {_ejsa: undefined};
1941+
const removeEventListenerSpy = spyOn(container, 'removeEventListener').and.callThrough();
1942+
1943+
const earlyEventContract = new EarlyEventContract(replaySink, container);
1944+
earlyEventContract.addEvents(['focus'], true);
1945+
1946+
targetElement.focus();
1947+
1948+
const earlyJsactionData: EarlyJsactionData | undefined = replaySink._ejsa;
1949+
expect(earlyJsactionData).toBeDefined();
1950+
expect(earlyJsactionData!.q.length).toBe(1);
1951+
expect(earlyJsactionData!.q[0].event.type).toBe('focus');
1952+
1953+
const dispatcher = jasmine.createSpy<Dispatcher>('dispatcher');
1954+
const eventContract = createEventContract({
1955+
eventContractContainerManager: new EventContractContainer(container),
1956+
eventTypes: ['focus'],
1957+
dispatcher,
1958+
});
1959+
1960+
eventContract.replayEarlyEvents(replaySink);
1961+
1962+
expect(replaySink._ejsa).toBeUndefined();
1963+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1);
1964+
expect(dispatcher).toHaveBeenCalledTimes(2);
1965+
const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher);
1966+
expect(eventInfoWrapper.getEventType()).toBe('focus');
1967+
expect(eventInfoWrapper.getEvent().type).toBe('focus');
1968+
expect(eventInfoWrapper.getTargetElement()).toBe(targetElement);
1969+
expect(eventInfoWrapper.getAction()?.name).toBe('handleFocus');
1970+
expect(eventInfoWrapper.getAction()?.element).toBe(actionElement);
1971+
});
1972+
19231973
it('early events are dispatched when target is cleared', () => {
19241974
const container = getRequiredElementById('click-container');
19251975
const actionElement = getRequiredElementById('click-action-element');
@@ -1978,7 +2028,9 @@ describe('EventContract', () => {
19782028
relatedTarget: container,
19792029
});
19802030

1981-
const earlyJsactionData: EarlyJsactionData | undefined = window._ejsa;
2031+
const earlyJsactionData: EarlyJsactionData | undefined = (
2032+
window as EarlyJsactionDataContainer
2033+
)._ejsa;
19822034
expect(earlyJsactionData).toBeDefined();
19832035
expect(earlyJsactionData!.q.length).toBe(1);
19842036
expect(earlyJsactionData!.q[0].event).toBe(mouseOutEvent);
@@ -1992,7 +2044,7 @@ describe('EventContract', () => {
19922044

19932045
eventContract.replayEarlyEvents();
19942046

1995-
expect(window._ejsa).toBeUndefined();
2047+
expect((window as EarlyJsactionDataContainer)._ejsa).toBeUndefined();
19962048
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1);
19972049
expect(dispatcher).toHaveBeenCalledTimes(3);
19982050
const eventInfoWrapper = getLastDispatchedEventInfoWrapper(dispatcher);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,9 @@
13431343
{
13441344
"name": "init_dom_triggers"
13451345
},
1346+
{
1347+
"name": "init_earlyeventcontract"
1348+
},
13461349
{
13471350
"name": "init_effect"
13481351
},

0 commit comments

Comments
 (0)