Angular - Material table, is it possible to update rows without updating the entire table?

After weeks of searching on Google and just one question with Stackoverflow, I finally managed to build my Angular CRUD application using the Material Table component . It shows data from the backend (JSON) and for CRUD operations I use dialogs similar to the one shown in the picture (this is editing, sorry for Croatian). Dialogs may not be the best way, inline editing may be better. But still, to add a new element you need something like a dialog.

enter image description here

The last thing I'm stuck with is how to update the fields in the table accordingly. Therefore, when you click "Save" in the dialog box, the data is updated in the backend (in the MySQL table), and not in the external one. I currently have a disgusting workaround for this, every time you do an update, it updates the whole table as well.

Anyway, here is the 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 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 is the DataService where I think 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 11/27/2017:

Ok, I finally figured out how to cause a newline to be added. I had to call dataChange.value inside the table component. As soon as you load it with some data, a new line will appear instantly.

 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); 

The same thing in a DataService will not work:

 this.dataChange.value.push(data); 

The plunker is here:

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

EDIT 11/28/2017:

Now it remains only to build the logic for adding, editing and deleting. Adding is simple, it's just "value.push (data)". Thanks for helping everyone.

+20
source share
9 answers

It took me some time, but I finally worked. Your answers and different approaches have helped. So, here is my CRUD implementation, if anyone has problems with this:

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

Screenshot: Alt text

Or you can check out the project demo: https://marinantonio.imtqy.com/angular-mat-table-crud/

The key parts are in the 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 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 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; } } .... 
+20
source

As I can see from your code that you are using pagination, you can do the following after the crud operation:

 this.dataSource.paginator = this.paginator; 

This will refresh the current page. And, glad that someone from Croatia uses angular material.

Here is the important part of my code:

 dialogRef.afterClosed().subscribe(result => { if (result === null) { return; } switch (mode) { // add new case 'C': { data.push(result.vendor); this.refreshTable(); break; } case 'U': { // update const index = data.findIndex((item) => item.buFmisVendorId === result.vendor.buFmisVendorId); if (index > -1) { data[index] = vendor; this.refreshTable(); } break; } } }); private refreshTable() { this.dataSource.paginator = this.paginator; } 
+6
source

I have some workarounds in editing data in a table without using modal windows.

You can take a look at my CRUD implementation with Angular 6 and Material

Data service

 import {Injectable} from '@angular/core'; import {HttpClient, HttpParams, HttpHeaders} from '@angular/common/http'; import {User} from './user'; @Injectable() export class UserService{ private url = "http://localhost:51120"; constructor(private http: HttpClient){ } getUsers(){ let getUrl = this.url + "/api/all/"; return this.http.get(getUrl); } createUser(user: User){ let saveUrl = this.url + "/api/Users"; return this.http.post(saveUrl, user); } updateUser(id: number, user: User) { const urlParams = new HttpParams().set("id", id.toString()); return this.http.post(this.url + "/api/update", user); } deleteUser(id: number){ const urlParams = new HttpParams().set("id", id.toString()); return this.http.delete(this.url + "/api/delete/" + id); } } 

Component

 @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [UserService] }) export class AppComponent implements OnInit { @ViewChild(MatPaginator) paginator: MatPaginator; addNewUser: User[] = [ { Id: 0, Name: null, Age: null, Email: null, Surname: null } ]; users: Array<User>; showTable: boolean; statusMessage: string; isLoaded: boolean = true; displayedColumnsUsers: string[] = ['Id', 'Name', 'Surname', 'Age', 'Email', 'Change', 'Delete']; displayedColumnsAddUser: string[] = ['Name', 'Surname', 'Age', 'Email', 'Save', 'Cancel']; dataSourceUsers: any; dataSourceAddUser: any; newUser : User; constructor(private serv: UserService, public dialog: MatDialog, public snackBar: MatSnackBar) { this.users = new Array<User>(); } @ViewChild(MatSort) sort: MatSort; ngOnInit() { this.loadUsers(); this.dataSourceAddUser = new MatTableDataSource(); } applyFilter(filterValue: string) { this.dataSourceUsers.filter = filterValue.trim().toLowerCase(); if (this.dataSourceUsers.paginator) { this.dataSourceUsers.paginator.firstPage(); } } private loadUsers() { this.isLoaded = true; this.serv.getUsers().subscribe((data: User[]) => { this.users = data; this.users.sort(function (obj1, obj2) { // Descending: first id less than the previous return obj2.Id - obj1.Id; }); this.isLoaded = false; this.dataSourceUsers = new MatTableDataSource(this.users); this.dataSourceAddUser = new MatTableDataSource(this.addNewUser); this.dataSourceUsers.sort = this.sort; this.dataSourceUsers.paginator = this.paginator; }, error => { alert("Error: " + error.name); this.isLoaded = false; } ); } deleteUserForDialog(user: User) { this.serv.deleteUser(user.Id).subscribe(data => { this.statusMessage = 'User ' + user.Name + ' is deleted', this.openSnackBar(this.statusMessage, "Success"); this.loadUsers(); }) } editUser(user: User) { this.serv.updateUser(user.Id, user).subscribe(data => { this.statusMessage = 'User ' + user.Name + ' is updated', this.openSnackBar(this.statusMessage, "Success"); this.loadUsers(); }, error => { this.openSnackBar(error.statusText, "Error"); } ); } saveUser(user: User) { if (user.Age != null && user.Name != null && user.Name != "" && user.Age != 0) { this.serv.createUser(user).subscribe(data => { this.statusMessage = 'User ' + user.Name + ' is added', this.showTable = false; this.openSnackBar(this.statusMessage, "Success"); this.loadUsers(); }, error => { this.showTable = false; this.openSnackBar(error.statusText, "Error"); } ); } else { this.openSnackBar("Please enter correct data", "Error") } } show() { this.showTable = true; this.addNewUser = [{ Id: 0, Name: null, Age: null, Email: null, Surname: null }]; } cancel() { this.showTable = false; } //snackBar openSnackBar(message: string, action: string) { this.snackBar.open(message, action, { duration: 3000, }); } //material dialog openDialog(element): void { const dialogRef = this.dialog.open(DialogOverviewExampleDialogComponent, { width: '250px', data: element, }); dialogRef.afterClosed().subscribe(result => { console.log('The dialog was closed'); if (result == "Confirm") { this.deleteUserForDialog(element); } }); } // Form field with error messages name = new FormControl('', [Validators.required]); getErrorMessage() { return this.name.hasError('required') ? 'You must enter a value' : this.name.hasError('name') ? 'Not a valid name' : ''; } age = new FormControl('', [Validators.required]); email = new FormControl('', [Validators.required, Validators.email]); surnameFormControl= new FormControl('', [Validators.required]); emailGetErrorMessage() { return this.email.hasError('required') ? 'You must enter a value' : this.email.hasError('email') ? 'Not a valid email' : ''; } onSubmit(newUser:User){ this.newUser = new User(0,"",0,"",""); } } 

https://github.com/AleksandrChuikov/Angular6MaterialCRUD

Here is the link to the demo: https://crud-angular6.azurewebsites.net

Updated to Angular 8

Click here to see screenshot

+6
source

A slightly different approach to deleting an item and updating a data table. It calls the api again, but this may work for smaller datasets.

 public deleteMember(memberId) { // Call the confirm dialog component this.confirmService.confirm('Confirm Delete', 'This action is final. Gone forever!') .switchMap(res => {if (res === true) { return this.appService.deleteItem(this.dbTable, memberId); }}) .subscribe( result => { this.success(); // Refresh DataTable to remove row. This solution calls the db and is a hack. this.ngAfterViewInit(); }, (err: HttpErrorResponse) => { console.log(err.error); console.log(err.message); this.messagesService.openDialog('Error', 'Delete did not happen.'); } ); } 

This is called at the top of the component, of course, but is included here for reference.

 private dbTable = 'members'; dataSource = new MatTableDataSource(); ngAfterViewInit() { this.appService = new AppService(this.http); this.dataSource.sort = this.sort; this.dataSource.paginator = this.paginator; // Populate the Material2 DataTable. Observable.merge(this.paginator.page) .startWith(null) // Delete this and no data is downloaded. .switchMap(() => { return this.appService.getItems( this.dbTable, this.paginator.pageIndex); }) .map(data => { return data.resource; }) .subscribe(data => { this.dataLength = data.length; this.dataSource.data = data; }); } 
+3
source

This solution uses my existing delete code, but the same for the update code. The key issue is finding the index of the array for the edited or deleted item. Note that as soon as the result is successful, I call the successful modification to notify the user, and then call the function to remove the row from the data table. Or you can update the data in this line with a little different code, for example, push the data into an array of objects. Thus, we do not need to download all the data again.

 public deleteMember(memberId) { // Call the confirm dialog component this.confirmService.confirm('Confirm Delete', 'This action is final. Gone forever!') .switchMap(res => {if (res === true) { return this.appService.deleteItem(this.dbTable, memberId); }}) .subscribe( result => { this.success(); // Refresh DataTable to remove row. this.updateDataTable (memberId); }, (err: HttpErrorResponse) => { console.log(err.error); console.log(err.message); this.messagesService.openDialog('Error', 'Delete did not happen.'); } ); } 

Now you can delete or update deleted or edited lines.

 private dsData: any; // Remove the deleted row from the data table. Need to remove from the downloaded data first. private updateDataTable (itemId) { this.dsData = this.dataSource.data; if (this.dsData.length > 0) { for (let i = 0; i < this.dsData.length; i++ ) { if (this.dsData[i].member_id === itemId) { this.dataSource.data.splice(i, 1); } } } this.dataSource.paginator = this.paginator; } 
+3
source

My answer is in Angular 6 Material 2 .

I used the splice function which takes as arguments the index of the edited row, then the number of rows to delete (in your case 1) and, thirdly, a new version of the edited row that will be inserted into this index:

 dialogRef.afterClosed().subscribe(result => { if(result !== '' && result !== null) { const idx_editedRow = this.mattabledatasource.data.indexOf(row); this.mattabledatasource.data.splice(idx_editedRow, 1, result); loadData(); } }); 
+3
source

In fact, you do not need to update the table after editing if you have the following HTML:

 <mat-table [dataSource]="dataSource" matSort> <ng-container matColumnDef="userName"> <mat-header-cell mat-sort-header> UserName </mat-header-cell> <mat-cell *matCellDef="let row"> {{row.userName}} </mat-cell> </ng-container> <ng-container matColumnDef="actions"> <mat-cell *matCellDef="let user"> <button mat-icon-button matTooltip="Edit" (click)="editUser(user)"> <mat-icon>edit</mat-icon> </button> </mat-cell> </ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-row *matRowDef="let row; columns: displayedColumns;"> </mat-row> </mat-table> 

And, in .ts you have:

 private editUser(user?: User) { let userTest: User = user; userTest.userName = "user123"; } 

You can see that automatically the line when you click Change username (in this case, "user123")

+3
source

Can you take a look at

 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); }); } 

Is a POST request and data sending in progress? You refer to Baza in a POST request, which should be "base" (lowercase B). Perhaps because of this, the request fails and the observed subscription is never executed ... you can double check this theory with the error handler in the subscription.

 addItem(baza: Baza): void { this.httpClient.post(this.API_URL, baza).subscribe(data => { const copiedData = this.data.slice(); copiedData.push(baza); console.log(copiedData); this.dataChange.next(copiedData); }, (errror) => { console.log(error); }); } 

Finally, regarding the changes, my approach will be slightly different. Add the same instance of the DataService to the component and pass the same link to the DataSource table, not to the new instance. Then pass the entire base object to the editing dialog, and not just its properties. Then, in the dialog box, close the source (unedited object), as well as new properties (or, even better, a new Baza class object with edited fields). Send them to our data service using the "edit / update" method. The edit / update method will filter an existing set of data arrays, looking for any records that correspond to our unedited object, and set them in accordance with our new object. A slightly abstracted example below

//eg. Component

 export class BazaComponent implements OnInit { .... constructor( public httpClient: HttpClient, public dialog: MatDialog, public dataService: DataService ){} .... public loadData() { this.dataSource = new ExampleDataSource(this.dataService, 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; }); } .... startEdit(baza: Baza) { const dialogRef = this.dialog.open(EditDialogComponent, { data: { baza: baza } }); dialogRef.afterClosed().subscribe(result => { // result will be simple array of our 'old' baza object that we passed in, and the 'new' baza object that contains the edits this.dataService.updateItem(result[0], result[1]); }); } dialogRef.close(['close',editBaza,baza]); 

//eg. services

 export class DataService { .... set data(data: Baza[]) { this.dataChange.next(data); } .... updateItem(oldBaza: Baza, newBaza: Baza){ this.data = this.data.map((baza: Baza) => { if(baza === oldBaza) return newBaza; return baza; }); } 
+2
source

The structure of the jobposting.component.ts file:

 export class JobPostingComponent implements OnInit { values: JobPosting[]; columns: string[] = ['title', 'vacancies','division.name']; displayedColumns: string[] = ['actions'].concat(this.columns); dataSource: MatTableDataSource<JobPosting>; 

I used findIndex for the row to update, and inserted the updated row values ​​into this index of the array of values.

 onEdit(data: JobPosting) { const dialogRef = this.dialog.open(AddJobPostingComponent, { data, width: '1000px' }); dialogRef.afterClosed().subscribe(res => { if (res !== undefined) { const id = res.id; const index = this.values.findIndex(x => x.id === id); this.values[index] = res; this.dataSource.data = this.values; } }); } 
+1
source

Source: https://habr.com/ru/post/1273587/


All Articles