161
votes

I am using a mat-table to list the content of the users chosen languages. They can also add new languages using dialog panel. After they added a language and returned back. I want my datasource to refresh to show the changes they made.

I initialize the datastore by getting user data from a service and passing that into a datasource in the refresh method.

Language.component.ts

import { Component, OnInit } from '@angular/core';
import { LanguageModel, LANGUAGE_DATA } from '../../../../models/language.model';
import { LanguageAddComponent } from './language-add/language-add.component';
import { AuthService } from '../../../../services/auth.service';
import { LanguageDataSource } from './language-data-source';
import { LevelbarComponent } from '../../../../directives/levelbar/levelbar.component';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { MatSnackBar, MatDialog } from '@angular/material';

@Component({
  selector: 'app-language',
  templateUrl: './language.component.html',
  styleUrls: ['./language.component.scss']
})
export class LanguageComponent implements OnInit {

  displayedColumns = ['name', 'native', 'code', 'level'];
  teachDS: any;
  user: any;

  constructor(private authService: AuthService, private dialog: MatDialog) { }

  ngOnInit() {
    this.refresh();
  }

  add() {
    this.dialog.open(LanguageAddComponent, {
      data: { user: this.user },
    }).afterClosed().subscribe(result => {
      this.refresh();
    });
  }

  refresh() {
    this.authService.getAuthenticatedUser().subscribe((res) => {
      this.user = res;
      this.teachDS = new LanguageDataSource(this.user.profile.languages.teach);   
    });
  }
}

language-data-source.ts

import {MatPaginator, MatSort} from '@angular/material';
import {DataSource} from '@angular/cdk/collections';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/map';

export class LanguageDataSource extends DataSource<any> {

  constructor(private languages) {
    super();
  }

  connect(): Observable<any> {
    return Observable.of(this.languages);
  }

  disconnect() {
    // No-op
  }

}

So I have tried to call a refresh method where I get the user from the backend again and then I reinitialize the data source. However this does not work, no changes are occurring.

26
If you want to trigger the change "from the data source", please have a look at stackoverflow.com/questions/47897694/…Yennefer
Event emmiters can be use in this case. stackoverflow.com/a/44858648/8300620Rohit Parte

26 Answers

71
votes

Trigger a change detection by using ChangeDetectorRef in the refresh() method just after receiving the new data, inject ChangeDetectorRef in the constructor and use detectChanges like this:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { LanguageModel, LANGUAGE_DATA } from '../../../../models/language.model';
import { LanguageAddComponent } from './language-add/language-add.component';
import { AuthService } from '../../../../services/auth.service';
import { LanguageDataSource } from './language-data-source';
import { LevelbarComponent } from '../../../../directives/levelbar/levelbar.component';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { MatSnackBar, MatDialog } from '@angular/material';

@Component({
  selector: 'app-language',
  templateUrl: './language.component.html',
  styleUrls: ['./language.component.scss']
})
export class LanguageComponent implements OnInit {
  displayedColumns = ['name', 'native', 'code', 'level'];
  teachDS: any;

  user: any;

  constructor(private authService: AuthService, private dialog: MatDialog,
              private changeDetectorRefs: ChangeDetectorRef) { }

  ngOnInit() {
    this.refresh();
  }

  add() {
    this.dialog.open(LanguageAddComponent, {
      data: { user: this.user },
    }).afterClosed().subscribe(result => {
      this.refresh();
    });
  }

  refresh() {
    this.authService.getAuthenticatedUser().subscribe((res) => {
      this.user = res;
      this.teachDS = new LanguageDataSource(this.user.profile.languages.teach);
      this.changeDetectorRefs.detectChanges();
    });
  }
}
120
votes

I don't know if the ChangeDetectorRef was required when the question was created, but now this is enough:

import { MatTableDataSource } from '@angular/material/table';

// ...

dataSource = new MatTableDataSource<MyDataType>();

refresh() {
  this.myService.doSomething().subscribe((data: MyDataType[]) => {
    this.dataSource.data = data;
  }
}

Example:
StackBlitz

60
votes

So for me, nobody gave the good answer to the problem that i met which is almost the same than @Kay. For me it's about sorting, sorting table does not occur changes in the mat. I purpose this answer since it's the only topic that i find by searching google. I'm using Angular 6.

As said here:

Since the table optimizes for performance, it will not automatically check for changes to the data array. Instead, when objects are added, removed, or moved on the data array, you can trigger an update to the table's rendered rows by calling its renderRows() method.

So you just have to call renderRows() in your refresh() method to make your changes appears.

See here for integration.

23
votes

Since you are using MatPaginator, you just need to do any change to paginator, this triggers data reload.

Simple trick:

this.paginator._changePageSize(this.paginator.pageSize); 

This updates the page size to the current page size, so basically nothing changes, except the private _emitPageEvent() function is called too, triggeing table reload.

20
votes

In Angular 9, the secret is this.dataSource.data = this.dataSource.data;

Example:

import { MatTableDataSource } from '@angular/material/table';

dataSource: MatTableDataSource<MyObject>;

refresh(): void {
    this.applySomeModif();
    // Do what you want with dataSource

    this.dataSource.data = this.dataSource.data;
}

applySomeModif(): void {
    // add some data
    this.dataSource.data.push(new MyObject());
    // delete index number 4
    this.dataSource.data.splice(4, 0);
}
10
votes

You can just use the datasource connect function

this.datasource.connect().next(data);

like so. 'data' being the new values for the datatable

10
votes
this.dataSource = new MatTableDataSource<Element>(this.elements);

Add this line below your action of add or delete the particular row.

refresh() {
  this.authService.getAuthenticatedUser().subscribe((res) => {
    this.user = new MatTableDataSource<Element>(res);   
  });
}
9
votes

Best way to do this is by adding an additional observable to your Datasource implementation.

In the connect method you should already be using Observable.merge to subscribe to an array of observables that include the paginator.page, sort.sortChange, etc. You can add a new subject to this and call next on it when you need to cause a refresh.

something like this:

export class LanguageDataSource extends DataSource<any> {

    recordChange$ = new Subject();

    constructor(private languages) {
      super();
    }

    connect(): Observable<any> {

      const changes = [
        this.recordChange$
      ];

      return Observable.merge(...changes)
        .switchMap(() => return Observable.of(this.languages));
    }

    disconnect() {
      // No-op
    }
}

And then you can call recordChange$.next() to initiate a refresh.

Naturally I would wrap the call in a refresh() method and call it off of the datasource instance w/in the component, and other proper techniques.

8
votes

Well, I ran into a similar problem where I added somthing to the data source and it's not reloading.

The easiest way I found whas simply to reassigning the data

let dataSource = ['a','b','c']
dataSource.push('d')
let cloned = dataSource.slice()
// OR IN ES6 // let cloned = [...dataSource]
dataSource = cloned
7
votes

You can also use renderRows() method.

@ViewChild(MatTable, {static: false}) table : MatTable // initialize

then this.table.renderRows();

for reference check this out -: https://www.freakyjolly.com/angular-7-8-edit-add-delete-rows-in-material-table-with-using-dialogs-inline-row-operation/

4
votes

You can easily update the data of the table using "concat":

for example:

language.component.ts

teachDS: any[] = [];

language.component.html

<table mat-table [dataSource]="teachDS" class="list">

And, when you update the data (language.component.ts):

addItem() {
    // newItem is the object added to the list using a form or other way
    this.teachDS = this.teachDS.concat([newItem]);
 }

When you're using "concat" angular detect the changes of the object (this.teachDS) and you don't need to use another thing.

PD: It's work for me in angular 6 and 7, I didn't try another version.

4
votes

I achieved a good solution using two resources:

refreshing both dataSource and paginator:

this.dataSource.data = this.users;
this.dataSource.connect().next(this.users);
this.paginator._changePageSize(this.paginator.pageSize);

where for example dataSource is defined here:

    users: User[];
    ...
    dataSource = new MatTableDataSource(this.users);
    ...
    this.dataSource.paginator = this.paginator;
    ...
4
votes

There are two ways to do it because Angular Material is inconsistent, and this is very poorly documented. Angular material table won't update when a new row will arrive. Surprisingly it is told it is because performance issues. But it looks more like a by design issue, they can not change. It should be expected for the table to update when new row occurs. If this behavior should not be enabled by default there should be a switch to switch it off.

Anyways, we can not change Angular Material. But we can basically use a very poorly documented method to do it:

One - if you use an array directly as a source:

call table.renderRows()

where table is ViewChild of the mat-table

Second - if you use sorting and other features

table.renderRows() surprisingly won't work. Because mat-table is inconsistent here. You need to use a hack to tell the source changed. You do it with this method:

this.dataSource.data = yourDataSource;

where dataSource is MatTableDataSource wrapper used for sorting and other features.

4
votes

After adding a new data row, I refreshed my material table by updating dataSource without using its instance.

table in HTML:

<table mat-table #table [dataSource]="myDataArray">

addUser() in component.ts:

public USER_DATA: user[] = [];


public newUser = {userName: "ABC", email: "[email protected]"};
public myDataArray: any;


addUser() {
    const newUsersArray = this.USER_DATA;
    newUsersArray.push(this.newUser);
    this.myDataArray = [...newUsersArray];//refresh the dataSource
    
  }
2
votes

This worked for me:

refreshTableSorce() {
    this.dataSource = new MatTableDataSource<Element>(this.newSource);
}
2
votes

I had tried ChangeDetectorRef, Subject and BehaviourSubject but what works for me

dataSource = [];
this.dataSource = [];
 setTimeout(() =>{
     this.dataSource = this.tableData[data];
 },200)
1
votes
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';

export class LanguageComponent implemnts OnInit {
  displayedColumns = ['name', 'native', 'code', 'leavel'];
  user: any;
  private update = new Subject<void>();
  update$ = this.update.asObservable();

  constructor(private authService: AuthService, private dialog: MatDialog) {}

   ngOnInit() {
     this.update$.subscribe(() => { this.refresh()});
   }

   setUpdate() {
     this.update.next();
   }

   add() {
     this.dialog.open(LanguageAddComponent, {
     data: { user: this.user },
   }).afterClosed().subscribe(result => {
     this.setUpdate();
   });
 }

 refresh() {
   this.authService.getAuthenticatedUser().subscribe((res) => {
     this.user = res;
     this.teachDS = new LanguageDataSource(this.user.profile.languages.teach);   
    });
  }
}
1
votes

in my case (Angular 6+), I inherited from MatTableDataSource to create MyDataSource. Without calling after this.data = someArray

this.entitiesSubject.next(this.data as T[])

data where not displayed

class MyDataSource

export class MyDataSource<T extends WhateverYouWant> extends MatTableDataSource<T> {

    private entitiesSubject = new BehaviorSubject<T[]>([]);


    loadDataSourceData(someArray: T[]){
        this.data = someArray //whenever it comes from an API asyncronously or not
        this.entitiesSubject.next(this.data as T[])// Otherwise data not displayed
    }

    public connect(): BehaviorSubject<T[]> {
        return this.entitiesSubject
    }

}//end Class 
1
votes

In Angular 10, this is what works for me: In the HTML:

<mat-table [dataSource]="myArray">

In the component TS:

    myArray: MyObject[] = [];

 addObjectToTable(object:MyObject): void {
      //TO PREVENT DUPLICATED OBJECTS
    if (object&& !this.myArray.includes(object)) {
      this.myArray.push(object);
      // TO FORCE DATA-TABLE's DATASOURCE TO REFRESH
      this.myArray= [...this.myArray];
    }
  }
1
votes

I have tried some of the previous suggestions. It does update the table but I have some concerns:

  1. Updating dataSource.data with its clone. e.g.
this.dataSource.data = [...this.dataSource.data];

If the data is large, this will reallocate lot of memory. Moreover, MatTable thinks that everything is new inside the table, so it may cause performance issue. I found my table flickers where my table has about 300 rows.

  1. Calling paginator._changePageSize. e.g.
this.paginator._changePageSize(this.paginator.pageSize);

It will emit page event. If you have already had some handling for the page event. You may find it weird because the event may be fired more than once. And there can be a risk that if somehow the event will trigger _changePageSize() indirectly, it will cause infinite loop...

I suggest another solution here. If your table is not relying on dataSource's filter field.

  1. You may update the filter field to trigger table refresh:
this.dataSource.filter = ' '; // Note that it is a space, not empty string

By doing so, the table will perform filtering and thus updating the UI of the table. But it requires having your own dataSource.filterPredicate() to handling your filtering logic.

0
votes

I think the MatTableDataSource object is some way linked with the data array that you pass to MatTableDataSource constructor.

For instance:

dataTable: string[];
tableDS: MatTableDataSource<string>;

ngOnInit(){
   // here your pass dataTable to the dataSource
   this.tableDS = new MatTableDataSource(this.dataTable); 
}

So, when you have to change data; change on the original list dataTable and then reflect the change on the table by call _updateChangeSubscription() method on tableDS.

For instance:

this.dataTable.push('testing');
this.tableDS._updateChangeSubscription();

That's work with me through Angular 6.

0
votes

This is working for me:

dataSource = new MatTableDataSource<Dict>([]);
    public search() {
        let url = `${Constants.API.COMMON}/dicts?page=${this.page.number}&` + 
        (this.name == '' ? '' : `name_like=${this.name}`);
    this._http.get<Dict>(url).subscribe((data)=> {
    // this.dataSource = data['_embedded'].dicts;
    this.dataSource.data =  data['_embedded'].dicts;
    this.page = data['page'];
    this.resetSelection();
  });
}

So you should declare your datasource instance as MatTableDataSource

0
votes

I did some more research and found this place to give me what I needed - feels clean and relates to update data when refreshed from server: https://blog.angular-university.io/angular-material-data-table/

Most credits to the page above. Below is a sample of how a mat-selector can be used to update a mat-table bound to a datasource on change of selection. I am using Angular 7. Sorry for being extensive, trying to be complete but concise - I have ripped out as many non-needed parts as possible. With this hoping to help someone else getting forward faster!

organization.model.ts:

export class Organization {
    id: number;
    name: String;
}

organization.service.ts:

import { Observable, empty } from 'rxjs';
import { of } from 'rxjs';

import { Organization } from './organization.model';

export class OrganizationService {
  getConstantOrganizations(filter: String): Observable<Organization[]> {
    if (filter === "All") {
      let Organizations: Organization[] = [
        { id: 1234, name: 'Some data' }
      ];
      return of(Organizations);
     } else {
       let Organizations: Organization[] = [
         { id: 5678, name: 'Some other data' }
       ];
     return of(Organizations);
  }

  // ...just a sample, other filterings would go here - and of course data instead fetched from server.
}

organizationdatasource.model.ts:

import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { catchError, finalize } from "rxjs/operators";

import { OrganizationService } from './organization.service';
import { Organization } from './organization.model';

export class OrganizationDataSource extends DataSource<Organization> {
  private organizationsSubject = new BehaviorSubject<Organization[]>([]);

  private loadingSubject = new BehaviorSubject<boolean>(false);

  public loading$ = this.loadingSubject.asObservable();

  constructor(private organizationService: OrganizationService, ) {
    super();
  }

  loadOrganizations(filter: String) {
    this.loadingSubject.next(true);

    return this.organizationService.getOrganizations(filter).pipe(
      catchError(() => of([])),
      finalize(() => this.loadingSubject.next(false))
    ).subscribe(organization => this.organizationsSubject.next(organization));
  }

  connect(collectionViewer: CollectionViewer): Observable<Organization[]> {
    return this.organizationsSubject.asObservable();
  }

  disconnect(collectionViewer: CollectionViewer): void {
    this.organizationsSubject.complete();
    this.loadingSubject.complete();
  }
}

organizations.component.html:

<div class="spinner-container" *ngIf="organizationDataSource.loading$ | async">
    <mat-spinner></mat-spinner>
</div>

<div>
  <form [formGroup]="formGroup">
    <mat-form-field fxAuto>
      <div fxLayout="row">
        <mat-select formControlName="organizationSelectionControl" (selectionChange)="updateOrganizationSelection()">
          <mat-option *ngFor="let organizationSelectionAlternative of organizationSelectionAlternatives"
            [value]="organizationSelectionAlternative">
            {{organizationSelectionAlternative.name}}
          </mat-option>
        </mat-select>
      </div>
    </mat-form-field>
  </form>
</div>

<mat-table fxLayout="column" [dataSource]="organizationDataSource">
  <ng-container matColumnDef="name">
    <mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
    <mat-cell *matCellDef="let organization">{{organization.name}}</mat-cell>
  </ng-container>

  <ng-container matColumnDef="number">
    <mat-header-cell *matHeaderCellDef>Number</mat-header-cell>
    <mat-cell *matCellDef="let organization">{{organization.number}}</mat-cell>
  </ng-container>

  <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
  <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>

organizations.component.scss:

.spinner-container {
    height: 360px;
    width: 390px;
    position: fixed;
}

organization.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs';

import { OrganizationService } from './organization.service';
import { Organization } from './organization.model';
import { OrganizationDataSource } from './organizationdatasource.model';

@Component({
  selector: 'organizations',
  templateUrl: './organizations.component.html',
  styleUrls: ['./organizations.component.scss']
})
export class OrganizationsComponent implements OnInit {
  public displayedColumns: string[];
  public organizationDataSource: OrganizationDataSource;
  public formGroup: FormGroup;

  public organizationSelectionAlternatives = [{
    id: 1,
    name: 'All'
  }, {
    id: 2,
    name: 'With organization update requests'
  }, {
    id: 3,
    name: 'With contact update requests'
  }, {
    id: 4,
    name: 'With order requests'
  }]

  constructor(
    private formBuilder: FormBuilder,
    private organizationService: OrganizationService) { }

  ngOnInit() {
    this.formGroup = this.formBuilder.group({
      'organizationSelectionControl': []
    })

    const toSelect = this.organizationSelectionAlternatives.find(c => c.id == 1);
    this.formGroup.get('organizationSelectionControl').setValue(toSelect);

    this.organizationDataSource = new OrganizationDataSource(this.organizationService);
    this.displayedColumns = ['name', 'number' ];
    this.updateOrganizationSelection();
  }

  updateOrganizationSelection() {
    this.organizationDataSource.loadOrganizations(this.formGroup.get('organizationSelectionControl').value.name);
  }
}
0
votes

After reading Material Table not updating post data update #11638 Bug Report I found the best (read, the easiest solution) was as suggested by the final commentor 'shhdharmen' with a suggestion to use an EventEmitter.

This involves a few simple changes to the generated datasource class

ie) add a new private variable to your datasource class

import { EventEmitter } from '@angular/core';
...
private tableDataUpdated = new EventEmitter<any>();

and where I push new data to the internal array (this.data), I emit an event.

public addRow(row:myRowInterface) {
    this.data.push(row);
    this.tableDataUpdated.emit();
}

and finally, change the 'dataMutation' array in the 'connect' method - as follows

const dataMutations = [
    this.tableDataUpdated,
    this.paginator.page,
    this.sort.sortChange
];
0
votes

// this is the dataSource
this.guests = [];

this.guests.push({id: 1, name: 'Ricardo'});

// refresh the dataSource this.guests = Array.from(this.guest);

0
votes

Try this may be it can help you

Start with function which load users.

loadUser() {
    this.userService.getListOfUsers().subscribe()
        (response: any) => {
            this.dataSource = response
            this.dataSource.paginator = this.paginator;
        }

}

The define refresh function, which will be used to refresh table after delete user.

refresh() {
    this.loadUser();
    this.dataSource.data = [...this.dataSource.data];
    this.dataSource.paginator = this.paginator;
}

Now you can call refresh() function after you finish delete user process, like below.

deleteUser() {
    ...... 
    this.refresh()
}