diff --git a/apps/demos/src/app/features/template/rx-virtual-for/rx-virtual-for.menu.ts b/apps/demos/src/app/features/template/rx-virtual-for/rx-virtual-for.menu.ts index edb77a425d..33cec9af82 100644 --- a/apps/demos/src/app/features/template/rx-virtual-for/rx-virtual-for.menu.ts +++ b/apps/demos/src/app/features/template/rx-virtual-for/rx-virtual-for.menu.ts @@ -23,4 +23,8 @@ export const RX_VIRTUAL_FOR_MENU_ITEMS = [ label: 'Crazy Update', link: 'crazy-update', }, + { + label: 'Scroll To', + link: 'scroll-to', + }, ]; diff --git a/apps/demos/src/app/features/template/rx-virtual-for/virtual-rendering/virtual-for-experiments.module.ts b/apps/demos/src/app/features/template/rx-virtual-for/virtual-rendering/virtual-for-experiments.module.ts index c8bbc2bde9..a1619b70ac 100644 --- a/apps/demos/src/app/features/template/rx-virtual-for/virtual-rendering/virtual-for-experiments.module.ts +++ b/apps/demos/src/app/features/template/rx-virtual-for/virtual-rendering/virtual-for-experiments.module.ts @@ -27,6 +27,7 @@ import { VirtualForMonkeyTestComponent } from './virtual-for-monkey-test.compone import { VirtualForReverseInfiniteScrollComponent } from './virtual-for-reverse-infinite-scroll.component'; import { VirtualForScrollWindowDemoComponent } from './virtual-for-scroll-window-demo.component'; import { VirtualForCustomScrollableDemoComponent } from './virtual-for-scrollable-demo.component'; +import { VirtualForScrollToDemoComponent } from './virtual-for-scrollto-demo.component'; @NgModule({ imports: [ @@ -60,6 +61,10 @@ import { VirtualForCustomScrollableDemoComponent } from './virtual-for-scrollabl path: 'crazy-update', component: VirtualForCrazyUpdateComponent, }, + { + path: 'scroll-to', + component: VirtualForScrollToDemoComponent, + }, ]), ValueProvidersModule, RxLet, diff --git a/apps/demos/src/app/features/template/rx-virtual-for/virtual-rendering/virtual-for-scrollto-demo.component.ts b/apps/demos/src/app/features/template/rx-virtual-for/virtual-rendering/virtual-for-scrollto-demo.component.ts new file mode 100644 index 0000000000..21bfdea9e3 --- /dev/null +++ b/apps/demos/src/app/features/template/rx-virtual-for/virtual-rendering/virtual-for-scrollto-demo.component.ts @@ -0,0 +1,108 @@ +import { coerceNumberProperty } from '@angular/cdk/coercion'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { NgTemplateOutlet } from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + OnInit, + QueryList, + TemplateRef, + ViewChild, + ViewChildren, +} from '@angular/core'; +import { RxStrategyNames } from '@rx-angular/cdk/render-strategies'; +import { patch, toDictionary, update } from '@rx-angular/cdk/transformations'; +import { RxState } from '@rx-angular/state'; +import { + BehaviorSubject, + combineLatest, + defer, + pairwise, + ReplaySubject, + Subject, + switchMap, +} from 'rxjs'; +import { + distinctUntilChanged, + map, + shareReplay, + startWith, + withLatestFrom, +} from 'rxjs/operators'; +import { ArrayProviderComponent } from '../../../../shared/debug-helper/value-provider/array-provider/array-provider.component'; +import { TestItem } from '../../../../shared/debug-helper/value-provider/index'; +import { + AutoSizeVirtualScrollStrategy, + RxVirtualFor, + RxVirtualScrollViewportComponent, +} from '@rx-angular/template/experimental/virtual-scrolling'; + +@Component({ + selector: 'rxa-virtual-for-scroll-to', + template: ` +

Scroll To

+
+ +
+ {{ item }} +
+
+
+ `, + styles: [ + ` + :host { + display: flex; + flex-flow: column; + height: 100%; + } + .container { + height: 100%; + flex-grow: 1; + } + rx-virtual-scroll-viewport { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + } + + .item { + width: 100%; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + background: lightpink; + border-top: 1px solid gray; + } + `, + ], + providers: [RxState], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + RxVirtualScrollViewportComponent, + AutoSizeVirtualScrollStrategy, + RxVirtualFor, + ], +}) +export class VirtualForScrollToDemoComponent implements OnInit { + items: string[] | undefined; + initialScrollIndex = 5; + + ngOnInit() { + this.items = Array.from({ length: 100 }, (_, i) => i.toString()); + } + + onScrolledIndexChange(index: number) { + console.log('onScrolledIndexChange', index); + } +} diff --git a/libs/template/experimental/virtual-scrolling/src/lib/scroll-strategies/autosize-virtual-scroll-strategy.ts b/libs/template/experimental/virtual-scrolling/src/lib/scroll-strategies/autosize-virtual-scroll-strategy.ts index 0168d72d2c..2df3b7b2a8 100644 --- a/libs/template/experimental/virtual-scrolling/src/lib/scroll-strategies/autosize-virtual-scroll-strategy.ts +++ b/libs/template/experimental/virtual-scrolling/src/lib/scroll-strategies/autosize-virtual-scroll-strategy.ts @@ -14,6 +14,7 @@ import { merge, MonoTypeOperatorFunction, Observable, + of, pairwise, ReplaySubject, Subject, @@ -28,6 +29,7 @@ import { mergeMap, startWith, switchMap, + take, takeUntil, takeWhile, tap, @@ -225,6 +227,14 @@ export class AutoSizeVirtualScrollStrategy< private readonly _scrolledIndex$ = new ReplaySubject(1); /** @internal */ readonly scrolledIndex$ = this._scrolledIndex$.pipe(distinctUntilChanged()); + /** + * @internal + * The action used to kick off the scroll process + */ + private scrollToTrigger$ = new Subject<{ + scrollTop: number; + behavior?: ScrollBehavior; + }>(); /** @internal */ private _scrolledIndex = 0; /** @internal */ @@ -332,6 +342,7 @@ export class AutoSizeVirtualScrollStrategy< this.maintainVirtualItems(); this.calcRenderedRange(); this.positionElements(); + this.listenToScrollTrigger(); } /** @internal */ @@ -352,7 +363,7 @@ export class AutoSizeVirtualScrollStrategy< if (_index !== this.scrolledIndex) { const scrollTop = this.calcInitialPosition(_index); this._scrollToIndex = _index; - this.scrollTo(scrollTop, behavior); + this.scrollToTrigger$.next({ scrollTop, behavior }); } } @@ -796,6 +807,25 @@ export class AutoSizeVirtualScrollStrategy< .subscribe(); } + /** listen to API initiated scroll triggers (e.g. initialScrollIndex) */ + private listenToScrollTrigger(): void { + this.scrollToTrigger$ + .pipe( + switchMap((scrollTo) => + // wait until containerRect at least emitted once + this.containerSize === 0 + ? this.viewport!.containerRect$.pipe( + map(() => scrollTo), + take(1), + ) + : of(scrollTo), + ), + this.until$(), + ) + .subscribe(({ scrollTop, behavior }) => { + this.scrollTo(scrollTop, behavior); + }); + } /** @internal */ private adjustContentSize(position: number) { let newContentSize = position; diff --git a/libs/template/experimental/virtual-scrolling/src/lib/virtual-scroll-viewport.component.scss b/libs/template/experimental/virtual-scrolling/src/lib/virtual-scroll-viewport.component.scss index 9359a3e845..d16f86983b 100644 --- a/libs/template/experimental/virtual-scrolling/src/lib/virtual-scroll-viewport.component.scss +++ b/libs/template/experimental/virtual-scrolling/src/lib/virtual-scroll-viewport.component.scss @@ -12,6 +12,10 @@ position: absolute; top: 0; bottom: 0; + + > * { + position: absolute; + } } &__sentinel { width: 1px;