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.
Table should update and show the rows of data that have been provided to it as an observable.
Table does not update with dynamic backend data. Works fine with static data built into the angular app.
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.
I think behavior is broken
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
@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]
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;
}
}
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'},
];
/**
set data(v: DataTableItem[]) { this.dataStream.next(v); }
get data(): DataTableItem[] { return this.dataStream.value; }
constructor(private paginator: MatPaginator, private sort: MatSort) {
super();
}
/**
// Set the paginators length
this.paginator.length = this.data.length;
return merge(...dataMutations).pipe(map(() => {
return this.getPagedData(this.getSortedData([...this.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._
Most helpful comment
Internally what happens is this. The table called
connectto 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