6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
9
+ import { Dispatcher , EventContract , EventInfoWrapper , registerDispatcher } from '@angular/core/primitives/event-dispatch' ;
10
+
11
+ import { APP_BOOTSTRAP_LISTENER , ApplicationRef , whenStable } from '../application/application_ref' ;
12
+ import { APP_ID } from '../application/application_tokens' ;
13
+ import { Injector } from '../di' ;
14
+ import { inject } from '../di/injector_compatibility' ;
9
15
import { Provider } from '../di/interface/provider' ;
16
+ import { attachLViewId , readLView } from '../render3/context_discovery' ;
10
17
import { TNode , TNodeType } from '../render3/interfaces/node' ;
11
18
import { RNode } from '../render3/interfaces/renderer_dom' ;
12
- import { CLEANUP , LView , TView } from '../render3/interfaces/view' ;
19
+ import { CLEANUP , LView , TVIEW , TView } from '../render3/interfaces/view' ;
20
+ import { isPlatformBrowser } from '../render3/util/misc_utils' ;
13
21
import { unwrapRNode } from '../render3/util/view_utils' ;
14
22
15
23
import { IS_EVENT_REPLAY_ENABLED } from './tokens' ;
16
24
17
25
export const EVENT_REPLAY_ENABLED_DEFAULT = false ;
26
+ export const CONTRACT_PROPERTY = 'ngContracts' ;
27
+
28
+ declare global {
29
+ var ngContracts : { [ key : string ] : EventContract } ;
30
+ }
18
31
19
32
/**
20
33
* Returns a set of providers required to setup support for event replay.
@@ -26,6 +39,33 @@ export function withEventReplay(): Provider[] {
26
39
provide : IS_EVENT_REPLAY_ENABLED ,
27
40
useValue : true ,
28
41
} ,
42
+ {
43
+ provide : APP_BOOTSTRAP_LISTENER ,
44
+ useFactory : ( ) => {
45
+ if ( isPlatformBrowser ( ) ) {
46
+ const injector = inject ( Injector ) ;
47
+ const appRef = inject ( ApplicationRef ) ;
48
+ return ( ) => {
49
+ // Kick off event replay logic once hydration for the initial part
50
+ // of the application is completed. This timing is similar to the unclaimed
51
+ // dehydrated views cleanup timing.
52
+ whenStable ( appRef ) . then ( ( ) => {
53
+ const appId = injector . get ( APP_ID ) ;
54
+ // This is set in packages/platform-server/src/utils.ts
55
+ const eventContract = globalThis [ CONTRACT_PROPERTY ] [ appId ] as EventContract ;
56
+ if ( eventContract ) {
57
+ const dispatcher = new Dispatcher ( ) ;
58
+ setEventReplayer ( dispatcher ) ;
59
+ // Event replay is kicked off as a side-effect of executing this function.
60
+ registerDispatcher ( eventContract , dispatcher ) ;
61
+ }
62
+ } ) ;
63
+ } ;
64
+ }
65
+ return ( ) => { } ; // noop for the server code
66
+ } ,
67
+ multi : true ,
68
+ }
29
69
] ;
30
70
}
31
71
@@ -79,3 +119,81 @@ export function setJSActionAttribute(
79
119
}
80
120
}
81
121
}
122
+
123
+ /**
124
+ * Registers a function that should be invoked to replay events.
125
+ */
126
+ function setEventReplayer ( dispatcher : Dispatcher ) {
127
+ dispatcher . setEventReplayer ( queue => {
128
+ for ( const event of queue ) {
129
+ handleEvent ( event ) ;
130
+ }
131
+ } ) ;
132
+ }
133
+
134
+ /**
135
+ * Finds an LView that a given DOM element belongs to.
136
+ */
137
+ function getLViewByElement ( target : HTMLElement ) : LView | null {
138
+ let lView = readLView ( target ) ;
139
+ if ( lView ) {
140
+ return lView ;
141
+ } else {
142
+ // If this node doesn't have LView info attached, then we need to
143
+ // traverse upwards up the DOM to find the nearest element that
144
+ // has already been monkey patched with data.
145
+ let parent = target as HTMLElement ;
146
+ while ( parent = parent . parentNode as HTMLElement ) {
147
+ lView = readLView ( parent ) ;
148
+ if ( lView ) {
149
+ // To prevent additional lookups, monkey-patch LView id onto this DOM node.
150
+ // TODO: consider patching all parent nodes that didn't have LView id, so that
151
+ // we can avoid lookups for more nodes.
152
+ attachLViewId ( target , lView ) ;
153
+ return lView ;
154
+ }
155
+ }
156
+ }
157
+ return null ;
158
+ }
159
+
160
+ function handleEvent ( event : EventInfoWrapper ) {
161
+ const nativeElement = event . getAction ( ) ! . element ;
162
+ // Dispatch event via Angular's logic
163
+ if ( nativeElement ) {
164
+ const lView = getLViewByElement ( nativeElement as HTMLElement ) ;
165
+ if ( lView !== null ) {
166
+ const tView = lView [ TVIEW ] ;
167
+ const eventName = event . getEventType ( ) ;
168
+ const origEvent = event . getEvent ( ) ;
169
+ const listeners = getEventListeners ( tView , lView , nativeElement , eventName ) ;
170
+ for ( const listener of listeners ) {
171
+ listener ( origEvent ) ;
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ type Listener = ( ( value : Event ) => unknown ) | ( ( ) => unknown ) ;
178
+
179
+ function getEventListeners (
180
+ tView : TView , lView : LView , nativeElement : Element , eventName : string ) : Listener [ ] {
181
+ const listeners : Listener [ ] = [ ] ;
182
+ const lCleanup = lView [ CLEANUP ] ;
183
+ const tCleanup = tView . cleanup ;
184
+ if ( tCleanup && lCleanup ) {
185
+ for ( let i = 0 ; i < tCleanup . length ; ) {
186
+ const storedEventName = tCleanup [ i ++ ] ;
187
+ const nativeElementIndex = tCleanup [ i ++ ] ;
188
+ if ( typeof storedEventName === 'string' ) {
189
+ const listenerElement = unwrapRNode ( lView [ nativeElementIndex ] ) as any as Element ;
190
+ const listener : Listener = lCleanup [ tCleanup [ i ++ ] ] ;
191
+ i ++ ; // increment to the next position;
192
+ if ( listenerElement === nativeElement && eventName === storedEventName ) {
193
+ listeners . push ( listener ) ;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ return listeners ;
199
+ }
0 commit comments