lVlyke / lithium-ngx-virtual-scroll

A fast and lightweight virtual scrolling solution for Angular that supports single column lists, grid lists and view caching.
MIT License
23 stars 4 forks source link

ScrollPosition not remembered across navigation #23

Closed ghenry22 closed 1 year ago

ghenry22 commented 1 year ago

Scroll down the list, check the scrollPosition value on the li-virtual-scroll. For example is 16448.

Click the item to navigate to a detail view. Go back to the list view.

The position in the list has changed. Check the scrollPosition, it's now 18640.

Scroll position (and hence visible position in the list) should be retained without change when clicking through to a detail view and then back to the list view.

ghenry22 commented 1 year ago

Update: enabling viewCache seems to resolve this, could be worthwhile adding this behaviour to the documentation or enabling the view cache by default.

lVlyke commented 1 year ago

Restoring the previous scroll position is out of scope for this library. The component responds to native HTML scroll events, so you should be able to use other generic solutions for restoring scroll position on navigation changes, such as the examples described here and here.

Additionally, here’s an example of a simple way to track and restore scroll positions for any kind of list, including virtual scroll lists using a basic directive and service:

Service:

import { Injectable } from '@angular/core';

export interface SavedScrollPosition {
    position: number;
}

@Injectable()
export class ScrollTracker {
    private scrollPositions: Record<string, SavedScrollPosition> = {};

    public saveScrollPosition(elementId: string, scrollPosition: SavedScrollPosition) {
        this.scrollPositions[elementId] = scrollPosition;
    }

    public getScrollPosition(elementId: string): SavedScrollPosition {
        return this.scrollPositions[elementId];
    }
}

Directive:

import { Directive, ElementRef, Input } from '@angular/core';
import { ScrollTracker } from './scroll-tracker.service';
import { Observable, fromEvent } from 'rxjs';
import { debounceTime, delay } from 'rxjs/operators';
import { AfterViewInit } from '@lithiumjs/angular';

@Directive({
    selector: '[scrollTracker]',
    standalone: true
})
export class ScrollTrackerDirective {

    @Input('scrollTracker')
    public elementId!: string;

    @AfterViewInit()
    private readonly afterViewInit$!: Observable<void>;

    private readonly element: HTMLElement;

    constructor(
        elementRef: ElementRef,
        private scrollTracker: ScrollTracker,
    ) {
        this.element = elementRef.nativeElement;

        // Wait for list rendering to complete and restore previous scroll pos
        this.afterViewInit$.pipe(
            delay(0)
        ).subscribe(() => this.restorePosition());

        // Store scroll pos every 250ms
        fromEvent<MouseEvent>(this.element, 'scroll').pipe(
            debounceTime(250)
        ).subscribe(() => this.savePosition());
    }

    private savePosition(): void {
        this.scrollTracker.saveScrollPosition(this.elementId, { position: this.element.scrollTop });
    }

    private restorePosition(): void {
        const scrollPos = this.scrollTracker.getScrollPosition(this.elementId);
        if (scrollPos !== undefined) {
            this.element.scrollTo({ top: scrollPos.position });
        }
    }
}

Then you can simply add the directive to your lists with a unique id, i.e.:

<li-virtual-scroll scrollTracker="some-virtual-list">
...
</li-virtual-scroll>
ghenry22 commented 1 year ago

That’s very useful, thanks for the example I will use a variation of that in a few places.

for most of my uses though the viewcache setting resolved this.

mill close this ticket off.