Skip to content

Commit

Permalink
feat(core): Replay events from the event contract using the dispatcher.
Browse files Browse the repository at this point in the history
This should accomplish event replay during full page hydration.
  • Loading branch information
iteriani committed Apr 22, 2024
1 parent ecbc728 commit 2826028
Show file tree
Hide file tree
Showing 9 changed files with 452 additions and 238 deletions.
1 change: 1 addition & 0 deletions packages/core/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ng_module(
),
deps = [
"//packages:types",
"//packages/core/primitives/event-dispatch",
"//packages/core/primitives/signals",
"//packages/core/src/compiler",
"//packages/core/src/di/interface",
Expand Down
10 changes: 5 additions & 5 deletions packages/core/primitives/event-dispatch/src/key_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
*/

/** Special keycodes used by jsaction for the generic click action. */
export enum KeyCode {
export const KeyCode = {
/**
* If on a Macintosh with an extended keyboard, the Enter key located in the
* numeric pad has a different ASCII code.
*/
MAC_ENTER = 3,
MAC_ENTER: 3,

/** The Enter key. */
ENTER = 13,
ENTER: 13,

/** The Space key. */
SPACE = 32,
}
SPACE: 32,
};
97 changes: 96 additions & 1 deletion packages/core/src/hydration/event_replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Dispatcher, EventContract, EventInfoWrapper, registerDispatcher} from '@angular/core/primitives/event-dispatch';

import {APP_BOOTSTRAP_LISTENER, ApplicationRef, whenStable} from '../application/application_ref';
import {APP_ID} from '../application/application_tokens';
import {Injector} from '../di';
import {inject} from '../di/injector_compatibility';
import {Provider} from '../di/interface/provider';
import {getLContext} from '../render3/context_discovery';
import {TNode, TNodeType} from '../render3/interfaces/node';
import {RNode} from '../render3/interfaces/renderer_dom';
import {CLEANUP, LView, TView} from '../render3/interfaces/view';
import {CLEANUP, LView, TVIEW, TView} from '../render3/interfaces/view';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {unwrapRNode} from '../render3/util/view_utils';

import {IS_EVENT_REPLAY_ENABLED} from './tokens';

export const EVENT_REPLAY_ENABLED_DEFAULT = false;
export const CONTRACT_PROPERTY = 'ngContracts';

declare global {
var ngContracts: {[key: string]: EventContract};
}

/**
* Returns a set of providers required to setup support for event replay.
Expand All @@ -26,6 +39,32 @@ export function withEventReplay(): Provider[] {
provide: IS_EVENT_REPLAY_ENABLED,
useValue: true,
},
{
provide: APP_BOOTSTRAP_LISTENER,
useFactory: () => {
if (isPlatformBrowser()) {
const injector = inject(Injector);
const appRef = inject(ApplicationRef);
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(() => {
const appId = injector.get(APP_ID);
// This is set in packages/platform-server/src/utils.ts
const eventContract = globalThis[CONTRACT_PROPERTY][appId] as EventContract;
if (eventContract) {
const dispatcher = new Dispatcher();
setEventReplayer(dispatcher);
registerDispatcher(eventContract, dispatcher);
}
});
};
}
return () => {}; // noop for the server code
},
multi: true,
}
];
}

Expand Down Expand Up @@ -79,3 +118,59 @@ export function setJSActionAttribute(
}
}
}

function setEventReplayer(dispatcher: Dispatcher) {
dispatcher.setEventReplayer(queue => {
for (const event of queue) {
handleEvent(event);
}
});
}

function getLViewByElement(element: Element): LView|null {
// TODO: we should *not* use discovery utils here, consider extracting
// the lookup logic into a separate function and use it here and in the
// discovery code.
const lContext = getLContext(element);
return lContext?.lView ?? null;
}

function handleEvent(event: EventInfoWrapper) {
const eventName = event.getEventType();
const nativeElement = event.getAction()!.element;
// Dispatch event via Angular's logic
if (nativeElement) {
const lView = getLViewByElement(nativeElement);
if (lView !== null) {
const tView = lView[TVIEW];
const listeners = getEventListeners(tView, lView, nativeElement, eventName);
for (const listener of listeners) {
listener(event.getEvent());
}
}
}
}

type Listener = ((value: Event) => unknown)|(() => unknown);

function getEventListeners(
tView: TView, lView: LView, nativeElement: Element, eventName: string): Listener[] {
const listeners: Listener[] = [];
const lCleanup = lView[CLEANUP];
const tCleanup = tView.cleanup;
if (tCleanup && lCleanup) {
for (let i = 0; i < tCleanup.length;) {
const storedEventName = tCleanup[i++];
const nativeElementIndex = tCleanup[i++];
if (typeof storedEventName === 'string') {
const listenerElement = unwrapRNode(lView[nativeElementIndex]) as any as Element;
const listener: Listener = lCleanup[tCleanup[i++]];
i++; // increment to the next position;
if (listenerElement === nativeElement && eventName === storedEventName) {
listeners.push(listener);
}
}
}
}
return listeners;
}
60 changes: 60 additions & 0 deletions packages/core/test/bundling/defer/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,12 @@
{
"name": "init_UnsubscriptionError"
},
{
"name": "init_a11y_click"
},
{
"name": "init_accessibility"
},
{
"name": "init_advance"
},
Expand Down Expand Up @@ -1091,6 +1097,9 @@
{
"name": "init_attribute"
},
{
"name": "init_attribute2"
},
{
"name": "init_attribute_interpolation"
},
Expand All @@ -1106,6 +1115,9 @@
{
"name": "init_bypass"
},
{
"name": "init_cache"
},
{
"name": "init_cached_injector_service"
},
Expand All @@ -1127,6 +1139,9 @@
{
"name": "init_change_detector_ref"
},
{
"name": "init_char"
},
{
"name": "init_class_differ"
},
Expand Down Expand Up @@ -1238,6 +1253,9 @@
{
"name": "init_create_injector"
},
{
"name": "init_custom_events"
},
{
"name": "init_debug_node"
},
Expand Down Expand Up @@ -1301,12 +1319,18 @@
{
"name": "init_discovery_utils"
},
{
"name": "init_dispatcher"
},
{
"name": "init_document"
},
{
"name": "init_dom"
},
{
"name": "init_dom2"
},
{
"name": "init_dom_triggers"
},
Expand Down Expand Up @@ -1367,12 +1391,33 @@
{
"name": "init_esm"
},
{
"name": "init_event"
},
{
"name": "init_event_contract_container"
},
{
"name": "init_event_contract_defines"
},
{
"name": "init_event_dispatch"
},
{
"name": "init_event_emitter"
},
{
"name": "init_event_info"
},
{
"name": "init_event_replay"
},
{
"name": "init_event_type"
},
{
"name": "init_eventcontract"
},
{
"name": "init_fields"
},
Expand Down Expand Up @@ -1553,6 +1598,9 @@
{
"name": "init_jit_options"
},
{
"name": "init_key_code"
},
{
"name": "init_keyvalue_differs"
},
Expand Down Expand Up @@ -1745,6 +1793,9 @@
{
"name": "init_property2"
},
{
"name": "init_property3"
},
{
"name": "init_property_interpolation"
},
Expand Down Expand Up @@ -1799,6 +1850,9 @@
{
"name": "init_reflection_capabilities"
},
{
"name": "init_register_events"
},
{
"name": "init_render"
},
Expand All @@ -1808,12 +1862,18 @@
{
"name": "init_render3"
},
{
"name": "init_replay"
},
{
"name": "init_reportUnhandledError"
},
{
"name": "init_resource_loading"
},
{
"name": "init_restriction"
},
{
"name": "init_sanitization"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@
"name": "EventManagerPlugin"
},
{
"name": "EventType"
"name": "EventType2"
},
{
"name": "GenericBrowserDomAdapter"
Expand Down
1 change: 1 addition & 0 deletions packages/platform-server/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ ts_library(
"//packages/common/testing",
"//packages/compiler",
"//packages/core",
"//packages/core/primitives/event-dispatch",
"//packages/core/testing",
"//packages/localize",
"//packages/localize/init",
Expand Down
Loading

0 comments on commit 2826028

Please sign in to comment.