eCommerce
Angular Data Table: A complete example of Server Pagination, Filtering, Sorting, Grouping, Edit forms, Sub-items.
This documentation contains a complete practical example of how to use Metronic for any CRUD applications development.
We use angular-in-memory
library for back-end emulation (Mock Back-end).
This library emulates CRUD operations over a RESTy API.
When your Real backend is done, for switching to it is please read the document How switching to the Real Back-end.
The end result of this documentation will be:
A complete example of how to implement (using Metronic theme) an Angular Material Data Table with server-side pagination, sorting and filtering using a custom CDK Data Source.
Scenario:
-
Standard Data table development. The Data table includes:
-
Actions:
eCommerce application structure
The given scheme is an example of eCommerce application showing cars sale. The following mockup represents entities to work with:
Folders and files
_core folder structure |
Folder | Description |
_server :
|
Fake database (for real REST server simulation)
Used library angular-in-memory.
The folder contains service fake-api.service which is imported into
e-commerce.module.ts :
HttpClientInMemoryWebApiModule.forFeature(FakeApiService)
|
models :
|
The folder contains description of entities listed in the application.
CustomerModel
ProductModel
ProductSpecificationModel
ProductRemarkModel
Entities are inherited from BaseModel class which operates next entities:
There is also data-sources folder described below.
|
services :
|
Standard angular services with REST API calls.
|
utils :
|
Auxiliary services |
Importing our services and Angular Material modules
Import all the Angular Material modules as described below. See e-commerce.module.ts
code:
import { NgModule } from '@angular/core';
import { CommonModule, } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { TranslateModule } from '@ngx-translate/core';
import { PartialsModule } from '../../../../partials/partials.module';
import { ECommerceComponent } from './e-commerce.component';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
// Core
import { FakeApiService } from './_core/_server/fake-api.service';
// Core => Services
import { CustomersService } from './_core/services/customers.service';
import { OrdersService } from './_core/services/orders.service';
import { ProductRemarksService } from './_core/services/product-remarks.service';
import { ProductSpecificationsService } from './_core/services/product-specifications.service';
import { ProductsService } from './_core/services/products.service';
import { SpecificationsService } from './_core/services/specification.service';
// Core => Utils
import { HttpUtilsService } from './_core/utils/http-utils.service';
import { TypesUtilsService } from './_core/utils/types-utils.service';
import { LayoutUtilsService } from './_core/utils/layout-utils.service';
// Shared
import { ActionNotificationComponent } from './_shared/action-natification/action-notification.component';
import { DeleteEntityDialogComponent } from './_shared/delete-entity-dialog/delete-entity-dialog.component';
import { FetchEntityDialogComponent } from './_shared/fetch-entity-dialog/fetch-entity-dialog.component';
import { UpdateStatusDialogComponent } from './_shared/update-status-dialog/update-status-dialog.component';
import { AlertComponent } from './_shared/alert/alert.component';
// Customers
import { CustomersListComponent } from './customers/customers-list/customers-list.component';
import { CustomerEditDialogComponent } from './customers/customer-edit/customer-edit.dialog.component';
// Products
import { ProductsListComponent } from './products/products-list/products-list.component';
import { ProductEditComponent } from './products/product-edit/product-edit.component';
import { RemarksListComponent } from './products/_subs/remarks/remarks-list/remarks-list.component';
import { SpecificationsListComponent } from './products/_subs/specifications/specifications-list/specifications-list.component';
import { SpecificationEditDialogComponent } from './products/_subs/specifications/specification-edit/specification-edit-dialog.component';
// Orders
import { OrdersListComponent } from './orders/orders-list/orders-list.component';
import { OrderEditComponent } from './orders/order-edit/order-edit.component';
// Material
import {
MatInputModule,
MatPaginatorModule,
MatProgressSpinnerModule,
MatSortModule,
MatTableModule,
MatSelectModule,
MatMenuModule,
MatProgressBarModule,
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatTabsModule,
MatNativeDateModule,
MatCardModule,
MatRadioModule,
MatIconModule,
MatDatepickerModule,
MatAutocompleteModule,
MAT_DIALOG_DEFAULT_OPTIONS,
MatSnackBarModule,
MatTooltipModule
} from '@angular/material';
const routes: Routes = [
{
path: '',
component: ECommerceComponent,
children: [
{
path: '',
redirectTo: 'customers',
pathMatch: 'full'
},
{
path: 'customers',
component: CustomersListComponent
},
{
path: 'orders',
component: OrdersListComponent
},
{
path: 'products',
component: ProductsListComponent,
},
{
path: 'products/add',
component: ProductEditComponent
},
{
path: 'products/edit',
component: ProductEditComponent
},
{
path: 'products/edit/:id',
component: ProductEditComponent
},
]
}
];
@NgModule({
imports: [
MatDialogModule,
CommonModule,
HttpClientModule,
PartialsModule,
RouterModule.forChild(routes),
FormsModule,
ReactiveFormsModule,
TranslateModule.forChild(),
MatButtonModule,
MatMenuModule,
MatSelectModule,
MatInputModule,
MatTableModule,
MatAutocompleteModule,
MatRadioModule,
MatIconModule,
MatNativeDateModule,
MatProgressBarModule,
MatDatepickerModule,
MatCardModule,
MatPaginatorModule,
MatSortModule,
MatCheckboxModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatTabsModule,
MatTooltipModule,
HttpClientInMemoryWebApiModule.forFeature(FakeApiService)
],
providers: [
{
provide: MAT_DIALOG_DEFAULT_OPTIONS,
useValue: {
hasBackdrop: true,
panelClass: 'm-mat-dialog-container__wrapper', // CSS wrapper for Material dialog
height: 'auto',
width: '900px'
}
},
HttpUtilsService,
CustomersService,
OrdersService,
ProductRemarksService,
ProductSpecificationsService,
ProductsService,
SpecificationsService,
TypesUtilsService,
LayoutUtilsService
],
entryComponents: [
ActionNotificationComponent,
CustomerEditDialogComponent,
DeleteEntityDialogComponent,
FetchEntityDialogComponent,
UpdateStatusDialogComponent,
SpecificationEditDialogComponent
],
declarations: [
ECommerceComponent,
// Shared
ActionNotificationComponent,
DeleteEntityDialogComponent,
FetchEntityDialogComponent,
UpdateStatusDialogComponent,
AlertComponent,
// Customers
CustomersListComponent,
CustomerEditDialogComponent,
// Orders
OrdersListComponent,
OrderEditComponent,
// Products
ProductsListComponent,
ProductEditComponent,
RemarksListComponent,
SpecificationsListComponent,
SpecificationEditDialogComponent
]
})
export class ECommerceModule { }
Here is a breakdown of the contents of common Material module:
- MatInputModule: this contains the components and directives for adding Material design Input Boxes to our application (needed for the search input box)
- MatTableModule: this is the core data table module, which includes the mat-table component and many related components and directives
- MatPaginatorModule: this is a generic pagination module, that can be used to paginate data in general. This module can also be used separately from the Data table, for example for implementing Detail pagination logic in a Master-Detail setup
- MatSortModule: this is an optional module that allows adding sortable headers to a data table
- MatProgressSpinnerModule: this module includes the progress indicator component that we will be using to indicate that data is being loaded from the backend
Сustomer list component
In order to have full understanding of what is going on we strongly recommend that you read the article https://blog.angular-university.io/angular-material-data-table.
The article describes the bases of angular Material Data Table formation and solutions for specific tasks (Data binding and Material Table, Sorting, Paginator and Filtration).
Also, to understand how Grouping works we strongly recommend that you read official documentation of Material Angular Table (Selection feature)
The ‘Customers list’ component view:
Material Table with custom DataSource
:
customers/customer-list/customer-list.component.html
:
<mat-table class="lmat-elevation-z8" [dataSource]="dataSource" matSort matSortActive="id" matSortDirection="asc" matSortDisableClear>
<!-- Material table HTML -->
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<div class="mat-table__message" *ngIf="!dataSource.hasItems">No records found</div><!-- Message for empty data -->
customers/customer-list/customer-list.component.ts
:
// Importing Material
import { SelectionModel } from '@angular/cdk/collections';
import { MatPaginator, MatSort, MatSnackBar, MatDialog } from '@angular/material';
// Importing CustomerDataSource - extends BaseDataSource
import { CustomersDataSource } from '../../_core/models/data-sources/customers.datasource';
// ...
// ...
export class CustomersListComponent implements OnInit {
// Variables declaration
// ...
// Columns which should view in table
displayedColumns = ['select', 'id', 'lastName', 'firstName', 'email', 'gender', 'status', 'type', 'actions'];
// ...
// ...
constructor(private customersService: CustomersService, ***other services) {}
// ...
/** LOAD DATA */
ngOnInit() {
// ...
this.dataSource = new CustomersDataSource(this.customersService); //Init DataSource
// First load
this.dataSource.loadCustomers(queryParams); // Loading data
// ...
}
}
Our CustomerDataSource
extends BaseDataSource.
_core/models/data-sources/_base.datasource.ts
:
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { Observable, BehaviorSubject, from } from 'rxjs';
import { QueryParamsModel } from '../query-models/query-params.model';
import { QueryResultsModel } from '../query-models/query-results.model';
import { BaseModel } from '../_base.model';
import * as _ from 'lodash';
// Why not use MatTableDataSource?
/* In this example, we will not be using the built-in MatTableDataSource because its designed for filtering,
sorting and pagination of a client - side data array.
Read the article: 'https://blog.angular-university.io/angular-material-data-table/'
**/
export class BaseDataSource implements DataSource {
entitySubject = new BehaviorSubject([]);
hasItems: boolean = false; // Need to show message: 'No records found'
// Loading | Progress bar
loadingSubject = new BehaviorSubject(false);
loading$: Observable;
// Paginator | Paginators count
paginatorTotalSubject = new BehaviorSubject(0);
paginatorTotal$: Observable;
constructor() {
this.loading$ = this.loadingSubject.asObservable();
this.paginatorTotal$ = this.paginatorTotalSubject.asObservable();
this.paginatorTotal$.subscribe(res => this.hasItems = res > 0);
}
connect(collectionViewer: CollectionViewer): Observable {
// Connecting data source
return this.entitySubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
// Disonnecting data source
this.entitySubject.complete();
this.loadingSubject.complete();
this.paginatorTotalSubject.complete();
}
}
The next table contains fields added by Metronic into standard DataSource
:
Field | Description |
loadingSubject : BehaviorSubject
loading$ : Observable<boolean> |
Used to display the load item. Example of use in customers/customer-list/customer-list.component.html :
<m-portlet [options]="{headLarge: true}" [loading$]="dataSource.loading$">
|
hasItems : boolean |
Used to display text No records found if the result of the query returns an empty data array
<div class="mat-table__message" *ngIf="!dataSource.hasItems">No records found</div>
|
CustomersDataSource
is inherited from BaseDataSource
and has the only method loadCustomers
:
import { Observable, of } from 'rxjs';
import { catchError, finalize, tap } from 'rxjs/operators';
import { CustomersService } from '../../services/customers.service';
import { QueryParamsModel } from '../query-models/query-params.model';
import { BaseDataSource } from './_base.datasource';
import { QueryResultsModel } from '../query-models/query-results.model';
export class CustomersDataSource extends BaseDataSource {
constructor(private customersService: CustomersService) {
super(); // Call BaseDataSource constructor
}
loadCustomers(queryParams: QueryParamsModel) {
this.loadingSubject.next(true);
this.customersService.findCustomers(queryParams).pipe(
tap(res => {
this.entitySubject.next(res.items); // Updating data
this.paginatorTotalSubject.next(res.totalCount); // Refreshing paginator
}),
catchError(err => of(new QueryResultsModel([], err))),
finalize(() => this.loadingSubject.next(false)) // Hiding loading
).subscribe();
}
}
loadCustomers
method is used to populate the table by using
customers/customer-list/customer-list.component.ts
component and has one
queryParams
input parameter with
QueryParamsModel
type:
QueryParamsModel class intended for the wrapping of request object and sending to the server |
Field |
Type |
Description |
filter |
any |
Filtration object. For example, filtration object for customers is CustomerModel |
sortOrder |
string |
string; // asc || desc
asc - is default value |
sortField |
string |
The filed intended for sorting. id is the default sorting value for Customers. |
pageNumber |
number |
Paginator number. 1 is default value |
pageSize |
number |
The field determines the number of rows displayed in the table. By default pageSize equals 10. |
loadCustomers
method calls findCustomers(queryParams)
from _core/services/customers.service.ts
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, forkJoin, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { HttpUtilsService } from '../utils/http-utils.service';
import { CustomerModel } from '../models/customer.model';
import { QueryParamsModel } from '../models/query-models/query-params.model';
import { QueryResultsModel } from '../models/query-models/query-results.model';
const API_CUSTOMERS_URL = 'api/customers';
@Injectable()
export class CustomersService {
constructor(private http: HttpClient, private httpUtils: HttpUtilsService) { }
// Method from server should return instance of QueryResultsModel type
// QueryResultsModel type has two fields =>
// 1. items:CustomerModel[]
// 2. totalsCount: number
findCustomers(queryParams: QueryParamsModel): Observable {
const params = this.httpUtils.getFindHTTPParams(queryParams);
const url = this.API_CUSTOMERS_URL + '/find';
return this.http.get(url, params); // Server call which return filter&sorting result
}
}
Data table Sorting:
Sorting view in the ‘Customers list’ component:

customers/customer-list/customer-list.component.html
:
<mat-table class="lmat-elevation-z8"
[dataSource]="dataSource"
matSort
matSortActive="id"
matSortDirection="asc"
matSortDisableClear>
<ng-container matColumnDef="id">
<!-- ATTRIBUTE mat-sort-header for sorting | https://material.angular.io/components/sort/overview -->
<mat-header-cell *matHeaderCellDef mat-sort-header>ID</mat-header-cell>
<mat-cell *matCellDef="let customer">{{customer.id}}</mat-cell>
</ng-container>
</mat-table>
customers/customer-list/customer-list.component.ts
:
// Importing Material MatSort
import { MatSort } from '@angular/material';
// ...
export class CustomersListComponent implements OnInit {
// ...
// Variables declaration
@ViewChild(MatSort) sort: MatSort;
// ...
ngOnInit() {
// If the user changes the sort order, reset back to the first page.
this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));
/* Data load will be triggered in two cases:
- when a pagination event occurs => this.paginator.page
- when a sort event occurs => this.sort.sortChange
**/
merge(this.sort.sortChange, this.paginator.page)
.pipe(
tap(() => { this.loadCustomersList(); })
)
.subscribe();
}
// ...
}
Data table Paginator:
Paginator view in 'Customers list' component:

customers/customer-list/customer-list.component.html
:
<!-- MATERIAL PAGINATOR | Binded to dasources -->
<!-- See off.documentations 'https://material.angular.io/components/paginator/overview' -->
<mat-paginator [pageSize]="10" [pageSizeOptions]="[3, 5, 10]" [length]="dataSource.paginatorTotal$ | async" [showFirstLastButtons]="true"></mat-paginator>
customers/customer-list/customer-list.component.ts
:
// Importing Material MatPaginator
import { MatPaginator } from '@angular/material';
// ...
export class CustomersListComponent implements OnInit {
// ...
// Variables declaration
@ViewChild(MatPaginator) paginator: MatPaginator;
// ...
ngOnInit() {
/* Data load will be triggered in two cases:
- when a pagination event occurs => this.paginator.page
- when a sort event occurs => this.sort.sortChange
**/
merge(this.sort.sortChange, this.paginator.page)
.pipe(
tap(() => { this.loadCustomersList(); })
)
.subscribe();
}
// ...
}
Data table Progress spinner (Loading element):
Progress spinner (loading element) view in the ‘Customers list’ component:

customers/customer-list/customer-list.component.html
:
<!-- MATERIAL SPINNER | Url: 'https://material.angular.io/components/progress-spinner/overview' -->
<mat-spinner [diameter]="20" *ngIf="dataSource.loading$ | async"></mat-spinner>
customers/customer-list/customer-list.component.html
:
//nothing is needed, loading is binded in BaseDataSource
Data table Grouping:
Grouping view in the ‘Customers list’ component:

customers/customer-list/customer-list.component.html
:
<!-- STYCKY PORTLET CONTROL | See structure => /metronic/sticky-form-actions -->
<m-portlet [options]="{headLarge: true}" [loading$]="dataSource.loading$">
<!-- start::Body (attribute: mPortletBody) -->
<ng-container mPortletBody>
<div class="m-form">
<!-- start::GROUP ACTIONS -->
<!-- Group actions list: 'Delete selected' | 'Fetch selected' | 'Update status for selected' -->
<!-- Group actions are shared for all LISTS | See '../../_shared' folder -->
<div class="row align-items-center collapse m-form__group-actions m--margin-top-20 m--margin-bottom-20"
[ngClass]="{'show' : selection.selected.length > 0}">
<!-- We show 'Group Actions' div if smth are selected -->
<div class="col-xl-12">
<div class="m-form__group m-form__group--inline">
<div class="m-form__label m-form__label-no-wrap">
<label class="m--font-bold m--font-danger-">
<span translate="ECOMMERCE.COMMON.SELECTED_RECORDS_COUNT"></span> {{ selection.selected.length }}
</label>
<!-- selectedCountsTitle => function from codeBehind (customer-list.component.ts file) -->
<!-- selectedCountsTitle => just returns title of selected items count -->
<!-- for example: Selected records count: 9 -->
</div>
<div class="m-form__control m-form__group--inline">
<button (click)="deleteCustomers()" mat-raised-button color="accent" matTooltip="Delete selected customers">
<mat-icon>delete</mat-icon> Delete All
</button> <!-- Call 'delete-entity-dialog' from _shared folder -->
<button (click)="fetchCustomers()" mat-raised-button matTooltip="Fetch selected customers">
<mat-icon>clear_all</mat-icon> Fetch Selected
</button> <!-- Call 'fetch-entity-dialog' from _shared folder -->
<button (click)="updateStatusForCustomers()" mat-raised-button matTooltip="Update status for selected customers">
<mat-icon>update</mat-icon> Update status
</button><!-- Call 'update-stated-dialog' from _shared folder -->
</div>
</div>
</div>
</div>
<!-- end::GROUP ACTIONS -->
</div>
</ng-container>
<!-- end::Body -->
<!-- other code -->
<div class="mat-table__wrapper">
<mat-table class="lmat-elevation-z8" [dataSource]="dataSource" matSort matSortActive="id" matSortDirection="asc" matSortDisableClear>
<!-- Checkbox Column -->
<!-- Table with selection -->
<!-- https://run.stackblitz.com/api/angular/v1?file=app%2Ftable-selection-example.ts -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef class="mat-column-checkbox">
<mat-checkbox (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row" class="mat-column-checkbox">
<mat-checkbox (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
</mat-checkbox>
</mat-cell>
</ng-container>
<!-- Other columns -->
</mat-table>
</div>
</m-portlet>
customers/customer-list/customer-list.component.ts
:
// Importing Material SelectionModel
import { SelectionModel } from '@angular/cdk/collections';
// ...
export class CustomersListComponent implements OnInit {
// ...
// Variables declaration
// Selection
selection = new SelectionModel(true, []);
customersResult: CustomerModel[] = [];
// ...
ngOnInit() {
// ...
this.dataSource.entitySubject.subscribe(res => (this.customersResult = res));
// ...
}
// ...
// ...
/** SELECTION */
isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.customersResult.length;
return numSelected === numRows;
}
masterToggle() {
if (this.selection.selected.length === this.customersResult.length) {
this.selection.clear();
} else {
this.customersResult.forEach(row => this.selection.select(row));
}
}
// ...
}
Data table Filtration:
Filtration view in the ‘Customers list’ component:

customers/customer-list/customer-list.component.html
:
<!-- STYCKY PORTLET CONTROL | See structure => /metronic/sticky-form-actions -->
<m-portlet [options]="{headLarge: true}" [loading$]="dataSource.loading$">
<!-- start::Body (attribute: mPortletBody) -->
<ng-container mPortletBody>
<div class="m-form">
<!-- start::FILTERS -->
<div class="m-form__filtration">
<div class="row align-items-center">
<div class="col-md-2 m--margin-bottom-10-mobile">
<!-- 'm margin-bottom-10-mobile' for adaptive make-up -->
<div class="m-form__control">
<mat-form-field class="mat-form-field-fluid">
<mat-select [(value)]="filterStatus" (selectionChange)="loadCustomersList()">
<mat-option value="">All</mat-option>
<mat-option value="0">Suspended</mat-option>
<mat-option value="1">Active</mat-option>
<mat-option value="Pending">Pending</mat-option>
</mat-select>
<mat-hint align="start">
<strong>Filter</strong> by Status</mat-hint>
</mat-form-field>
</div>
</div>
<div class="col-md-2 m--margin-bottom-10-mobile">
<div class="m-form__control">
<mat-form-field class="mat-form-field-fluid">
<mat-select [(value)]="filterType" (selectionChange)="loadCustomersList()">
<mat-option value="">All</mat-option>
<mat-option value="0">Business</mat-option>
<mat-option value="1">Individual</mat-option>
</mat-select>
<mat-hint align="start">
<strong>Filter</strong> by Type</mat-hint>
</mat-form-field>
</div>
</div>
<div class="col-md-2 m--margin-bottom-10-mobile">
<mat-form-field class="mat-form-field-fluid">
<input matInput placeholder="Search customer" #searchInput placeholder="Search">
<mat-hint align="start">
<strong>Search</strong> in all fields</mat-hint>
</mat-form-field>
</div>
</div>
</div>
<!-- end::FILTERS -->
</div>
</ng-container>
<!-- other code -->
</m-portlet>
customers/customer-list/customer-list.component.ts
:
// Importing RXJS functions
// RXJS
import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
import { fromEvent, merge, forkJoin } from 'rxjs';
// ...
export class CustomersListComponent implements OnInit {
// ...
// Variables declaration
// Filter fields
@ViewChild('searchInput') searchInput: ElementRef;
filterStatus: string = '';
filterType: string = '';
// ...
ngOnInit() {
// ...
// Filtration, bind to searchInput
fromEvent(this.searchInput.nativeElement, 'keyup')
.pipe(
debounceTime(150), // The user can type quite quickly in the input box, and that could trigger a lot of server requests. With this operator, we are limiting the amount of server requests emitted to a maximum of one every 150ms
distinctUntilChanged(), // This operator will eliminate duplicate values
tap(() => {
this.paginator.pageIndex = 0;
this.loadCustomersList();
})
)
.subscribe();
// ...
}
// ...
// ...
/** FILTRATION */
filterConfiguration(isGeneralSearch: boolean = true): any {
const filter: any = {};
const searchText: string = this.searchInput.nativeElement.value; // Read from input
if (this.filterStatus && this.filterStatus.length > 0) {
filter.status = +this.filterStatus; // Read from select
}
if (this.filterType && this.filterType.length > 0) {
filter.type = +this.filterType; // Read from select
}
filter.lastName = searchText;
if (!isGeneralSearch) { // Check for first loading
return filter;
}
filter.firstName = searchText;
filter.email = searchText;
filter.ipAddress = searchText;
return filter;
}
// ...
}
Data table Empty data Warning (in case of data absence):
Empty data Warning (in case of data absence) view in the ‘Customers list’ component:

customers/customer-list/customer-list.component.html
:
<!-- Message for empty data -->
<div class="mat-table__message" *ngIf="!dataSource.hasItems">No records found</div>
customers/customer-list/customer-list.component.html
:
//nothing is needed, hasItems is binded in BaseDataSource
Interceptor for HTTP Requests & Responses
By using HttpInteceptors we can cache, log, debug and catch errors during work with REST Api.
More information about HTTP Interceptors you can get from following article https://medium.com/@MetonymyQT/angular-http-interceptors-what-are-they-and-how-to-use-them-52e060321088
_core/utils/intercept.service.ts
:
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@Injectable()
export class InterceptService implements HttpInterceptor {
// intercept request and add token
intercept(
request: HttpRequest,
next: HttpHandler
): Observable> {
// modify request
request = request.clone({
setHeaders: {
Authorization: `Bearer ${localStorage.getItem('accessToken')}`
}
});
// console.log('----request----');
console.log(request);
// console.log('--- end of request---');
return next.handle(request).pipe(
tap(
event => {
if (event instanceof HttpResponse) {
// console.log('all looks good');
// http response status code
console.log(event.status);
}
},
error => {
// http response status code
// console.log('----response----');
// console.error('status code:');
console.error(error.status);
console.error(error.message);
// console.log('--- end of response---');
}
)
);
}
}
Now let's add our service to e-commerce.module.ts
e-commerce.module.ts
:
//...
providers: [
InterceptService,
{
provide: HTTP_INTERCEPTORS,
useClass: InterceptService,
multi: true
},
//..
]
//...
Actions
'Delete item' action


customers/customer-list/customer-list.component.ts
:
<m-portlet [options]="{headLarge: true}" [loading$]="dataSource.loading$">
<ng-container mPortletBody>
<div class="mat-table__wrapper">
<mat-table class="lmat-elevation-z8" [dataSource]="dataSource" matSort matSortActive="id" matSortDirection="asc" matSortDisableClear>
<!-- other columns -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
<mat-cell *matCellDef="let customer">
<button mat-icon-button color="warn"
matTooltip="Delete customer"
type="button"
(click)="deleteCustomer(customer)">
<mat-icon>delete</mat-icon>
</button>
<!-- other actions -->
</mat-cell>
</ng-container>
</mat-table>
</div>
</ng-container>
</m-portlet>
customers/customer-list/customer-list.component.html
:
// Importing Services
import { CustomersService } from '../../_core/services/customers.service';
import { LayoutUtilsService, MessageType } from '../../_core/utils/layout-utils.service';
import { TranslateService } from '@ngx-translate/core';
// ...
export class CustomersListComponent implements OnInit {
// ...
// Variables declaration
// ...
constructor(private customersService: CustomersService,
private layoutUtilsService: LayoutUtilsService,
private translate: TranslateService) {}
// ...
/** ACTIONS */
/** Delete */
deleteCustomer(_item: CustomerModel) {
// Translated messages
const _title: string = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_SIMPLE.TITLE');
const _description: string = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_SIMPLE.DESCRIPTION');
const _waitDesciption: string = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_SIMPLE.WAIT_DESCRIPTION');
const _deleteMessage = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_SIMPLE.MESSAGE');
// Confirmation Dialog
const dialogRef = this.layoutUtilsService.deleteElement(_title, _description, _waitDesciption);
dialogRef.afterClosed().subscribe(res => {
if (!res) {
return; // User canceled action
}
// Server call
this.customersService.deleteCustomer(_item.id).subscribe(() => {
this.layoutUtilsService.showActionNotification(_deleteMessage, MessageType.Delete);
this.loadCustomersList();
});
});
}
}
deleteCustomer
method calls customersService.deleteCustomer
method within itself.
_core/services/customers.service.ts
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, forkJoin, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { HttpUtilsService } from '../utils/http-utils.service';
import { CustomerModel } from '../models/customer.model';
import { QueryParamsModel } from '../models/query-models/query-params.model';
import { QueryResultsModel } from '../models/query-models/query-results.model';
const API_CUSTOMERS_URL = 'api/customers';
@Injectable()
export class CustomersService {
constructor(private http: HttpClient, private httpUtils: HttpUtilsService) { }
// DELETE => delete the customer from the server
deleteCustomer(customerId: number): Observable {
const url = `${API_CUSTOMERS_URL}/${customerId}`;
return this.http.delete(url);
}
// DELETE => delete selected customers from the server
deleteCustomers(ids: number[] = []) {
const url = this.API_CUSTOMERS_URL + '/delete';
return this.http.get(url, { params: ids });
}
}
'Delete selected items' action

customers/customer-list/customer-list.component.html
:
<m-portlet [options]="{headLarge: true}" [loading$]="dataSource.loading$">
<ng-container mPortletBody>
<!-- start::GROUP ACTIONS -->
<!-- Group actions list: 'Delete selected' | 'Fetch selected' | 'Update status for selected' -->
<!-- Group actions are shared for all LISTS | See '../../_shared' folder -->
<div class="row align-items-center collapse m-form__group-actions m--margin-top-20 m--margin-bottom-20"
[ngClass]="{'show' : selection.selected.length > 0}">
<!-- We show 'Group Actions' div if smth are selected -->
<div class="col-xl-12">
<div class="m-form__group m-form__group--inline">
<div class="m-form__label m-form__label-no-wrap">
<label class="m--font-bold m--font-danger-">
<span translate="ECOMMERCE.COMMON.SELECTED_RECORDS_COUNT"></span> {{ selection.selected.length }}
</label>
<!-- selectedCountsTitle => function from codeBehind (customer-list.component.ts file) -->
<!-- selectedCountsTitle => just returns title of selected items count -->
<!-- for example: Selected records count: 4 -->
</div>
<div class="m-form__control m-form__group--inline">
<button (click)="deleteCustomers()" mat-raised-button color="accent" matTooltip="Delete selected customers">
<mat-icon>delete</mat-icon> Delete All
</button> <!-- Call 'delete-entity-dialog' from _shared folder -->
</div>
</div>
</div>
</div>
<!-- end::GROUP ACTIONS -->
</ng-container>
</m-portlet>
customers/customer-list/customer-list.component.html
:
// Importing Services
import { CustomersService } from '../../_core/services/customers.service';
import { LayoutUtilsService, MessageType } from '../../_core/utils/layout-utils.service';
import { TranslateService } from '@ngx-translate/core';
// ...
export class CustomersListComponent implements OnInit {
// ...
// Variables declaration
// ...
constructor(private customersService: CustomersService,
private layoutUtilsService: LayoutUtilsService,
private translate: TranslateService) {}
// ...
/** ACTIONS */
deleteCustomers() {
const _title: string = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_MULTY.TITLE');
const _description: string = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_MULTY.DESCRIPTION');
const _waitDesciption: string = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_MULTY.WAIT_DESCRIPTION');
const _deleteMessage = this.translate.instant('ECOMMERCE.CUSTOMERS.DELETE_CUSTOMER_MULTY.MESSAGE');
const dialogRef = this.layoutUtilsService.deleteElement(_title, _description, _waitDesciption);
dialogRef.afterClosed().subscribe(res => {
if (!res) {
return;
}
const idsForDeletion: number[] = [];
for (let i = 0; i < this.selection.selected.length; i++) {
idsForDeletion.push(this.selection.selected[i].id);
}
// Server call
this.customersService.deleteCustomers(idsForDeletion)
.subscribe(() => {
this.layoutUtilsService.showActionNotification(_deleteMessage, MessageType.Delete);
this.loadCustomersList();
this.selection.clear();
});
});
}
}
'Create & Edit' item in modal>:
'Edit & Create' items in 'Customers list' component:

customers/customer-list/customer-list.component.html
:
<m-portlet [options]="{headLarge: true}" [loading$]="dataSource.loading$">
<ng-container mPortletHeadTools>
<button (click)="addCustomer()" mat-raised-button matTooltip="Create new customer" color="primary" type="button">
<span translate="ECOMMERCE.CUSTOMERS.NEW_CUSTOMER">New Customer</span>
</button>
<!-- Buttons (Material Angular) | See off.documenations 'https://material.angular.io/components/button/overview' -->
<!-- mat-raised-button | Rectangular contained button w/ elevation -->
</ng-container>
<!-- end::Header -->
<ng-container mPortletBody>
<div class="mat-table__wrapper">
<mat-table class="lmat-elevation-z8" [dataSource]="dataSource" matSort matSortActive="id" matSortDirection="asc" matSortDisableClear>
<!-- other columns -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
<mat-cell *matCellDef="let customer">
<button mat-icon-button color="primary"
matTooltip="Edit customer"
(click)="editCustomer(customer)">
<mat-icon>create</mat-icon>
</button>
<!-- other actions -->
</mat-cell>
</ng-container>
</mat-table>
</div>
</ng-container>
</m-portlet>
customers/customer-list/customer-list.component.ts
:
// Services
import { CustomersService } from '../../_core/services/customers.service';
import { LayoutUtilsService, MessageType } from '../../_core/utils/layout-utils.service';
// ...
export class CustomersListComponent implements OnInit {
// ...
// Variables declaration
constructor(
private customersService: CustomersService,
public dialog: MatDialog,
public snackBar: MatSnackBar,
private layoutUtilsService: LayoutUtilsService,
private translate: TranslateService
) {}
addCustomer() {
const newCustomer = new CustomerModel();
newCustomer.clear(); // Set all defaults fields
this.editCustomer(newCustomer);
}
/** Edit */
editCustomer(customer: CustomerModel) {
let saveMessageTranslateParam = 'ECOMMERCE.CUSTOMERS.EDIT.';
saveMessageTranslateParam += customer.id > 0 ? 'UPDATE_MESSAGE' : 'ADD_MESSAGE';
const _saveMessage = this.translate.instant(saveMessageTranslateParam);
const _messageType = customer.id > 0 ? MessageType.Update : MessageType.Create;
// Call 'Edit & Create' modal
const dialogRef = this.dialog.open(CustomerEditDialogComponent, { data: { customer } });
dialogRef.afterClosed().subscribe(res => {
if (!res) {
return; // The action was canceled by user
}
this.layoutUtilsService.showActionNotification(_saveMessage, _messageType, 10000, true, false);
this.loadCustomersList();
});
}
}


customers/customer-edit/customer-edit.dialog.component.html
:
<div class="m-portlet" [ngClass]="{ 'm-portlet--body-progress' : viewLoading, 'm-portlet--body-progress-overlay' : loadingAfterSubmit }">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon m--hide">
<i class="la la-gear"></i>
</span>
<h3 class="m-portlet__head-text">{{getTitle()}}</h3>
</div>
</div>
</div>
<form class="m-form" [formGroup]="customerForm">
<div class="m-portlet__body">
<div class="m-portlet__body-progress">
<mat-spinner [diameter]="20"></mat-spinner>
</div>
<m-alert *ngIf="hasFormErrors" type="warn" [duration]="30000" [showCloseButton]="true" (close)="onAlertClose($event)">
Oh snap! Change a few things up and try submitting again.
</m-alert>
<div class="form-group m-form__group row">
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<input matInput placeholder="Enter First Name" formControlName="firstName" />
<mat-error>First Name is
<strong>required</strong>
</mat-error>
<mat-hint align="start">Please enter
<strong>First Name</strong>
</mat-hint>
</mat-form-field>
</div>
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<input matInput placeholder="Enter Last Name" formControlName="lastName" />
<mat-error>Last Name is
<strong>required</strong>
</mat-error>
<mat-hint align="start">Please enter
<strong>Last Name</strong>
</mat-hint>
</mat-form-field>
</div>
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<input matInput placeholder="Enter Login" formControlName="userName" />
<mat-error>Login is
<strong>required</strong>
</mat-error>
<mat-hint align="start">Please enter
<strong>Login</strong>
</mat-hint>
</mat-form-field>
</div>
</div>
<div class="m-separator m-separator--dashed"></div>
<div class="form-group m-form__group row">
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<input type="email" matInput placeholder="Enter Email" formControlName="email" />
<mat-error>Email is
<strong>required</strong>
</mat-error>
<mat-hint align="start">Please enter
<strong>Email</strong>
</mat-hint>
</mat-form-field>
</div>
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<input matInput [matDatepicker]="picker" placeholder="Choose a Date of Birth" formControlName="dob" />
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
<mat-hint align="start">Please enter
<strong>Date of Birth</strong> in 'mm/dd/yyyy' format</mat-hint>
</mat-form-field>
</div>
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<input type="email" matInput placeholder="Enter IP Address" formControlName="ipAddress" />
<mat-error>IP Address
<strong>required</strong>
</mat-error>
<mat-hint align="start">We'll never share customer
<strong>IP Address</strong> with anyone else</mat-hint>
</mat-form-field>
</div>
</div>
<div class="m-separator m-separator--dashed"></div>
<div class="form-group m-form__group row">
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<mat-select placeholder="Gender" formControlName="gender">
<mat-option value="Female">Female</mat-option>
<mat-option value="Male">Male</mat-option>
</mat-select>
<mat-hint align="start">
<strong>Gender</strong>
</mat-hint>
</mat-form-field>
</div>
<div class="col-lg-4 m--margin-bottom-20-mobile">
<mat-form-field class="mat-form-field-fluid">
<mat-select placeholder="Type" formControlName="type">
<mat-option value="0">Business</mat-option>
<mat-option value="1">Individual</mat-option>
</mat-select>
<mat-hint align="start">
<strong>Account Type</strong>
</mat-hint>
</mat-form-field>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__no-border m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid">
<div class="row text-right">
<div class="col-lg-12">
<button type="button" mat-raised-button [mat-dialog-close]="data.animal" cdkFocusInitial matTooltip="Cancel changes">
Cancel
</button>
<button type="button" mat-raised-button color="primary" (click)="onSubmit()" [disabled]="viewLoading" matTooltip="Save changes">
Save
</button>
</div>
</div>
</div>
</div>
</form>
</div>
customers/customer-edit/customer-edit.dialog.component.ts
:
import { Component, OnInit, Inject, ChangeDetectionStrategy } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TypesUtilsService } from '../../_core/utils/types-utils.service';
import { CustomersService } from '../../_core/services/customers.service';
import { CustomerModel } from '../../_core/models/customer.model';
@Component({
selector: 'm-customers-edit-dialog',
templateUrl: './customer-edit.dialog.component.html',
// changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomerEditDialogComponent implements OnInit {
customer: CustomerModel;
customerForm: FormGroup;
hasFormErrors: boolean = false;
viewLoading: boolean = false;
loadingAfterSubmit: boolean = false;
constructor(public dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) public data: any,
private fb: FormBuilder,
private customerService: CustomersService,
private typesUtilsService: TypesUtilsService) { }
/** LOAD DATA */
ngOnInit() {
this.customer = this.data.customer;
this.createForm();
/* Server loading imitation. Remove this on real code */
this.viewLoading = true;
setTimeout(() => {
this.viewLoading = false;
}, 1000);
}
createForm() {
this.customer.dob = this.typesUtilsService.getDateFromString(this.customer.dateOfBbirth);
this.customerForm = this.fb.group({
firstName: [this.customer.firstName, Validators.required],
lastName: [this.customer.lastName, Validators.required],
email: [
this.customer.email,
[Validators.required, Validators.email]
],
dob: [this.customer.dob, Validators.nullValidator],
userName: [this.customer.userName, Validators.required],
gender: [this.customer.gender, Validators.required],
ipAddress: [this.customer.ipAddress, Validators.required],
type: [this.customer.type.toString(), Validators.required]
});
}
/** UI */
getTitle(): string {
if (this.customer.id > 0) {
return `Edit customer '${this.customer.firstName} ${this.customer.lastName}'`;
}
return 'New customer';
}
isControlInvalid(controlName: string): boolean {
const control = this.customerForm.controls[controlName];
const result = control.invalid && control.touched;
return result;
}
/** ACTIONS */
prepareCustomer(): CustomerModel {
const controls = this.customerForm.controls;
const _customer = new CustomerModel();
_customer.id = this.customer.id;
_customer.dateOfBbirth = this.typesUtilsService.dateCustomFormat(controls['dob'].value);
_customer.firstName = controls['firstName'].value;
_customer.lastName = controls['lastName'].value;
_customer.email = controls['email'].value;
_customer.userName = controls['userName'].value;
_customer.gender = controls['gender'].value;
_customer.ipAddress = controls['ipAddress'].value;
_customer.type = +controls['type'].value;
_customer.status = this.customer.status;
return _customer;
}
onSubmit() {
this.hasFormErrors = false;
this.loadingAfterSubmit = false;
const controls = this.customerForm.controls;
/** check form */
if (this.customerForm.invalid) {
Object.keys(controls).forEach(controlName =>
controls[controlName].markAsTouched()
);
this.hasFormErrors = true;
return;
}
const editedCustomer = this.prepareCustomer();
if (editedCustomer.id > 0) {
this.updateCustomer(editedCustomer);
} else {
this.createCustomer(editedCustomer);
}
}
updateCustomer(_customer: CustomerModel) {
this.loadingAfterSubmit = true;
this.viewLoading = true;
// Server call
this.customerService.updateCustomer(_customer).subscribe(res => {
this.viewLoading = false;
this.dialogRef.close({
_customer,
isEdit: true
});
});
}
createCustomer(_customer: CustomerModel) {
this.loadingAfterSubmit = true;
this.viewLoading = true;
// Server call
this.customerService.createCustomer(_customer).subscribe(res => {
this.viewLoading = false;
this.dialogRef.close({
_customer,
isEdit: false
});
});
}
onAlertClose($event) {
this.hasFormErrors = false;
}
}
_core/services/customers.service.ts
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, forkJoin, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { HttpUtilsService } from '../utils/http-utils.service';
import { CustomerModel } from '../models/customer.model';
import { QueryParamsModel } from '../models/query-models/query-params.model';
import { QueryResultsModel } from '../models/query-models/query-results.model';
const API_CUSTOMERS_URL = 'api/customers';
@Injectable()
export class CustomersService {
constructor(private http: HttpClient, private httpUtils: HttpUtilsService) { }
// CREATE => POST: add a new customer to the server
createCustomer(customer: CustomerModel): Observable {
return this.http.post(API_CUSTOMERS_URL, customer, this.httpUtils.getHTTPHeader());
}
// UPDATE => PUT: update the customer on the server
updateCustomer(customer: CustomerModel): Observable {
return this.http.put(API_CUSTOMERS_URL, customer, this.httpUtils.getHTTPHeader());
}
}