@@ -26,6 +26,9 @@ import {
26
26
signal ,
27
27
ViewChild ,
28
28
ViewEncapsulation ,
29
+ ApplicationRef ,
30
+ effect ,
31
+ linkedSignal ,
29
32
} from '@angular/core' ;
30
33
import {
31
34
animationFrameScheduler ,
@@ -170,19 +173,16 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
170
173
*/
171
174
private _renderedContentOffsetNeedsRewrite = false ;
172
175
173
- /** Whether there is a pending change detection cycle. */
174
- private _isChangeDetectionPending = false ;
175
-
176
176
/** A list of functions to run after the next change detection cycle. */
177
- private _runAfterChangeDetection : Function [ ] = [ ] ;
177
+ private _runAfterChangeDetection = signal < Function [ ] > ( [ ] ) ;
178
178
179
179
/** Subscription to changes in the viewport size. */
180
180
private _viewportChanges = Subscription . EMPTY ;
181
181
182
- private _injector = inject ( Injector ) ;
183
-
184
182
private _isDestroyed = false ;
185
183
184
+ private _changeDetectionNeeded = linkedSignal ( ( ) => this . _runAfterChangeDetection ( ) . length > 0 ) ;
185
+
186
186
constructor ( ...args : unknown [ ] ) ;
187
187
188
188
constructor ( ) {
@@ -202,6 +202,41 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
202
202
this . elementRef . nativeElement . classList . add ( 'cdk-virtual-scrollable' ) ;
203
203
this . scrollable = this ;
204
204
}
205
+
206
+ const injector = inject ( ApplicationRef ) . injector ;
207
+ effect (
208
+ ( ) => {
209
+ if ( ! this . _changeDetectionNeeded ( ) || this . _isDestroyed ) {
210
+ return ;
211
+ }
212
+
213
+ // Apply the content transform. The transform can't be set via an Angular binding because
214
+ // bypassSecurityTrustStyle is banned in Google. However the value is safe, it's composed of
215
+ // string literals, a variable that can only be 'X' or 'Y', and user input that is run through
216
+ // the `Number` function first to coerce it to a numeric value.
217
+ this . _contentWrapper . nativeElement . style . transform = this . _renderedContentTransform ;
218
+
219
+ // Apply changes to Angular bindings. Note: We must call `markForCheck` to run change detection
220
+ // from the root, since the repeated items are content projected in. Calling `detectChanges`
221
+ // instead does not properly check the projected content.
222
+ this . _changeDetectorRef . markForCheck ( ) ;
223
+
224
+ afterNextRender (
225
+ {
226
+ mixedReadWrite : ( ) => {
227
+ // TODO: should this be done at the top?
228
+ const runAfterChangeDetection = this . _runAfterChangeDetection ( ) ;
229
+ this . _runAfterChangeDetection . set ( [ ] ) ;
230
+ for ( const fn of runAfterChangeDetection ) {
231
+ fn ( ) ;
232
+ }
233
+ } ,
234
+ } ,
235
+ { injector} ,
236
+ ) ;
237
+ } ,
238
+ { injector} ,
239
+ ) ;
205
240
}
206
241
207
242
override ngOnInit ( ) {
@@ -238,7 +273,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
238
273
)
239
274
. subscribe ( ( ) => this . _scrollStrategy . onContentScrolled ( ) ) ;
240
275
241
- this . _markChangeDetectionNeeded ( ) ;
276
+ this . _changeDetectionNeeded . set ( true ) ;
242
277
} ) ,
243
278
) ;
244
279
}
@@ -274,7 +309,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
274
309
this . _dataLength = newLength ;
275
310
this . _scrollStrategy . onDataLengthChanged ( ) ;
276
311
}
277
- this . _doChangeDetection ( ) ;
312
+ this . _changeDetectionNeeded . set ( true ) ;
278
313
} ) ;
279
314
} ) ;
280
315
}
@@ -317,7 +352,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
317
352
if ( this . _totalContentSize !== size ) {
318
353
this . _totalContentSize = size ;
319
354
this . _calculateSpacerSize ( ) ;
320
- this . _markChangeDetectionNeeded ( ) ;
355
+ this . _changeDetectionNeeded . set ( true ) ;
321
356
}
322
357
}
323
358
@@ -328,7 +363,11 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
328
363
range = { start : 0 , end : Math . max ( this . _renderedRange . end , range . end ) } ;
329
364
}
330
365
this . _renderedRangeSubject . next ( ( this . _renderedRange = range ) ) ;
331
- this . _markChangeDetectionNeeded ( ( ) => this . _scrollStrategy . onContentRendered ( ) ) ;
366
+ this . _runAfterChangeDetection . update ( v => [
367
+ ...v ,
368
+ ( ) => this . _scrollStrategy . onContentRendered ( ) ,
369
+ ] ) ;
370
+ this . _changeDetectionNeeded . set ( true ) ;
332
371
}
333
372
}
334
373
@@ -366,15 +405,19 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
366
405
// We know this value is safe because we parse `offset` with `Number()` before passing it
367
406
// into the string.
368
407
this . _renderedContentTransform = transform ;
369
- this . _markChangeDetectionNeeded ( ( ) => {
370
- if ( this . _renderedContentOffsetNeedsRewrite ) {
371
- this . _renderedContentOffset -= this . measureRenderedContentSize ( ) ;
372
- this . _renderedContentOffsetNeedsRewrite = false ;
373
- this . setRenderedContentOffset ( this . _renderedContentOffset ) ;
374
- } else {
375
- this . _scrollStrategy . onRenderedOffsetChanged ( ) ;
376
- }
377
- } ) ;
408
+ this . _runAfterChangeDetection . update ( v => [
409
+ ...v ,
410
+ ( ) => {
411
+ if ( this . _renderedContentOffsetNeedsRewrite ) {
412
+ this . _renderedContentOffset -= this . measureRenderedContentSize ( ) ;
413
+ this . _renderedContentOffsetNeedsRewrite = false ;
414
+ this . setRenderedContentOffset ( this . _renderedContentOffset ) ;
415
+ } else {
416
+ this . _scrollStrategy . onRenderedOffsetChanged ( ) ;
417
+ }
418
+ } ,
419
+ ] ) ;
420
+ this . _changeDetectionNeeded . set ( true ) ;
378
421
}
379
422
}
380
423
@@ -482,56 +525,6 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
482
525
this . _viewportSize = this . scrollable . measureViewportSize ( this . orientation ) ;
483
526
}
484
527
485
- /** Queue up change detection to run. */
486
- private _markChangeDetectionNeeded ( runAfter ?: Function ) {
487
- if ( runAfter ) {
488
- this . _runAfterChangeDetection . push ( runAfter ) ;
489
- }
490
-
491
- // Use a Promise to batch together calls to `_doChangeDetection`. This way if we set a bunch of
492
- // properties sequentially we only have to run `_doChangeDetection` once at the end.
493
- if ( ! this . _isChangeDetectionPending ) {
494
- this . _isChangeDetectionPending = true ;
495
- this . ngZone . runOutsideAngular ( ( ) =>
496
- Promise . resolve ( ) . then ( ( ) => {
497
- this . _doChangeDetection ( ) ;
498
- } ) ,
499
- ) ;
500
- }
501
- }
502
-
503
- /** Run change detection. */
504
- private _doChangeDetection ( ) {
505
- if ( this . _isDestroyed ) {
506
- return ;
507
- }
508
-
509
- this . ngZone . run ( ( ) => {
510
- // Apply changes to Angular bindings. Note: We must call `markForCheck` to run change detection
511
- // from the root, since the repeated items are content projected in. Calling `detectChanges`
512
- // instead does not properly check the projected content.
513
- this . _changeDetectorRef . markForCheck ( ) ;
514
-
515
- // Apply the content transform. The transform can't be set via an Angular binding because
516
- // bypassSecurityTrustStyle is banned in Google. However the value is safe, it's composed of
517
- // string literals, a variable that can only be 'X' or 'Y', and user input that is run through
518
- // the `Number` function first to coerce it to a numeric value.
519
- this . _contentWrapper . nativeElement . style . transform = this . _renderedContentTransform ;
520
-
521
- afterNextRender (
522
- ( ) => {
523
- this . _isChangeDetectionPending = false ;
524
- const runAfterChangeDetection = this . _runAfterChangeDetection ;
525
- this . _runAfterChangeDetection = [ ] ;
526
- for ( const fn of runAfterChangeDetection ) {
527
- fn ( ) ;
528
- }
529
- } ,
530
- { injector : this . _injector } ,
531
- ) ;
532
- } ) ;
533
- }
534
-
535
528
/** Calculates the `style.width` and `style.height` for the spacer element. */
536
529
private _calculateSpacerSize ( ) {
537
530
this . _totalContentHeight . set (
0 commit comments