Search code examples
javascriptangulartypescriptdatatableprimeng

Is there a way to preserve PrimeNG table scroll position on data update?


I'm currently working on a project with PrimeNG 12 tables using Angular 12, but I'm having a bit of trouble with Angular's change detection and table updates, specifically with trying to prevent a PrimeNG p-table from scrolling back to the top on data refresh.

For some background, I have a service (let's call it LocationService) that is responsible for holding a list of locations and updating them every, say, 500 milliseconds. Something like the following:

location.ts

interface Location {
    id: string;
    lat: number;
    lng: number
}

location.service.ts

// RxJS Behaviour Subjects
_locationUpdates$: BehaviorSubject<Location[]> = new BehaviorSubject<Location[]>([]);

// RxJS Observables
locationUpdates$: Observable<Location[]> = _locationUpdates.asObservable();

// Service Function
startUpdates() {

    // Called within a service
    timer(0, 500).subscribe(() => {
        this._locations.forEach(location => {
            location.lat = // ... some method to calculate new latitude
            location.lng = // ... some method to calculate new longitude
        });

        this._locationUpdates$.next(this._locations);
    });
}

Now, I've got a component that subscribes to the location updates and displays them as (Lat, Lng) in a p-table, but also has the requirement that the longitude is between -45.0 and 45.0 degrees (other components may not have this requirement). Scroll order doesn't matter, although most of the time it's in the same order since the rows aren't changing all that much.

I am currently subscribing to it like so, using virtual scrolling as there may be hundreds of potential rows:

location-component.html

<ng-container *ngIf="locations$ | async as locations">
    <p-table
        dataKey="id"
        scrollHeight="flex"
        [rows]="100"
        [scrollable]="true"
        [value]="locations"
        [virtualScroll]="true"
        [virtualRowHeight]="34"
    >
        <ng-template pTemplate="header">
            <tr>
                <th>Latitude</th>
                <th>Longitude</th>
            </tr>
        </ng-template>

        <ng-template pTemplate="body" let-location>
            <tr>
                <td>{{ location.lat }}</td>
                <td>{{ location.lng }}</td>
            </tr>
        </ng-template>
    </p-table>
</ng-container>

location-component.ts

// RxJS observables
locations$: Observable<Location[]>;

// Constructor
constructor(private _locationService: LocationService) {
    locations$ = _locationService.locationUpdates$
        .pipe(
            map(locations => locations.filter(location => location.lng >= -45.0 && location.lng <= 45.0))
        )
}

The issue here is that when I run the code, the table snaps to the top every time the data is refreshed. I presume this is due to the fact that the array reference changes. However, if I move away from the async pipe and use a local array, simply modifying it on updates like so:

.subscribe(locations => {
    this._locations.splice(0, this._locations.length, ...locations.filter(location => location.lng >= -45.0 && location.lng <= 45.0));
    this._changeDetectorRef.detectChanges();
}

Angular doesn't detect the change, even with he detectChanges() call (it seems to do nothing). And using something like this._locations = this._locations.slice() to notify it another way just brings me back to the problem above.

My question to y'all is this: does anyone have any tips on how I might solve this problem? My goal is to refresh the data every X milliseconds but to also allow the user to scroll as it updates.

Any help would be greatly appreciated!


Solution

  • Alright, so I managed to find the culprit behind this - PrimeNG's default table behaviour is to call the function resetScrollTop() every time a new filter is applied.

    In my case, I have a number of filters added to the table (text filter, a couple boolean filters, etc.), which means that every time the table data is updated every 500 milliseconds or so, these filters are re-applied, causing the table scroll position to constantly be reset (as seen in the PrimeNG source here).

    I managed to solve this in a bit of a hacky way by simply overwriting the function for this specific table:

    location-component.html

    <p-table #locationsTable ...> ... </p-table>
    

    location-component.ts

    ngAfterViewInit() {
        if (this.locationsTable != null) {
            this.locationsTable.resetScrollTop = function() { }
        }
    }
    

    This seems to fix the issue for now, although if anyone knows a better way I'm always open to hearing it. Hopefully though this answer will help someone else in a similar situation in the future.