@@ -10,7 +10,7 @@ import {
10
10
ChangeDetectionScheduler ,
11
11
NotificationSource ,
12
12
} from '../change_detection/scheduling/zoneless_scheduling' ;
13
- import { assertInInjectionContext , Injector , runInInjectionContext , ɵɵdefineInjectable } from '../di' ;
13
+ import { Injector , assertInInjectionContext , runInInjectionContext , ɵɵdefineInjectable } from '../di' ;
14
14
import { inject } from '../di/injector_compatibility' ;
15
15
import { ErrorHandler } from '../error_handler' ;
16
16
import { DestroyRef } from '../linker/destroy_ref' ;
@@ -20,6 +20,16 @@ import {NgZone} from '../zone/ng_zone';
20
20
21
21
import { isPlatformBrowser } from './util/misc_utils' ;
22
22
23
+ /**
24
+ * An argument list containing the first non-never type in the given type array, or an empty
25
+ * argument list if there are no non-never types in the type array.
26
+ */
27
+ export type ɵFirstAvailable < T extends unknown [ ] > = T extends [ infer H , ...infer R ]
28
+ ? [ H ] extends [ never ]
29
+ ? ɵFirstAvailable < R >
30
+ : [ H ]
31
+ : [ ] ;
32
+
23
33
/**
24
34
* The phase to run an `afterRender` or `afterNextRender` callback in.
25
35
*
@@ -37,15 +47,14 @@ import {isPlatformBrowser} from './util/misc_utils';
37
47
* so, Angular is better able to minimize the performance degradation associated with
38
48
* manual DOM access, ensuring the best experience for the end users of your application
39
49
* or library.
40
- *
41
- * @developerPreview
42
50
*/
43
- export enum AfterRenderPhase {
51
+ enum AfterRenderPhase {
44
52
/**
45
53
* Use `AfterRenderPhase.EarlyRead` for callbacks that only need to **read** from the
46
54
* DOM before a subsequent `AfterRenderPhase.Write` callback, for example to perform
47
- * custom layout that the browser doesn't natively support. **Never** use this phase
48
- * for callbacks that can write to the DOM or when `AfterRenderPhase.Read` is adequate.
55
+ * custom layout that the browser doesn't natively support. Prefer the
56
+ * `AfterRenderPhase.EarlyRead` phase if reading can wait until after the write phase.
57
+ * **Never** write to the DOM in this phase.
49
58
*
50
59
* <div class="alert is-important">
51
60
*
@@ -58,27 +67,27 @@ export enum AfterRenderPhase {
58
67
59
68
/**
60
69
* Use `AfterRenderPhase.Write` for callbacks that only **write** to the DOM. **Never**
61
- * use this phase for callbacks that can read from the DOM.
70
+ * read from the DOM in this phase .
62
71
*/
63
72
Write ,
64
73
65
74
/**
66
75
* Use `AfterRenderPhase.MixedReadWrite` for callbacks that read from or write to the
67
- * DOM, that haven't been refactored to use a different phase. **Never** use this phase
68
- * for callbacks that can use a different phase instead.
76
+ * DOM, that haven't been refactored to use a different phase. **Never** use this phase if
77
+ * it is possible to divide the work among the other phases instead.
69
78
*
70
79
* <div class="alert is-critical">
71
80
*
72
81
* Using this value can **significantly** degrade performance.
73
- * Instead, prefer refactoring into multiple callbacks using a more specific phase.
82
+ * Instead, prefer dividing work into the appropriate phase callbacks .
74
83
*
75
84
* </div>
76
85
*/
77
86
MixedReadWrite ,
78
87
79
88
/**
80
89
* Use `AfterRenderPhase.Read` for callbacks that only **read** from the DOM. **Never**
81
- * use this phase for callbacks that can write to the DOM.
90
+ * write to the DOM in this phase .
82
91
*/
83
92
Read ,
84
93
}
@@ -95,18 +104,6 @@ export interface AfterRenderOptions {
95
104
* If this is not provided, the current injection context will be used instead (via `inject`).
96
105
*/
97
106
injector ?: Injector ;
98
-
99
- /**
100
- * The phase the callback should be invoked in.
101
- *
102
- * <div class="alert is-critical">
103
- *
104
- * Defaults to `AfterRenderPhase.MixedReadWrite`. You should choose a more specific
105
- * phase instead. See `AfterRenderPhase` for more information.
106
- *
107
- * </div>
108
- */
109
- phase ?: AfterRenderPhase ;
110
107
}
111
108
112
109
/**
@@ -174,20 +171,107 @@ export function internalAfterNextRender(
174
171
}
175
172
176
173
/**
177
- * Register a callback to be invoked each time the application
178
- * finishes rendering.
174
+ * Register callbacks to be invoked each time the application finishes rendering, during the
175
+ * specified phases. The available phases are:
176
+ * - `earlyRead`
177
+ * Use this phase to **read** from the DOM before a subsequent `write` callback, for example to
178
+ * perform custom layout that the browser doesn't natively support. Prefer the `read` phase if
179
+ * reading can wait until after the write phase. **Never** write to the DOM in this phase.
180
+ * - `write`
181
+ * Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.
182
+ * - `mixedReadWrite`
183
+ * Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if
184
+ * it is possible to divide the work among the other phases instead.
185
+ * - `read`
186
+ * Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.
187
+ *
188
+ * <div class="alert is-critical">
189
+ *
190
+ * You should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`
191
+ * phases when possible, to avoid performance degradation.
192
+ *
193
+ * </div>
194
+ *
195
+ * Note that:
196
+ * - Callbacks run in the following phase order *after each render*:
197
+ * 1. `earlyRead`
198
+ * 2. `write`
199
+ * 3. `mixedReadWrite`
200
+ * 4. `read`
201
+ * - Callbacks in the same phase run in the order they are registered.
202
+ * - Callbacks run on browser platforms only, they will not run on the server.
203
+ *
204
+ * The first phase callback to run as part of this spec will receive no parameters. Each
205
+ * subsequent phase callback in this spec will receive the return value of the previously run
206
+ * phase callback as a parameter. This can be used to coordinate work across multiple phases.
207
+ *
208
+ * Angular is unable to verify or enforce that phases are used correctly, and instead
209
+ * relies on each developer to follow the guidelines documented for each value and
210
+ * carefully choose the appropriate one, refactoring their code if necessary. By doing
211
+ * so, Angular is better able to minimize the performance degradation associated with
212
+ * manual DOM access, ensuring the best experience for the end users of your application
213
+ * or library.
214
+ *
215
+ * <div class="alert is-important">
216
+ *
217
+ * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.
218
+ * You must use caution when directly reading or writing the DOM and layout.
219
+ *
220
+ * </div>
221
+ *
222
+ * @param spec The callback functions to register
223
+ *
224
+ * @usageNotes
225
+ *
226
+ * Use `afterRender` to read or write the DOM after each render.
227
+ *
228
+ * ### Example
229
+ * ```ts
230
+ * @Component ({
231
+ * selector: 'my-cmp',
232
+ * template: `<span #content>{{ ... }}</span>`,
233
+ * })
234
+ * export class MyComponent {
235
+ * @ViewChild ('content') contentRef: ElementRef;
236
+ *
237
+ * constructor() {
238
+ * afterRender({
239
+ * read: () => {
240
+ * console.log('content height: ' + this.contentRef.nativeElement.scrollHeight);
241
+ * }
242
+ * });
243
+ * }
244
+ * }
245
+ * ```
246
+ *
247
+ * @developerPreview
248
+ */
249
+ export function afterRender < E = never , W = never , M = never > (
250
+ spec : {
251
+ earlyRead ?: ( ) => E ;
252
+ write ?: ( ...args : ɵFirstAvailable < [ E ] > ) => W ;
253
+ mixedReadWrite ?: ( ...args : ɵFirstAvailable < [ W , E ] > ) => M ;
254
+ read ?: ( ...args : ɵFirstAvailable < [ M , W , E ] > ) => void ;
255
+ } ,
256
+ opts ?: AfterRenderOptions ,
257
+ ) : AfterRenderRef ;
258
+
259
+ /**
260
+ * Register a callback to be invoked each time the application finishes rendering, during the
261
+ * `mixedReadWrite` phase.
179
262
*
180
263
* <div class="alert is-critical">
181
264
*
182
- * You should always explicitly specify a non-default [ phase](api/core/AfterRenderPhase) , or you
183
- * risk significant performance degradation.
265
+ * You should prefer specifying an explicit phase for the callback instead , or you risk significant
266
+ * performance degradation.
184
267
*
185
268
* </div>
186
269
*
187
270
* Note that the callback will run
188
271
* - in the order it was registered
189
272
* - once per render
190
273
* - on browser platforms only
274
+ * - during the `mixedReadWrite` phase
191
275
*
192
276
* <div class="alert is-important">
193
277
*
@@ -212,16 +296,30 @@ export function internalAfterNextRender(
212
296
* @ViewChild ('content') contentRef: ElementRef;
213
297
*
214
298
* constructor() {
215
- * afterRender(() => {
216
- * console.log('content height: ' + this.contentRef.nativeElement.scrollHeight);
217
- * }, {phase: AfterRenderPhase.Read});
299
+ * afterRender({
300
+ * read: () => {
301
+ * console.log('content height: ' + this.contentRef.nativeElement.scrollHeight);
302
+ * }
303
+ * });
218
304
* }
219
305
* }
220
306
* ```
221
307
*
222
308
* @developerPreview
223
309
*/
224
- export function afterRender ( callback : VoidFunction , options ?: AfterRenderOptions ) : AfterRenderRef {
310
+ export function afterRender ( callback : VoidFunction , options ?: AfterRenderOptions ) : AfterRenderRef ;
311
+
312
+ export function afterRender (
313
+ callbackOrSpec :
314
+ | VoidFunction
315
+ | {
316
+ earlyRead ?: ( ) => unknown ;
317
+ write ?: ( r ?: unknown ) => unknown ;
318
+ mixedReadWrite ?: ( r ?: unknown ) => unknown ;
319
+ read ?: ( r ?: unknown ) => void ;
320
+ } ,
321
+ options ?: AfterRenderOptions ,
322
+ ) : AfterRenderRef {
225
323
ngDevMode &&
226
324
assertNotInReactiveContext (
227
325
afterRender ,
@@ -238,37 +336,112 @@ export function afterRender(callback: VoidFunction, options?: AfterRenderOptions
238
336
239
337
performanceMarkFeature ( 'NgAfterRender' ) ;
240
338
241
- const afterRenderEventManager = injector . get ( AfterRenderEventManager ) ;
242
- // Lazily initialize the handler implementation, if necessary. This is so that it can be
243
- // tree-shaken if `afterRender` and `afterNextRender` aren't used.
244
- const callbackHandler = ( afterRenderEventManager . handler ??=
245
- new AfterRenderCallbackHandlerImpl ( ) ) ;
246
- const phase = options ?. phase ?? AfterRenderPhase . MixedReadWrite ;
247
- const destroy = ( ) => {
248
- callbackHandler . unregister ( instance ) ;
249
- unregisterFn ( ) ;
250
- } ;
251
- const unregisterFn = injector . get ( DestroyRef ) . onDestroy ( destroy ) ;
252
- const instance = runInInjectionContext ( injector , ( ) => new AfterRenderCallback ( phase , callback ) ) ;
253
-
254
- callbackHandler . register ( instance ) ;
255
- return { destroy} ;
339
+ return afterRenderImpl ( callbackOrSpec , injector , /* once */ false ) ;
256
340
}
257
341
258
342
/**
259
- * Register a callback to be invoked the next time the application
260
- * finishes rendering.
343
+ * Register callbacks to be invoked the next time the application finishes rendering, during the
344
+ * specified phases. The available phases are:
345
+ * - `earlyRead`
346
+ * Use this phase to **read** from the DOM before a subsequent `write` callback, for example to
347
+ * perform custom layout that the browser doesn't natively support. Prefer the `read` phase if
348
+ * reading can wait until after the write phase. **Never** write to the DOM in this phase.
349
+ * - `write`
350
+ * Use this phase to **write** to the DOM. **Never** read from the DOM in this phase.
351
+ * - `mixedReadWrite`
352
+ * Use this phase to read from and write to the DOM simultaneously. **Never** use this phase if
353
+ * it is possible to divide the work among the other phases instead.
354
+ * - `read`
355
+ * Use this phase to **read** from the DOM. **Never** write to the DOM in this phase.
356
+ *
357
+ * <div class="alert is-critical">
358
+ *
359
+ * You should prefer using the `read` and `write` phases over the `earlyRead` and `mixedReadWrite`
360
+ * phases when possible, to avoid performance degradation.
361
+ *
362
+ * </div>
363
+ *
364
+ * Note that:
365
+ * - Callbacks run in the following phase order *once, after the next render*:
366
+ * 1. `earlyRead`
367
+ * 2. `write`
368
+ * 3. `mixedReadWrite`
369
+ * 4. `read`
370
+ * - Callbacks in the same phase run in the order they are registered.
371
+ * - Callbacks run on browser platforms only, they will not run on the server.
372
+ *
373
+ * The first phase callback to run as part of this spec will receive no parameters. Each
374
+ * subsequent phase callback in this spec will receive the return value of the previously run
375
+ * phase callback as a parameter. This can be used to coordinate work across multiple phases.
376
+ *
377
+ * Angular is unable to verify or enforce that phases are used correctly, and instead
378
+ * relies on each developer to follow the guidelines documented for each value and
379
+ * carefully choose the appropriate one, refactoring their code if necessary. By doing
380
+ * so, Angular is better able to minimize the performance degradation associated with
381
+ * manual DOM access, ensuring the best experience for the end users of your application
382
+ * or library.
383
+ *
384
+ * <div class="alert is-important">
385
+ *
386
+ * Components are not guaranteed to be [hydrated](guide/hydration) before the callback runs.
387
+ * You must use caution when directly reading or writing the DOM and layout.
388
+ *
389
+ * </div>
390
+ *
391
+ * @param spec The callback functions to register
392
+ *
393
+ * @usageNotes
394
+ *
395
+ * Use `afterNextRender` to read or write the DOM once,
396
+ * for example to initialize a non-Angular library.
397
+ *
398
+ * ### Example
399
+ * ```ts
400
+ * @Component ({
401
+ * selector: 'my-chart-cmp',
402
+ * template: `<div #chart>{{ ... }}</div>`,
403
+ * })
404
+ * export class MyChartCmp {
405
+ * @ViewChild ('chart') chartRef: ElementRef;
406
+ * chart: MyChart|null;
407
+ *
408
+ * constructor() {
409
+ * afterNextRender({
410
+ * write: () => {
411
+ * this.chart = new MyChart(this.chartRef.nativeElement);
412
+ * }
413
+ * });
414
+ * }
415
+ * }
416
+ * ```
417
+ *
418
+ * @developerPreview
419
+ */
420
+ export function afterNextRender < E = never , W = never , M = never > (
421
+ spec : {
422
+ earlyRead ?: ( ) => E ;
423
+ write ?: ( ...args : ɵFirstAvailable < [ E ] > ) => W ;
424
+ mixedReadWrite ?: ( ...args : ɵFirstAvailable < [ W , E ] > ) => M ;
425
+ read ?: ( ...args : ɵFirstAvailable < [ M , W , E ] > ) => void ;
426
+ } ,
427
+ opts ?: AfterRenderOptions ,
428
+ ) : AfterRenderRef ;
429
+
430
+ /**
431
+ * Register a callback to be invoked the next time the application finishes rendering, during the
432
+ * `mixedReadWrite` phase.
261
433
*
262
434
* <div class="alert is-critical">
263
435
*
264
- * You should always explicitly specify a non-default [ phase](api/core/AfterRenderPhase) , or you
265
- * risk significant performance degradation.
436
+ * You should prefer specifying an explicit phase for the callback instead , or you risk significant
437
+ * performance degradation.
266
438
*
267
439
* </div>
268
440
*
269
441
* Note that the callback will run
270
442
* - in the order it was registered
271
443
* - on browser platforms only
444
+ * - during the `mixedReadWrite` phase
272
445
*
273
446
* <div class="alert is-important">
274
447
*
@@ -295,9 +468,11 @@ export function afterRender(callback: VoidFunction, options?: AfterRenderOptions
295
468
* chart: MyChart|null;
296
469
*
297
470
* constructor() {
298
- * afterNextRender(() => {
299
- * this.chart = new MyChart(this.chartRef.nativeElement);
300
- * }, {phase: AfterRenderPhase.Write});
471
+ * afterNextRender({
472
+ * write: () => {
473
+ * this.chart = new MyChart(this.chartRef.nativeElement);
474
+ * }
475
+ * });
301
476
* }
302
477
* }
303
478
* ```
@@ -307,6 +482,18 @@ export function afterRender(callback: VoidFunction, options?: AfterRenderOptions
307
482
export function afterNextRender (
308
483
callback : VoidFunction ,
309
484
options ?: AfterRenderOptions ,
485
+ ) : AfterRenderRef ;
486
+
487
+ export function afterNextRender (
488
+ callbackOrSpec :
489
+ | VoidFunction
490
+ | {
491
+ earlyRead ?: ( ) => unknown ;
492
+ write ?: ( r ?: unknown ) => unknown ;
493
+ mixedReadWrite ?: ( r ?: unknown ) => unknown ;
494
+ read ?: ( r ?: unknown ) => void ;
495
+ } ,
496
+ options ?: AfterRenderOptions ,
310
497
) : AfterRenderRef {
311
498
! options && assertInInjectionContext ( afterNextRender ) ;
312
499
const injector = options ?. injector ?? inject ( Injector ) ;
@@ -317,27 +504,75 @@ export function afterNextRender(
317
504
318
505
performanceMarkFeature ( 'NgAfterNextRender' ) ;
319
506
507
+ return afterRenderImpl ( callbackOrSpec , injector , /* once */ true ) ;
508
+ }
509
+
510
+ /**
511
+ * Shared implementation for `afterRender` and `afterNextRender`.
512
+ */
513
+ function afterRenderImpl (
514
+ callbackOrSpec :
515
+ | VoidFunction
516
+ | {
517
+ earlyRead ?: ( ) => unknown ;
518
+ write ?: ( r ?: unknown ) => unknown ;
519
+ mixedReadWrite ?: ( r ?: unknown ) => unknown ;
520
+ read ?: ( r ?: unknown ) => void ;
521
+ } ,
522
+ injector : Injector ,
523
+ once : boolean ,
524
+ ) : AfterRenderRef {
525
+ const spec : {
526
+ earlyRead ?: ( ) => unknown ;
527
+ write ?: ( r ?: unknown ) => unknown ;
528
+ mixedReadWrite ?: ( r ?: unknown ) => unknown ;
529
+ read ?: ( r ?: unknown ) => void ;
530
+ } = callbackOrSpec instanceof Function ? { mixedReadWrite : callbackOrSpec } : callbackOrSpec ;
531
+
320
532
const afterRenderEventManager = injector . get ( AfterRenderEventManager ) ;
321
533
// Lazily initialize the handler implementation, if necessary. This is so that it can be
322
534
// tree-shaken if `afterRender` and `afterNextRender` aren't used.
323
535
const callbackHandler = ( afterRenderEventManager . handler ??=
324
536
new AfterRenderCallbackHandlerImpl ( ) ) ;
325
- const phase = options ?. phase ?? AfterRenderPhase . MixedReadWrite ;
537
+
538
+ const pipelinedArgs : [ ] | [ unknown ] = [ ] ;
539
+ const instances : AfterRenderCallback [ ] = [ ] ;
540
+
326
541
const destroy = ( ) => {
327
- callbackHandler . unregister ( instance ) ;
542
+ for ( const instance of instances ) {
543
+ callbackHandler . unregister ( instance ) ;
544
+ }
328
545
unregisterFn ( ) ;
329
546
} ;
330
547
const unregisterFn = injector . get ( DestroyRef ) . onDestroy ( destroy ) ;
331
- const instance = runInInjectionContext (
332
- injector ,
333
- ( ) =>
334
- new AfterRenderCallback ( phase , ( ) => {
335
- destroy ( ) ;
336
- callback ( ) ;
337
- } ) ,
338
- ) ;
339
-
340
- callbackHandler . register ( instance ) ;
548
+
549
+ const registerCallback = (
550
+ phase : AfterRenderPhase ,
551
+ phaseCallback : undefined | ( ( ...args : unknown [ ] ) => unknown ) ,
552
+ ) => {
553
+ if ( ! phaseCallback ) {
554
+ return ;
555
+ }
556
+ const callback = once
557
+ ? ( ...args : [ unknown ] ) => {
558
+ destroy ( ) ;
559
+ phaseCallback ( ...args ) ;
560
+ }
561
+ : phaseCallback ;
562
+
563
+ const instance = runInInjectionContext (
564
+ injector ,
565
+ ( ) => new AfterRenderCallback ( phase , pipelinedArgs , callback ) ,
566
+ ) ;
567
+ callbackHandler . register ( instance ) ;
568
+ instances . push ( instance ) ;
569
+ } ;
570
+
571
+ registerCallback ( AfterRenderPhase . EarlyRead , spec . earlyRead ) ;
572
+ registerCallback ( AfterRenderPhase . Write , spec . write ) ;
573
+ registerCallback ( AfterRenderPhase . MixedReadWrite , spec . mixedReadWrite ) ;
574
+ registerCallback ( AfterRenderPhase . Read , spec . read ) ;
575
+
341
576
return { destroy} ;
342
577
}
343
578
@@ -350,15 +585,20 @@ class AfterRenderCallback {
350
585
351
586
constructor (
352
587
readonly phase : AfterRenderPhase ,
353
- private callbackFn : VoidFunction ,
588
+ private pipelinedArgs : [ ] | [ unknown ] ,
589
+ private callbackFn : ( ...args : unknown [ ] ) => unknown ,
354
590
) {
355
591
// Registering a callback will notify the scheduler.
356
592
inject ( ChangeDetectionScheduler , { optional : true } ) ?. notify ( NotificationSource . NewRenderHook ) ;
357
593
}
358
594
359
595
invoke ( ) {
360
596
try {
361
- this . zone . runOutsideAngular ( this . callbackFn ) ;
597
+ const result = this . zone . runOutsideAngular ( ( ) =>
598
+ this . callbackFn . apply ( null , this . pipelinedArgs as [ unknown ] ) ,
599
+ ) ;
600
+ // Clear out the args and add the result which will be passed to the next phase.
601
+ this . pipelinedArgs . splice ( 0 , this . pipelinedArgs . length , result ) ;
362
602
} catch ( err ) {
363
603
this . errorHandler ?. handleError ( err ) ;
364
604
}
0 commit comments