How to maintain vertical scroll when updating Angular 5 data table?
Asked Answered
U

4

11

I'd like to frequently update my data table (Covalent td-data-table with several ng-template) with new data pulled from a JSON REST API. More rows than will fit on browser so user may need to scroll vertically. But when I update the data in the table it redraws completely i.e. the vertical scroll position resets to the top, tool tips flash, etc..

Hacks to e.g. save/restore the vertical scroll such as below kind of work but they create a lot of visual mess, especially in Firefox.

// save vertical scroll
this.scrollTop = this.tableElt.nativeElement.querySelector('.td-data-table-scrollable').scrollTop;

// update table data here
this.data = newData;

// restore vertical scroll
setImmediate(() => {
    this.tableElt.nativeElement.querySelector('.td-data-table-scrollable').scrollTop = this.scrollTop;
  }
});

How can I cleanly update the data in a table (or any component really) without hacking to reset scroll positions & putting up with a lot of flashing behaviour?

If there is no solution using the Covalent data table, is there another Angular 2+ control that handles this properly?

Animated screen capture of problem: Vertical scroll snaps back when data is updated. Vertical scroll should be maintained across data updates. screencapture

Undoubted answered 11/4, 2018 at 8:33 Comment(6)
hello, i think you need ngx-infinite-scroll , it works perfectly, you can use the (scrolled) to trigger you request and get new data and you will not lose your scroll position, here is an example using it: echoesplayer.com/#/search/videosCharlenecharleroi
At the ngFor you should use for the list, Do you use the trackBy function? Do you do it through the indices? But you need it for this guy. If you put the code in question it will be much easier to help you.Ryanryann
@FatehMohamed it's not clear to me how I might use ngx-infinite with td-data-table.Undoubted
@Ryanryann trackBy sounds like we're on the right track, but td-data-table does not have an ngFor. I can't find any way to use trackBy with td-data-table.Undoubted
After a cursor glance, <td-virtual-scroll-container [style.height.px]="400" [data]="data" [trackBy]="trackByFn"> ref covalent-nightly/virtual-scroll/ - can you wrap your table in a virtual scroll?Frae
@RichardMatsen td-virtual-scroll-container supports only lists, it can neither contain or be contained in a table. Not sure who/why would upvote this answer.Undoubted
P
2

I suggest you switch to angular material and bind it to an observable data source.

https://material.angular.io/components/table/overview#observable-stream-of-data-arrays

When the datasource(Observable) is updated with new data it will update the DOM and there will be no need to follow scroll events.

In case you are worried about the number on items listed on a page supports pagination in a simple way; . https://material.angular.io/components/table/overview#pagination

SideNote: Switch to angular material as their components are well documented, with examples and sample codes Let me know in case you get stuck.

Paucker answered 18/4, 2018 at 14:46 Comment(4)
the angular material data table is very primitive, most egregiously it will not stick table headers in place, they scroll when you scroll the data! That is one of the main reasons I need td-data-table which can stick the table headers in place while the data is scrolled.Undoubted
The sticky header issue was solved. github.com/angular/material2/issues/9143 Use suggested CSS .mat-header-row { position: sticky; position: -webkit-sticky; top: 0; background-color: inherit; }Paucker
thanks I didn't realize that, that may be a direction forward. Still I have dozens of tables I'd like to avoid rewriting if possible, question is about td-data-table not mat-table.Undoubted
In end I went with @stevebiko's approach and switched to native angular material table. works ok, still supports templates, sticky workaround works well. I ditched td-data-table, never got it working to satisfaction. sorry happened too late for the bounty.Undoubted
P
9

You could try using a trackBy function. This function will be used to determine what rows of your *ngFor have changed to optimize redraws.

<tbody>
<tr td-data-table-row *ngFor="let row of basicData; trackBy: getRowId">
  <td td-data-table-cell *ngFor="let column of columns" [numeric]="column.numeric">
    {{column.format ? column.format(row[column.name]) : row[column.name]}}
  </td>
  <td td-data-table-cell (click)="openPrompt(row, 'comments')">
    <button mat-button [class.mat-accent]="!row['comments']">{{row['comments'] || 'Add Comment'}}</button>
  </td>
</tr>
</tbody>

And then in your Typescript:

getRowById(index, row) {
   // Return some unique primitive idenitifier (string, number, etc.)
   return row['some unique property'];
}

For more info on trackBy check out:

https://netbasal.com/angular-2-improve-performance-with-trackby-cc147b5104e5 https://blog.angular-university.io/angular-2-ngfor/

Also, NGX-Datatable works very well for this use-case. It has built in virtual scrolling. https://github.com/swimlane/ngx-datatable

Powers answered 18/4, 2018 at 16:37 Comment(4)
I'm using td-data-table not ngx-datatable. trackBy may be an answer with ngx-datatable but it's not clear where/how to use with td-data-table.Undoubted
Also @Powers your code uses td-data-table-row/cell/etc. Question as stated uses td-data-table and ng-template, hence no *ngFor, hence no trackBy.Undoubted
So, is there another Angular 2+ control that handles this properly - this is ok, but the correct syntax for td-data-table is not ok?Henriques
Not sure what you mean @eric99. I'd be very happy to use td-data-table-row/cell/etc. as demonstrated above, but the functionality isn't equivalent. For example, no sticky headers, no ng-templates so you can't mix & match columns into tables with hacking *ngIf, etc.. But td-data-table seems to fall flat when used w/ vertical scrolling, hence the purpose of the original question.Undoubted
P
2

I suggest you switch to angular material and bind it to an observable data source.

https://material.angular.io/components/table/overview#observable-stream-of-data-arrays

When the datasource(Observable) is updated with new data it will update the DOM and there will be no need to follow scroll events.

In case you are worried about the number on items listed on a page supports pagination in a simple way; . https://material.angular.io/components/table/overview#pagination

SideNote: Switch to angular material as their components are well documented, with examples and sample codes Let me know in case you get stuck.

Paucker answered 18/4, 2018 at 14:46 Comment(4)
the angular material data table is very primitive, most egregiously it will not stick table headers in place, they scroll when you scroll the data! That is one of the main reasons I need td-data-table which can stick the table headers in place while the data is scrolled.Undoubted
The sticky header issue was solved. github.com/angular/material2/issues/9143 Use suggested CSS .mat-header-row { position: sticky; position: -webkit-sticky; top: 0; background-color: inherit; }Paucker
thanks I didn't realize that, that may be a direction forward. Still I have dozens of tables I'd like to avoid rewriting if possible, question is about td-data-table not mat-table.Undoubted
In end I went with @stevebiko's approach and switched to native angular material table. works ok, still supports templates, sticky workaround works well. I ditched td-data-table, never got it working to satisfaction. sorry happened too late for the bounty.Undoubted
M
1

Pushing data to the same array variable which is being used by *ngFor wont re-render html.

Simple example: https://angular-frjdnf.stackblitz.io

Edit:

I have made a sample project with covalent table using *ngFor with angular updating only the additional row and not re-rendering the whole table.

Link to Stackblitz.io sample project.

HTML

<button (click)="addData()">Add Data</button>
<table td-data-table>
    <thead>
    <tr td-data-table-column-row>
      <td td-data-table-column *ngFor="let column of columns">
        {{column.label}}
      </td>
    </tr>
    </thead>
    <tbody>
    <tr td-data-table-row *ngFor="let row of basicData">
      <td td-data-table-cell *ngFor="let column of columns">
        {{ row[column.name] }}
      </td>
    </tr>
    </tbody>
  </table>

TS

basicData has the initial data that was loaded into the table and I am just pushing some dummy data on click of the "Add Data" button to the basicData for the purpose of testing the scroll bar.

import { Component } from '@angular/core';
import { ITdDataTableColumn } from '@covalent/core/data-table';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  columns: ITdDataTableColumn[] = [
    { name: 'first_name',  label: 'First Name' },
    { name: 'last_name', label: 'Last Name' },
    { name: 'gender', label: 'Gender' }
  ];

  count = 0;

  basicData: any[] = [
    {
      "first_name": "Sully",
      "gender": "Male",
      "last_name": "Clutterham"
    },
    ...
  ]

  additionalData: any[] = [
    {
      "first_name": "Sully",
      "gender": "Male",
      "last_name": "Clutterham"
    },
    ...
  ]

  addData(){
    if(this.count >= this.additionalData.length)
      this.count = 0;

    this.basicData.push(this.additionalData[this.count]);
    this.count++;
  }

}

Hope this helps.

Myotonia answered 20/4, 2018 at 8:57 Comment(4)
there is no *ngFor, see Basic Data Table code at (teradata.github.io/covalent/#/components/data-table).Undoubted
From the given demo page, If you check "Custom Data Table" Demo code, they have got <table td-data-table>...<tr td-data-table-row *ngFor="let row of basicData">...</tr>, and they are passing the data from Json file to basicData. I guess you are following the "Basic Data Table" Demo structure, where you need to pass only the data through the input decorator [data] of <td-data-table> directive. Could you try to use the "Data Table (Atomic) Components" type (as mentioned at the end of page) ?Myotonia
From "Custom Data Table" Code section, they are using the "Data Table Components", where they are using custom attributes like td-data-table-row, td-data-table-cell. <table td-data-table> <thead> ... </thead> <tbody> <tr td-data-table-row *ngFor="let row of basicData"> <td td-data-table-cell *ngFor="let column of columns"> {{ row[column.name] }} </td> </tr> </tbody> </table>Myotonia
I'd like to use atomic components but then I lose all the features of td-data-table e.g. automatic sticky headers. Question is specifically for td-data-table with ng-template children, not td-data-table, td-data-table is what I need to get working.Undoubted
I
0

It looks like when you get more data, you are refreshing whole list(array used in *ngFor) so angular is re-drawing full table again. Try to push only difference data to that array.

Internship answered 17/4, 2018 at 14:41 Comment(2)
1.-Use ViewChild to get the "table". 2.-On ngDestroy (or before refresh the table) save the position of naviteElement scroll in a service, localhost or variable.3.- when update the table use this saved variableNear
@Near that's what the sample code I pasted does. As mentioned it creates a lot of flashing & redraw, especially in Firefox.Undoubted

© 2022 - 2024 — McMap. All rights reserved.