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:
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:
- Create
addRow
function that inserts an empty object into thedataSource
(similar to what we did in the second part) - 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.
Bye for now 👋