Muhi Masri

[Part 3] Load, Add, Update and Delete Table Rows using API Services

Learn how to load, add, update and delete table rows with API services using Angular Material

Last updated on April 08, 2022

angular
angular material
Angular Editable Table

In this part of the Angular Material Table series, we take all operations we built in the first and second parts (load, add, edit and delete data) and connect them to API services.

We won’t be building or using any custom backend solution as this is not the objective of this series. Instead, we are utilizing DummyJSON, which is a free fake API for testing and prototyping.

At the end of this tutorial, we should have the following results (all video demos will have the network panel open to show API calls):

As we continue building upon the same code from previous parts in this series, it’s recommended that you go through these tutorials from the beginning to understand how the code structure works as we will not be explaining it. You can navigate to any part using the table of contents.

Before we begin, the complete repository is accessible here and a working example is available below:

Create a service and a model class

Previously, we only had static data in the component class with no model as we were only focusing on building an editable table. Now that we are connecting operations to API services, we will add a model and a service layer.

DummyJSON provides us with different resources like posts, users, products, comments..etc. For simplicity purposes, we’ll pick users and create a new user.ts model:

export interface User {
  isSelected: boolean;
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  birthDate: string;
  isEdit: boolean;
}

export const UserColumns = [
  {
    key: 'isSelected',
    type: 'isSelected',
    label: '',
  },
  {
    key: 'firstName',
    type: 'text',
    label: 'First Name',
  },
  {
    key: 'lastName',
    type: 'text',
    label: 'Last Name',
  },
  {
    key: 'email',
    type: 'email',
    label: 'Email',
  },
  {
    key: 'birthDate',
    type: 'date',
    label: 'Date of Birth',
  },
  {
    key: 'isEdit',
    type: 'isEdit',
    label: '',
  },
];

A quick recap from the first part, UserColumns is used in the template to help us create dynamic input fields when editing a row. For example, the firstName column has a text type, so it should show a textbox when editing a row. If we change it to date, it will populate a date input field. We can use any type we’d like as long as it’s defined in the template. Another reminder; isSelected and isEdit are not part of the backend model. They are used in the view model to manage the row’s state (whether or not the row is in edit mode or is selected).

In the app.component.ts class, let’s change what we had earlier to use the new `UserColumns` model:

import { Component } from '@angular/core';
import { UserColumns } from './model/user';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  displayedColumns: string[] = UserColumns.map((col) => col.key);
  dataSource = USER_DATA;
  columnsSchema: any = UserColumns;
}

Now, we can create a basic user.service.ts class with a service URL and other required imports like Observable, HttpClient, User model…

import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private serviceUrl = 'https://dummyjson.com/users'

  constructor(private http: HttpClient) {}
}

Load data with REST API

Now that we have the model and service classes set up correctly, we can add a getUsers method to the service to trigger an API call and return an observable list of users:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../model/user';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private serviceUrl = 'https://dummyjson.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http
      .get(this.serviceUrl)
      .pipe<User[]>(map((data: any) => data.users));
  }
}

In the app.component.ts class, let’s subscribe to the service method and assign the results to the dataSource object:

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { User, UserColumns } from './model/user';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  displayedColumns: string[] = UserColumns.map((col) => col.key);
  columnsSchema: any = UserColumns;
  dataSource = new MatTableDataSource<User>();

  constructor(public dialog: MatDialog, private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe((res: any) => {
      this.dataSource.data = res;
    });
  }
}

Previously, dataSource used to be of type Array, but we are changing it to MatTableDataSource to leverage its features later in this series.

Now if we run the code, we should get the following results:

image 3 1024x710

Update a row with REST API

A patch API service is usually the most suitable operation for updating data. So let’s create a updateUser function, attach the user id to the service URL and pass the user object as a parameter:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../model/user';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private serviceUrl = 'https://dummyjson.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http
      .get(this.serviceUrl)
      .pipe<User[]>(map((data: any) => data.users));
  }

  updateUser(user: User): Observable<User> {
    return this.http.patch<User>(`${this.serviceUrl}/${user.id}`, user);
  }
}

Now we can create a new editRow function in the app.component.ts class that subscribes to the updateUser API service:

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { User, UserColumns } from './model/user';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  displayedColumns: string[] = UserColumns.map((col) => col.key);
  columnsSchema: any = UserColumns;
  dataSource = new MatTableDataSource<User>();

  constructor(public dialog: MatDialog, private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe((res: any) => {
      this.dataSource.data = res;
    });
  }

  editRow(row: User) {
    this.userService.updateUser(row).subscribe(() => row.isEdit = false);
  }
}

After subscribing to the service call, we can change the row to non-edit mode again row.isEdit = false

Lastly, in the app.component.html template, let’s add the editRow function to the button’s click event and pass it the element as a parameter:

<button mat-button (click)="editRow(element)">Done</button>

Here is the final results in action:

Add a row with REST API

Adding a new row requires a post API service with the user object passed as a parameter. Let’s add a new addUser function in user.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../model/user';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private serviceUrl = 'https://dummyjson.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http
      .get(this.serviceUrl)
      .pipe<User[]>(map((data: any) => data.users));
  }

  updateUser(user: User): Observable<User> {
    return this.http.patch<User>(`${this.serviceUrl}/${user.id}`, user);
  }

  addUser(user: User): Observable<User> {
    return this.http.post<User>(`${this.serviceUrl}/add`, user);
  }
}

In app.component.ts, we need to do two modifications:

  1. Create addRow function that inserts an empty object into the dataSource (similar to what we did in the second part)
  2. Because the “Done” button will call the same function, we’ll update editRow function to support both adding and editing a row. We can know which service to trigger based on whether or not the id is zero.
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { User, UserColumns } from './model/user';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  displayedColumns: string[] = UserColumns.map((col) => col.key);
  columnsSchema: any = UserColumns;
  dataSource = new MatTableDataSource<User>();

  constructor(public dialog: MatDialog, private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe((res: any) => {
      this.dataSource.data = res;
    });
  }

  editRow(row: User) {
    if (row.id === 0) {
      this.userService.addUser(row).subscribe((newUser: User) => {
        row.id = newUser.id;
        row.isEdit = false;
      });
    } else {
      this.userService.updateUser(row).subscribe(() => (row.isEdit = false));
    }
  }

  addRow() {
    const newRow: User = {
      id: 0,
      firstName: '',
      lastName: '',
      email: '',
      birthDate: '',
      isEdit: true,
      isSelected: false,
    };
    this.dataSource.data = [newRow, ...this.dataSource.data];
  }
}

After adding a row, we’re assigning it the new id to ensure that the row gets updated next time.

Let’s take a look at what we have so far:

Delete rows with REST API

Deleting a row requires a delete API service with the id attached to the URL. Let’s add a new deleteUser function in the user.service.ts class:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../model/user';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private serviceUrl = 'https://dummyjson.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http
      .get(this.serviceUrl)
      .pipe<User[]>(map((data: any) => data.users));
  }

  updateUser(user: User): Observable<User> {
    return this.http.patch<User>(`${this.serviceUrl}/${user.id}`, user);
  }

  addUser(user: User): Observable<User> {
    return this.http.post<User>(`${this.serviceUrl}/add`, user);
  }

  deleteUser(id: number): Observable<User> {
    return this.http.delete<User>(`${this.serviceUrl}/${id}`);
  }
}

In app.component.ts, we’ll create a removeRow function that triggers a delete service. Then, after deleting successfully, we can filter the dataSource to remove the deleted row from the table:

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { User, UserColumns } from './model/user';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  displayedColumns: string[] = UserColumns.map((col) => col.key);
  columnsSchema: any = UserColumns;
  dataSource = new MatTableDataSource<User>();

  constructor(public dialog: MatDialog, private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe((res: any) => {
      this.dataSource.data = res;
    });
  }

  editRow(row: User) {
    if (row.id === 0) {
      this.userService.addUser(row).subscribe((newUser: User) => {
        row.id = newUser.id;
        row.isEdit = false;
      });
    } else {
      this.userService.updateUser(row).subscribe(() => (row.isEdit = false));
    }
  }

  addRow() {
    const newRow: User = {
      id: 0,
      firstName: '',
      lastName: '',
      email: '',
      birthDate: '',
      isEdit: true,
      isSelected: false,
    };
    this.dataSource.data = [newRow, ...this.dataSource.data];
  }

  removeRow(id: number) {
    this.userService.deleteUser(id).subscribe(() => {
      this.dataSource.data = this.dataSource.data.filter(
        (u: User) => u.id !== id
      );
    });
  }
}

Below you can see how the delete REST is being triggered:

We added a feature to remove multiple rows with a confirmation dialog in the last part. Let’s modify it to make it work with an API call.

Since DummyJSON doesn’t support multiple deletes (at least as far I know), we can use forkJoin from Rxjs lib to do the job. Let’s create a deleteUsers function that takes a list of users and generate multiple API calls:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { forkJoin, Observable } from 'rxjs';
import { User } from '../model/user';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private serviceUrl = 'https://dummyjson.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http
      .get(this.serviceUrl)
      .pipe<User[]>(map((data: any) => data.users));
  }

  updateUser(user: User): Observable<User> {
    return this.http.patch<User>(`${this.serviceUrl}/${user.id}`, user);
  }

  addUser(user: User): Observable<User> {
    return this.http.post<User>(`${this.serviceUrl}/add`, user);
  }

  deleteUser(id: number): Observable<User> {
    return this.http.delete<User>(`${this.serviceUrl}/${id}`);
  }

  deleteUsers(users: User[]): Observable<User[]> {
    return forkJoin(
      users.map((user) =>
        this.http.delete<User>(`${this.serviceUrl}/${user.id}`)
      )
    );
  }
}

The function is very similar to the deleteUser one, except that we are using map to generate multiple http calls, and in return, we’ll get a list of observable users.

In app.component.ts, we’ll keep the same function we did in the last part but enhance it to call the deleteUsers service when clicking the submit button in the dialog:

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { User, UserColumns } from './model/user';
import { UserService } from './services/user.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  displayedColumns: string[] = UserColumns.map((col) => col.key);
  columnsSchema: any = UserColumns;
  dataSource = new MatTableDataSource<User>();

  constructor(public dialog: MatDialog, private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe((res: any) => {
      this.dataSource.data = res;
    });
  }

  editRow(row) {
    if (row.id === 0) {
      this.userService.addUser(row).subscribe((newUser: User) => {
        row.id = newUser.id;
        row.isEdit = false;
      });
    } else {
      this.userService.updateUser(row).subscribe(() => (row.isEdit = false));
    }
  }

  addRow() {
    const newRow: User = {
      id: 0,
      firstName: '',
      lastName: '',
      email: '',
      birthDate: null,
      isEdit: true,
      isSelected: false,
    };
    this.dataSource.data = [newRow, ...this.dataSource.data];
  }

  removeRow(id) {
    this.userService.deleteUser(id).subscribe(() => {
      this.dataSource.data = this.dataSource.data.filter(
        (u: User) => u.id !== id
      );
    });
  }

  removeSelectedRows() {
    const users = this.dataSource.data.filter((u: User) => u.isSelected);
    this.dialog
      .open(ConfirmDialogComponent)
      .afterClosed()
      .subscribe((confirm) => {
        if (confirm) {
          this.userService.deleteUsers(users).subscribe(() => {
            this.dataSource.data = this.dataSource.data.filter(
              (u: User) => !u.isSelected
            );
          });
        }
      });
  }
}

Here is how deleting multiple rows looks like:

Summary

If you made it to this section, pat yourself on the back! You just learned how to:

  • Create a User model and interface
  • Create a service layer with different API calls
  • Load, add, edit and delete rows with Material table and API services

You can access the complete repository here. I hope you enjoyed learning from this article and if you have any questions, please leave a comment below.

Bye for now!

Recommended Reading

Learn how to validate table rows and input fields with Angular Material and HTML form validation

angular
angular material

Discussion

Master your front-end development techniques 🌟

Subscribe to stay up-to-date and receive quality front-end development articles straight to your inbox!

No spam, sales, or ads. Unsubscribe anytime you wish.

© 2022, Muhi Masri