Components: Material Table not updating post data update

Created on 3 Jun 2018  路  11Comments  路  Source: angular/components

Bug, feature request, or proposal:

The Material Table used in my application is not updating after data is fetched from the backend. Initially, the DataSource has no data. Using console logs I have determined that connect is called on the DataSource 'before' 'the subscribe to the graphql query returns (initiated in the constructor of the DataSource). Hence the initial view of the table is empty. But when the data arrives, the data field in the DataSource is updated, and since the connect method provides the data as an observable, the table should update/refresh. This is not happening. Interestingly, the pagination support in the mat-table (i.e., mat-paginator) is updating to show '1-4 of 4' - which is correct, as 4 items are returned from the backend in my test environment, but the table does not show any rows.

What is the expected behavior?

Table should update and show the rows of data that have been provided to it as an observable.

What is the current behavior?

Table does not update with dynamic backend data. Works fine with static data built into the angular app.

What are the steps to reproduce?

Providing a StackBlitz reproduction is the best way to share your issue.

StackBlitz starter: https://goo.gl/wwnhMV

Trying to figure this out, and will try to update ASAP.

What is the use-case or motivation for changing an existing behavior?

I think behavior is broken

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Angular CLI: 6.0.3
Node: 9.11.1
OS: linux x64
Angular: 6.0.2
... animations, cdk, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

Package Version

@angular-devkit/architect 0.6.3
@angular-devkit/build-angular 0.6.1
@angular-devkit/build-optimizer 0.6.1
@angular-devkit/core 0.0.29
@angular-devkit/schematics 0.6.3
@angular/cli 6.0.3
@angular/material 6.2.0
@ngtools/webpack 6.0.1
@schematics/angular 0.6.3
@schematics/update 0.6.3
rxjs 6.1.0
typescript 2.7.2
webpack 4.6.0

Have also tried with @angular/[email protected]

Is there anything else we should know?

Some code snippets:
=====DataSource=====================

export class FleetTableDataSource extends DataSource<FleetTableItem> {
  data: FleetTableItem[] = [];
  loading: boolean = true;
  systems: any[] = [];

  constructor(private paginator: MatPaginator, private sort: MatSort, private fleetDataSvc: FleetDataService) {
    super();
    console.log("constructorr " + new Date())
    this.fleetDataSvc.getAllSystems().subscribe(({ data }) => {
      this.loading = data.loading;
      this.systems = data.allSystems.edges;
      for (let system of this.systems) {
        var f = <FleetTableItem>{};
        f.x = system.node.x;
        f.a = system.node.A.a;
        f.b = system.node.A.b;
        f.y = system.node.y;
        f.z = system.node.B.d + "/" + system.node.B.e;
        this.data.push(f);
      }
      console.log("Data has arrived " + new Date());
    });

  connect(): Observable<FleetTableItem[]> {
    console.log("connect called " + new Date());
    // Combine everything that affects the rendered data into one update
    // stream for the data-table to consume.
    const dataMutations = [
      observableOf(this.data),
      this.paginator.page,
      this.sort.sortChange
    ];

    // Set the paginators length
    this.paginator.length = this.data.length;

    return merge(...dataMutations).pipe(map(() => {
      return this.getPagedData(this.getSortedData([...this.data]));
    }));
  }

======= FleetDataService ===========

const allSystemsSummary = gql`
query Systems_Summary {
  allSystems {
    edges {
      node {
        A {
          a
          b
        }
        id
        x
        y
        B {
          d
          e
        }
      }
    }
  }
}`;

@Injectable()
export class FleetDataService {
    constructor(private apollo: Apollo) {
    }
    getAllSystems(): Observable<ApolloQueryResult<any>> {
        return this.apollo.watchQuery<any>({
            query: allSystemsSummary
          }).valueChanges;
    }
}

Most helpful comment

Internally what happens is this. The table called connect to retrieve a stream from the data source. When the data source emits an array on that stream, the table re-renders its rows.

In your case, you are adding to the array, but not emitting anything new so the table is not signaled to re-render.

Here's an example of a working demo. The change I made was adding a data stream that emits your data. When new data is added, we take a copy of the old data, push a new value, and emit that new array on the stream.

https://stackblitz.com/edit/angular-material2-issue-yzhsml?file=app/data-table-datasource.ts

All 11 comments

Here is the StackBlitz reproduction:

https://stackblitz.com/edit/angular-material2-issue-o2dqg9

It consists of the standard mat-table generated code fused directly into the app component. The EXAMPLE_DATA loads initially, with 21 rows (1-20) - All that is standard. Then, I have created a click handler for the div wrapping the mat-table:

<div class="mat-elevation-z8" (click)="addData()">
  <mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">

The addData() handler calls a separate function called addData() on the DataSource, which adds one new entry to the data field (id: 21, name: Uranium). This simulates the late arriving data.

Run the example and see the original table and paginator load. Then, when you click anywhere on the table (it causes the click handler addData() to fire.....Eventually, you will see the paginator keep increasing at the bottom for every click. console logs will show that DataSource.data has grown by one for each click, which aligns with the paginator increase. But no new rows are added. So the mat-paginator is successfully subscribing to the DataSource.data Observable, but not the mat-table.

Now, if you were to sort one of the columns, the missing data in the table re-appears. Obviously we are looking at a bug here.

Any thoughts/updates?

Please use our issue queue for bugs and features. Unfortunately we don't always have the bandwidth to dive into investigations for troubleshooting. You may have better help if you ask our community is places like StackOverflow or Gitter

@andrewseguin But this is a bug. Did you try out the stackblitz reproduction above? I spent quite a bit of time on it to prove my point.

Internally what happens is this. The table called connect to retrieve a stream from the data source. When the data source emits an array on that stream, the table re-renders its rows.

In your case, you are adding to the array, but not emitting anything new so the table is not signaled to re-render.

Here's an example of a working demo. The change I made was adding a data stream that emits your data. When new data is added, we take a copy of the old data, push a new value, and emit that new array on the stream.

https://stackblitz.com/edit/angular-material2-issue-yzhsml?file=app/data-table-datasource.ts

@andrewseguin This is equivalent to hacking to achieve the goal. It is violating the contract of the datasource concept. We expect when the data source is updated somewhere outside of the datasource class scope, the table should automatically update itself without calling any AddNew sort of method in the datasource class. It is also violating the open-close, and inverstion of control principle of OOD. Please think how this can be fixed, it is surely a bug. To try reproduce what I am saying, I just addded new element outside of datasource addData() {
EXAMPLE_DATA.push({id: 21, name: 'methane'});
} within AppComponent.

--New App Component.

import {
Component, ElementRef, OnInit, ViewChild
} from '@angular/core';
import { MatPaginator, MatSort } from '@angular/material';
import { DataTableDataSource, EXAMPLE_DATA } from './data-table-datasource';
import { VERSION } from '@angular/material';
@Component({
selector: 'material-app',
templateUrl: 'app.component.html'
})

export class AppComponent implements OnInit {
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
dataSource: DataTableDataSource;

/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['id', 'name'];
selectedRowIndex: number = -1;

highlight(row){
this.selectedRowIndex = row.id;
}

ngOnInit() {
this.dataSource = new DataTableDataSource(this.paginator, this.sort);
}

addData() {
EXAMPLE_DATA.push({id: 21, name: 'methane'});
}

}

-- New DataSource (Note, I removed AddData method from here.

import { DataSource } from '@angular/cdk/collections';
import { MatPaginator, MatSort } from '@angular/material';
import { map } from 'rxjs/operators';
import { Observable, of as observableOf, merge, BehaviorSubject } from 'rxjs';

// TODO: Replace this with your own data model type
export interface DataTableItem {
name: string;
id: number;
}

// TODO: replace this with real data from your application
export let EXAMPLE_DATA: DataTableItem[] = [
{id: 1, name: 'Hydrogen'},
{id: 2, name: 'Helium'},
{id: 3, name: 'Lithium'},
{id: 4, name: 'Beryllium'},
{id: 5, name: 'Boron'},
{id: 6, name: 'Carbon'},
{id: 7, name: 'Nitrogen'},
{id: 8, name: 'Oxygen'},
{id: 9, name: 'Fluorine'},
{id: 10, name: 'Neon'},
{id: 11, name: 'Sodium'},
{id: 12, name: 'Magnesium'},
{id: 13, name: 'Aluminum'},
{id: 14, name: 'Silicon'},
{id: 15, name: 'Phosphorus'},
{id: 16, name: 'Sulfur'},
{id: 17, name: 'Chlorine'},
{id: 18, name: 'Argon'},
{id: 19, name: 'Potassium'},
{id: 20, name: 'Calcium'},
];

/**

  • Data source for the DataTable view. This class should
  • encapsulate all logic for fetching and manipulating the displayed data
  • (including sorting, pagination, and filtering).
    */
    export class DataTableDataSource extends DataSource {
    dataStream = new BehaviorSubject

set data(v: DataTableItem[]) { this.dataStream.next(v); }
get data(): DataTableItem[] { return this.dataStream.value; }

constructor(private paginator: MatPaginator, private sort: MatSort) {
super();
}

/**

  • Connect this data source to the table. The table will only update when
  • the returned stream emits new items.
  • @returns A stream of the items to be rendered.
    */
    connect(): Observable // Combine everything that affects the rendered data into one update
    // stream for the data-table to consume.
    const dataMutations = [
    this.dataStream,
    this.paginator.page,
    this.sort.sortChange
    ];
// Set the paginators length
this.paginator.length = this.data.length;

return merge(...dataMutations).pipe(map(() => {
  return this.getPagedData(this.getSortedData([...this.data]));
}));

}

/**

  • Called when the table is being destroyed. Use this function, to clean up
  • any open connections or free any held resources that were set up during connect.
    */
    disconnect() {}

/**

  • Paginate the data (client-side). If you're using server-side pagination,
  • this would be replaced by requesting the appropriate data from the server.
    */
    private getPagedData(data: DataTableItem[]) {
    const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
    return data.splice(startIndex, this.paginator.pageSize);
    }

/**

  • Sort the data (client-side). If you're using server-side sorting,
  • this would be replaced by requesting the appropriate data from the server.
    */
    private getSortedData(data: DataTableItem[]) {
    if (!this.sort.active || this.sort.direction === '') {
    return data;
    }
return data.sort((a, b) => {
  const isAsc = this.sort.direction === 'asc';
  switch (this.sort.active) {
    case 'name': return compare(a.name, b.name, isAsc);
    case 'id': return compare(+a.id, +b.id, isAsc);
    default: return 0;
  }
});

}
}

/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
function compare(a, b, isAsc) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}

has this bug fixed yet? i am having same issue here

As per AndrewSeguin this is not a bug. See his comment and stackblitz creation using BehaviorSubjects.....

I achieved the same using EventEmitter.

// component.ts
...
dataMustBeUpdated = new EventEmitter<any>();
// whenever I think data is updated, I will call : this.dataMustBeUpdated.emit();
...
this.dataSource = new DataSource(this.paginator, this.sort, this.dataMustBeUpdated);
...
// datasource.ts
...
constructor(private paginator: MatPaginator,
    private sort: MatSort,
    private dataMustBeUpdated: EventEmitter<any>){...}
...
connect(): Observable<any[]> {
 ...
 const dataMutations = [
       this.paginator.page,
       this.sort.sortChange,
       this.dataMustBeUpdated
     ];
 ...
 return merge(...dataMutations).pipe(...);
 ...
 }
}

I spent the better part of 2 hours combing through things and this is still the best that I found.

In case someone comes through here later and is half brain dead at the end of the day like me, the take-away here is @sensibleinventions 's comment above. Use a BehaviorSubject instead of doing slice()

Cleaned up take-away:

export class DataTableDataSource extends DataSource {
    private _dataStream = new BehaviorSubject<YourDataTableItem[]>( [] );
    public set data(v: YourDataTableItem[]) { this._dataStream.next(v); }
    public get data(): YourDataTableItem[] { return this._dataStream.value; }
  connect(): Observable<DataTableItem[]> {
    // Combine everything that affects the rendered data into one update
    // stream for the data-table to consume.
    const dataMutations = [
        this._dataStream,
        this.paginator.page,
        this.sort.sortChange
    ];
    ...
  }



md5-687c6ab4e0b9db148e18e3c5b46639b0



  ...
} // end of file

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vitaly-t picture vitaly-t  路  3Comments

jelbourn picture jelbourn  路  3Comments

constantinlucian picture constantinlucian  路  3Comments

LoganDupont picture LoganDupont  路  3Comments

julianobrasil picture julianobrasil  路  3Comments