Angular - Material Table, is it possible to update rows without entire table refresh?

besthiroeu picture besthiroeu · Nov 22, 2017 · Viewed 50.8k times · Source

After few weeks of Googling and only one Stackoverflown question so far I finally managed to build up my Angular CRUD App using Material Table Component. It shows data from backend (JSON) and for CRUD operations I'm using dialogs like one shown on picture (this is edit, sorry for Croatian). Dialogs might not be the best way to go, inline editing might be better. But still, for adding new item you need something like dialog.

enter image description here

Last thing I'm stuck with is how to update fields in table accordingly. So when you press 'Save' on dialog, data gets updated in backend (in MySQL table) but not in fronted. For the time being I have ugly workaround for this, every time when you do an update, it refreshes whole table as well.

Anyway here's code:

Table component:

export class BazaComponent implements OnInit {
  ....
  constructor(public httpClient: HttpClient, public dialog: MatDialog) {
  }

  ngOnInit() {
    this.loadData();
  }

  // TODO: Simplfy this...
  addNew(ident: number, naziv: string, mt: number, kutija: number,
         komada: number, jm: string, orginal: number, lokacija: number, napomena: string) {
    console.log('add new clicked');
    const dialogRef = this.dialog.open(AddDialogComponent, {
      data: {ident: ident, naziv: naziv, mt: mt, kutija: kutija,
        komada: komada, jm: jm, orginal: orginal, lokacija: lokacija, napomena: napomena }
    });

    dialogRef.afterClosed().subscribe(result => {
      console.log(result);
      if (result === 1) {
        this.loadData();  // --> This is a temp workaround, every time when I do CRUD operation just redraw whole thing again
      }
    });
  }

  startEdit(id: number, ident: number, naziv: string, mt: number, kutija: number,
            komada: number, jm: string, orginal: number, lokacija: number, napomena: string) {

    const dialogRef = this.dialog.open(EditDialogComponent, {
      data: {id: id, ident: ident, naziv: naziv, mt: mt, kutija: kutija,
        komada: komada, jm: jm, orginal: orginal, lokacija: lokacija, napomena: napomena}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        this.loadData(); // --> This is a temp workaround, every time when I do CRUD operation just redraw whole thing again
      }
    });
  }

  deleteItem(id: number, ident: number, naziv: string, mt: number) {
    const dialogRef = this.dialog.open(DeleteDialogComponent, {
      data: {id: id, ident: ident, naziv: naziv, mt: mt}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        this.loadData();
      }
    });
  }


  public loadData() {
    this.exampleDatabase = new DataService(this.httpClient);
    this.dataSource = new ExampleDataSource(this.exampleDatabase, this.paginator, this.sort);
    Observable.fromEvent(this.filter.nativeElement, 'keyup')
      .debounceTime(150)
      .distinctUntilChanged()
      .subscribe(() => {
        if (!this.dataSource) {
          return;
        }
        this.dataSource.filter = this.filter.nativeElement.value;
      });
  }
}


export class ExampleDataSource extends DataSource<Baza> {
  _filterChange = new BehaviorSubject('');

  get filter(): string {
    return this._filterChange.value;
  }

  set filter(filter: string) {
    this._filterChange.next(filter);
  }

  filteredData: Baza[] = [];
  renderedData: Baza[] = [];

  constructor(private _exampleDatabase: DataService,
              private _paginator: MatPaginator,
              private _sort: MatSort) {
    super();
    // Reset to the first page when the user changes the filter.
    this._filterChange.subscribe(() => this._paginator.pageIndex = 0);
  }

  /** Connect function called by the table to retrieve one stream containing the data to render. */
  connect(): Observable<Baza[]> {
    // Listen for any changes in the base data, sorting, filtering, or pagination
    const displayDataChanges = [
      this._exampleDatabase.dataChange,
      this._sort.sortChange,
      this._filterChange,
      this._paginator.page,
    ];

    this._exampleDatabase.getAllItems();

    return Observable.merge(...displayDataChanges).map(() => {
      // Filter data
      this.filteredData = this._exampleDatabase.data.slice().filter((item: Baza) => {
        const searchStr = (item.ident + item.naziv + item.mt + item.lokacija + item.napomena).toLowerCase();
        return searchStr.indexOf(this.filter.toLowerCase()) !== -1;
      });

      // Sort filtered data
      const sortedData = this.sortData(this.filteredData.slice());

      // Grab the page's slice of the filtered sorted data.
      const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
      this.renderedData = sortedData.splice(startIndex, this._paginator.pageSize);
      return this.renderedData;
    });
  }

  disconnect() {
  }

  /** Returns a sorted copy of the database data. */
  sortData(data: Baza[]): Baza[] {
  ... sort stuff
}

Here's DataService where I guess I should do field updates:

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
import { Baza } from '../models/kanban.baza';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

    @Injectable()
    export class DataService {
      private readonly API_URL = 'http://localhost/api/'

      /** Stream that emits whenever the data has been modified. */
      dataChange: BehaviorSubject<Baza[]> = new BehaviorSubject<Baza[]>([]);

      constructor(private httpClient: HttpClient) {
      }

      get data(): Baza[] {
        return this.dataChange.value;
      }

      getAllItems(): void {
        this.httpClient.get<Baza[]>(this.API_URL).subscribe(data => {
          this.dataChange.next(data['items']);
        });
      }

    addItem(baza: Baza): void {
      this.httpClient.post(this.API_URL, Baza).subscribe(data => {
          //THIS WAS MY BEST TRY BUT IT DOESN'T WORK :(
          const copiedData = this.data.slice();
          copiedData.push(baza);
          console.log(copiedData);
          this.dataChange.next(copiedData);
      });
    }


      updateItem(baza: Baza): void {
        this.httpClient.put(this.API_URL + baza.id, baza).subscribe();
      }

      deleteItem(id: number): void {
        this.httpClient.delete(this.API_URL + id, {headers: new HttpHeaders().set('Access-Control-Allow-Origin', '*')} ).subscribe();
    }
}

UPDATE 27.11.2017:

Okay, I've finally figured out how to trigger new row addition. I had to call dataChange.value inside table component. Once you load it with some data new row will appear instantaniously.

const data = {id: 208, ident: 233, naziv: 'test', mt: 291, komada: 2, jm: 'a', orginal: 100, lokacija: 3, napomena: 'pls work'};
this.exampleDatabase.dataChange.value.push(data);

Same thing in DataService won't work:

this.dataChange.value.push(data); 

Plunker is here:

https://plnkr.co/edit/IWCVsBRl54F7ylGNIJJ3?p=info

EDIT 28.11.2017:

Now only thing left is building logic for add, edit and delete. For add is easy, it's just `value.push(data)'. Thanks for help everyone.

Answer

besthiroeu picture besthiroeu · Nov 28, 2017

Took me some time but I finally got everything working. Your answers and different approaches helped aswell. So, here's my CRUD implementation if anyone gets in trouble with this:

https://github.com/marinantonio/angular-mat-table-crud

Screenshot: Alt Text

Or you can check project demo: https://marinantonio.github.io/angular-mat-table-crud/

Key parts are in table.ts file:

....
addNew(issue: Issue) {
    const dialogRef = this.dialog.open(AddDialogComponent, {
      data: {issue: issue }
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        this.exampleDatabase.dataChange.value.push(this.dataService.getDialogData());
        this.refreshTable();
      }
    });
  }

  startEdit(i: number, id: number, title: string, state: string, url: string, created_at: string, updated_at: string) {
    this.index = i;
    this.id2 = id;
    console.log(this.index);
    const dialogRef = this.dialog.open(EditDialogComponent, {
      data: {id: id, title: title, state: state, url: url, created_at: created_at, updated_at: updated_at}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        // Part where we do frontend update, first you need to find record using id
        const foundIndex = this.exampleDatabase.dataChange.value.findIndex(x => x.id === this.id2);
        // Then you update that record using dialogData
        this.exampleDatabase.dataChange.value[foundIndex] = this.dataService.getDialogData();
        // And lastly refresh table
        this.refreshTable();
      }
    });
  }

  deleteItem(i: number, id: number, title: string, state: string, url: string) {
    this.index = i;
    this.id2 = id;
    const dialogRef = this.dialog.open(DeleteDialogComponent, {
      data: {id: id, title: title, state: state, url: url}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        const foundIndex = this.exampleDatabase.dataChange.value.findIndex(x => x.id === this.id2);
        this.exampleDatabase.dataChange.value.splice(foundIndex, 1);
        this.refreshTable();
      }
    });
  }


  private refreshTable() {
    // If there's no data in filter we do update using pagination, next page or previous page
    if (this.dataSource._filterChange.getValue() === '') {
      if (this.dataSource._paginator.pageIndex === 0) {
        this.dataSource._paginator.nextPage();
        this.dataSource._paginator.previousPage();
      } else {
        this.dataSource._paginator.previousPage();
        this.dataSource._paginator.nextPage();
      }
      // If there's something in filter, we reset it to 0 and then put back old value
    } else {
      this.dataSource.filter = '';
      this.dataSource.filter = this.filter.nativeElement.value;
    }
}
....