Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Build Angular data-table with CRUD Operations and Advanced Column Filtering

5.00/5 (4 votes)
12 Feb 2021CPOL5 min read 44.7K   852  
An Angular application that includes crud operations, column filtering, form dialog, confirm dialog and behavior subject
In this article, we build a web application based on Angular framework, Angular Material, Reactive forms and Observables.

 

Introduction

The main goals of this article are to learn how to:

  • manipulate mat-table through crud operations
  • add column filtering on mat-table
  • create a form dialog
  • create a confirm dialog
  • use BehaviorSubject

To achieve these goals, we will create an application for persons management.

Prerequisites

To understand this article well, you should have some knowledge about Angular, JavaScript/Typescript , HTML and CSS.

Before we start, we have to setup Angular environment. To do that, I recommend you to visit the Angular official documentation.

Create a New Application and Setup Angular Material

For this demo, I have used Angular version 9.

  1. First, we need to create a new Angular application by running the following command line:
    JavaScript
    ng new angular-datatable
  2. Next, install Angular material to have a nice design for your UI components by running this command line:
    JavaScript
    ng add @angular/material
  3. Declare all needed Angular material components modules inside app.module.ts:
    JavaScript
     imports: [
      BrowserModule,
      BrowserAnimationsModule,
    
      CdkTableModule,
      MatTableModule,
      MatPaginatorModule,
      MatSortModule,
      MatMenuModule,
      MatIconModule,
      MatButtonModule,
      MatDialogModule,
      ReactiveFormsModule,
      MatInputModule,
      MatSelectModule
    ],
    

Create Model

  1. First, we have to define an entity class named Person. It contains the following properties:
    • Id: unique identifier
    • FirstName: the first name
    • Age
    • Job: the job name that person can have like: Dentist, Software developer...
    C#
      export class Person {
    
      id?: number;
      firstName: string;
      age: number;
      job: string;
    
      constructor(id: number = null, 
                  firstName: string = '', age: number = 0, job: string = '') {
        this.id = id;
        this.firstName = firstName;
        this.age = age;
        this.job = job;
      }
    }
  2. Then, we need to declare an array of persons for our project which works only on the client side. This data is like a local data store.
    JavaScript
    import { Person } from "../models/person";
    
    export const personsData: Person[] = [
      new Person(1, 'person 1', 30, 'Software Developer'),
      new Person(2, 'person 2', 33, 'Dentist'),
      new Person(3, 'person 3', 32, 'Physician Assistant'),
      new Person(4, 'person 4', 33, 'Software Developer'),
      new Person(5, 'person 5', 34, 'Software Developer'),
      new Person(6, 'person 6', 25, 'Nurse'),
      new Person(7, 'person 7', 36, 'Software Developer'),
      new Person(8, 'person 8', 27, 'Physician'),
      new Person(9, 'person 9', 28, 'Software Developer'),
      new Person(10, 'person 10', 28, 'Software Developer')
    ]

Implement Crud Operations

To manage the data store of persons, we need to create an Angular service for this purpose.

ng generate service person.service

This service contains:

  • persons$: Type of BehaviorSubject<Person[]>, this kind of observables used to push received messages to all subscribers. In our example, we use it to refresh data-table after a CRUD operation
  • persons: Contains a copy of our data store, it’s updated after each CRUD operations
  • getAll(): Returns a list of available persons
  • edit(person: Person): Replaces some properties of an existing entity and refreshes the displayed list
  • remove(id: number): Delete an existing entity from the data store and refresh the displayed entries of data-table
JavaScript
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { personsData } from '../constants/persons-static-data';
import { Person } from '../models/person';

@Injectable({
  providedIn: 'root'
})

export class PersonService {

  persons$: BehaviorSubject<Person[]>;
  persons: Array<Person> = [];

  constructor() {
    this.persons$ = new BehaviorSubject([]);
    this.persons = personsData;
  }

  getAll() {
    this.persons$.next(this.persons);
  }

  add(person: Person) {
    this.persons.push(person);
  }

  edit(person: Person) {
    let findElem = this.persons.find(p => p.id == person.id);
    findElem.firstName = person.firstName;
    findElem.age = person.age;
    findElem.job = person.job;
    this.persons$.next(this.persons);
  }

  remove(id: number) {
   
    this.persons = this.persons.filter(p => {
      return p.id != id
    });

    this.persons$.next(this.persons);
  }
}

Display Data

  1. Create DataTableComponent by running the following command line inside app/components folder:
    ng g c data-table

    This component is the main component, it contains the data-table that displays and manages the persons list and offers the possibility to filter by column using a custom filter which will be implemented later. To read more about mat-table, you can visit this link.

  2. Next, we need to prepare the HTML template by editing data-table.component.html:
    HTML
    <div class="mat-elevation-z8">
      <table mat-table [dataSource]="dataSource" matSort class="mat-elevation-z8">
        <ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column">
          <th mat-header-cell *matHeaderCellDef>
            <div style="display: flex; align-items: center;">
              <span mat-sort-header>{{column}}</span>
             
            </div>
          </th>
          <td mat-cell *matCellDef="let element"> {{element[column]}} </td>
        </ng-container>
        <ng-container [matColumnDef]="'actions'">
          <th mat-header-cell *matHeaderCellDef> actions </th>
          <td mat-cell *matCellDef="let element">
            <button mat-icon-button (click)="edit(element)">
              <mat-icon mat-icon-button color='primary'>edit</mat-icon>
            </button>
            <button mat-icon-button (click)="delete(element['id'])">
              <mat-icon mat-icon-button color="warn">delete</mat-icon>
            </button>
          </td>
        </ng-container>
        <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
        <tr mat-row *matRowDef="let row; columns: columnsToDisplay;"></tr>
      </table>
    
      <mat-paginator [pageSize]="5" 
      [pageSizeOptions]="[5, 10, 50]" showFirstLastButtons></mat-paginator>
    
    </div>
  3. Then, we should do the implementation part by editing the data-table.component.ts:
    JavaScript
    import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
    import { Subscription } from 'rxjs';
    
    import { MatPaginator } from '@angular/material/paginator';
    import { MatTableDataSource } from '@angular/material/table';
    import { MatSort } from '@angular/material/sort';
    import { MatDialog } from '@angular/material/dialog';
    import { ConfirmationDialogComponent } _
             from '../confirmation-dialog/confirmation-dialog.component';
    import { PersonFormDialogComponent } _
             from '../person-form-dialog/person-form-dialog.component';
    import { PersonService } from 'src/app/core/services/person.service';
    import { Person } from 'src/app/core/models/person';
    
    @Component({
      selector: 'app-data-table',
      templateUrl: './data-table.component.html',
      styleUrls: ['./data-table.component.scss']
    })
    export class DataTableComponent implements OnInit, OnDestroy, AfterViewInit {
      @ViewChild(MatPaginator) paginator: MatPaginator;
      @ViewChild(MatSort) sort: MatSort;
    
      public displayedColumns: string[] = ['firstName', 'age', 'job'];
      public columnsToDisplay: string[] = [...this.displayedColumns, 'actions'];
    
      /**
       * it holds a list of active filter for each column.
       * example: {firstName: {contains: 'person 1'}}
       *
       */
      public columnsFilters = {};
    
      public dataSource: MatTableDataSource<person>;
      private serviceSubscribe: Subscription;
    
      constructor(private personsService: PersonService, public dialog: MatDialog) {
        this.dataSource = new MatTableDataSource<person>();
      }   
    
      edit(data: Person) {
        const dialogRef = this.dialog.open(PersonFormDialogComponent, {
          width: '400px',
          data: data
        });
    
        dialogRef.afterClosed().subscribe(result => {
          if (result) {
            this.personsService.edit(result);
          }
        });
      }
    
      delete(id: any) {
        const dialogRef = this.dialog.open(ConfirmationDialogComponent);
    
        dialogRef.afterClosed().subscribe(result => {
          if (result) {
            this.personsService.remove(id);
          }
        });
      }
    
      ngAfterViewInit(): void {
        this.dataSource.paginator = this.paginator;
        this.dataSource.sort = this.sort;
      }
    
      /**
       * initialize data-table by providing persons list to the dataSource.
       */
      ngOnInit(): void {
        this.personsService.getAll();
        this.serviceSubscribe = this.personsService.persons$.subscribe(res => {
          this.dataSource.data = res;
        })
      }
    
      ngOnDestroy(): void {
        this.serviceSubscribe.unsubscribe();
      }
    }
    
    </person></person>

    To load data into mat-table, we need to get a list from persons service. To do that, we need to call getAll method of persons service and subscribe to persons$ observable.
    We also prepare empty methods for delete and edit actions.

Delete an Existing Person

  1. Create ConfirmationDialogComponent by running the following command line inside app/components folder:
    JavaScript
    ng g c confirmation-dialog

    This component is useful to show confirm action dialog for users who want to make a critical action like delete operation.

    To show this component on dialog, we use the MatDialog service.
    The MatDialog service is in charge of showing dialog, passing data to dialog and configuring it.

    When this dialog is open, user will get two choices:

    • Yes: It confirms the action by returning true to afterClosed observable and refreshes data store by calling delete method of person service.

    • No: It rejects the action by returning false to afterClosed observable.

  2. Next, we need to edit template file of confirmation-dialog.component.html:
    HTML
    <h1 mat-dialog-title>Confirm action</h1>
    <div mat-dialog-content>Are you sure to want remove this item ?</div>
    <div mat-dialog-actions class="mt-15">
      <button mat-raised-button color="primary" 
      [mat-dialog-close]="true" cdkFocusInitial>Yes</button>
      <button mat-raised-button mat-dialog-close>No</button>
    </div>
  3. Declare this component as an entryComponent in app.module.ts:
    JavaScript
    entryComponents: [ConfirmationDialogComponent]
    
  4. Implement delete action of data-table.component.ts:
    JavaScript
    delete(id: any) {
      const dialogRef = this.dialog.open(ConfirmationDialogComponent);
    
      dialogRef.afterClosed().subscribe(result => {
        if (result) {
          this.personsService.remove(id);
        }
      });
    }
    
  5. Finally, when you run this application, you should be able to delete an existing user after confirmation action.

Update an Existing Person

  1. Create PersonFormDialogComponent by running the following command line inside app/components folder:
    JavaScript
    ng g c person-form-dialog

    This component displays selected person data into a form dialog and the user is able to introduce some changes on his properties.
    To create this form, we use a Reactive Form approach to have a deep control of form state and an efficient way for input validation.

    When a user clicks on the edit icon from one of the datatable rows, the selected person will be injected to form a dialog component by using the MatDialog service and MAT_DIALOG_DATA.
    If the form is valid, the user can save changes and the result will be passed to afterClosed observable to be treated by edit method of persons service.
    For our example, we suppose that all form controls are mandatory fields otherwise user can’t be able to save change.

  2. Next, we need to build our template person-form-dialog.component.html:
    HTML
    <h1 mat-dialog-title>Edit Person</h1>
    
    <div mat-dialog-content>
      <form [formGroup]="formInstance">
        <div>
          <mat-form-field class="fullWidth" appearance="outline">
            <mat-label>first Name *</mat-label>
            <input matInput type="text" 
            name="firstName" formControlName="firstName">
            <mat-error *ngIf="
            formInstance.controls['firstName']?.errors?.required">field required</mat-error>
          </mat-form-field>
        </div>
    
        <div class="mt-5">
          <mat-form-field class="fullWidth" appearance="outline">
            <mat-label>Age *</mat-label>
            <input matInput type="number" name="age" formControlName="age" />
            <mat-error *ngIf="
            formInstance.controls['age']?.errors?.required">field required</mat-error>
          </mat-form-field>
        </div>
    
        <div class="mt-5">
    
          <mat-form-field class="fullWidth" appearance="outline">
            <mat-label>Job *</mat-label>
            <mat-select name="job" formControlName="job">
              <mat-option value="Software Developer">Software Developer</mat-option>
              <mat-option value="Physician">Physician</mat-option>
              <mat-option value="Dentist">Dentist</mat-option>
              <mat-option value="Nurse">Nurse</mat-option>
            </mat-select>
            <mat-error *ngIf="formInstance.controls['job']?.errors?.required">field required
            </mat-error>
          </mat-form-field>
    
        </div>
      </form>
    </div>
    
    <div class="mt-5" mat-dialog-actions>
      <button mat-raised-button color="primary" 
      [disabled]="formInstance.dirty && formInstance.errors" (click)="save()"
        cdkFocusInitial>Yes</button>
      <button mat-raised-button mat-dialog-close>No</button>
    
    </div>
  3. Edit the person-form-dialog.component.ts:
    import { Component, Inject, OnInit } from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
    import { Person } from 'src/app/core/models/person';
    
    @Component({
      selector: 'app-form-dialog',
      templateUrl: './person-form-dialog.component.html',
      styleUrls: ['./person-form-dialog.component.scss']
    })
    
    export class PersonFormDialogComponent implements OnInit {
      formInstance: FormGroup;
    
      constructor(public dialogRef: MatDialogRef<PersonFormDialogComponent>,
        @Inject(MAT_DIALOG_DATA) public data: Person) {
        this.formInstance = new FormGroup({
          "id":  new FormControl('', Validators.required),
          "firstName": new FormControl('', Validators.required),
          "age": new FormControl('', Validators.required),
          "job": new FormControl('', Validators.required),
        });
    
        this.formInstance.setValue(data);
      }
    
      ngOnInit(): void {
    
      }
    
      save(): void {
        this.dialogRef.close(Object.assign(new Person(), this.formInstance.value));
      }
    }
  4. Implement edit method of data-table.component.ts:
    JavaScript
    edit(data: Person) {
      const dialogRef = this.dialog.open(PersonFormDialogComponent, {
        width: '400px',
        data: data
      });
    
      dialogRef.afterClosed().subscribe(result => {
        if (result) {
          this.personsService.edit(result);
        }
      });
    }
    
  5. Finally, we should get this result when we run the application:

Implement Filtering Column

The idea is to filter data by searching some values on a specific column.
Our text filter is additive, we can combine several search criteria from different columns.
For our example, we will implement these string comparison methods which are case-insensitive:

  • contains: data should contain a substring of searched value
  • equals: data should be equal to searched value
  • greater than: data should be greater than the searched value
  • less than: data must be less than the search value
  • end with: data must end with search value
  • start with: data must start with search value

Each column can hold only one type of filter at the same time.

To do that, we should:

  1. Modify data-table.component.html to add drop-down filter:

    This filter is an angular mat-menu that contains a list of available filtering operations, an input text filter and buttons to clear filter from column or to add a new one.

    HTML
    <div class="mat-elevation-z8">
      <table mat-table [dataSource]="dataSource" matSort class="mat-elevation-z8">
        <ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column">
          <th mat-header-cell *matHeaderCellDef>
            <div style="display: flex; align-items: center;">
              <span mat-sort-header>{{column}}</span>
              <button mat-icon-button>
                <mat-icon mat-icon-button color="primary" [matMenuTriggerFor]="menu"
                  [matMenuTriggerData]="{columnName: column}">filter_list </mat-icon>
              </button>
            </div>
          </th>
          <td mat-cell *matCellDef="let element"> {{element[column]}} </td>
        </ng-container>
    
        <ng-container [matColumnDef]="'actions'">
          <th mat-header-cell *matHeaderCellDef> actions </th>
          <td mat-cell *matCellDef="let element">
            <button mat-icon-button (click)="edit(element)">
              <mat-icon mat-icon-button color='primary'>edit</mat-icon>
            </button>
    
            <button mat-icon-button (click)="delete(element['id'])">
              <mat-icon mat-icon-button color="warn">delete</mat-icon>
            </button>
          </td>
        </ng-container>
        <tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
        <tr mat-row *matRowDef="let row; columns: columnsToDisplay;"></tr>
      </table>
      <mat-paginator [pageSize]="5" 
      [pageSizeOptions]="[5, 10, 50]" showFirstLastButtons></mat-paginator>
    </div>
    
    <!-- menu for column filtering-->
    
    <mat-menu #menu="matMenu" class="matMenu">
      <ng-template matMenuContent let-dataColumnName="columnName">
        <div class="flex-column" (click)="$event.stopPropagation();">
          <div class="mb-5">
            <mat-form-field class="fullWidth" appearance="outline">
              <mat-label>Choose a filter *</mat-label>
              <mat-select #selectedOperationFilter [value]="'contains'">
                <mat-option value="contains" select>Contains</mat-option>
                <mat-option value="equals">Equals</mat-option>
                <mat-option value="greaterThan">Greater than</mat-option>
                <mat-option value="lessThan">Less than</mat-option>
                <mat-option value="endWith">End with</mat-option>
                <mat-option value="startWith">Start With</mat-option>
              </mat-select>
            </mat-form-field>
    
          </div>
    
          <div class="mb-5 fullWidth">
            <mat-form-field class="fullWidth" appearance="outline">
              <mat-label>write a value*</mat-label>
              <input matInput #searchValue type="text">
            </mat-form-field>
          </div>
    
          <div class="fullWidth flex-row mb-5 flex-justify-space-between">
            <button [disabled]="!searchValue.value" mat-raised-button color="primary"
              class="flex-row flex-align-center btn-filter-action"
              (click)="applyFilter(dataColumnName, 
                       selectedOperationFilter.value,  searchValue.value)">
    
              <mat-icon>check</mat-icon>
              <label>filter</label>
            </button>
    
            <button mat-raised-button 
             class="flex-row flex-align-center btn-filter-action" color="warn"
              (click)="clearFilter(dataColumnName)">
              <mat-icon>clear</mat-icon>
              <label>reset</label>
            </button>
          </div>
        </div>
      </ng-template>
    </mat-menu>
  2. Implement actions related to the filter in data-table.component.ts:
    JavaScript
    private filter() {
    
      this.dataSource.filterPredicate = (data: Person, filter: string) => {
    
        let find = true;
    
        for (var columnName in this.columnsFilters) {
    
          let currentData = "" + data[columnName];
    
          //if there is no filter, jump to next loop, otherwise do the filter.
          if (!this.columnsFilters[columnName]) {
            return;
          }
    
          let searchValue = this.columnsFilters[columnName]["contains"];
    
          if (!!searchValue && currentData.indexOf("" + searchValue) < 0) {
    
            find = false;
            //exit loop
            return;
          }
    
          searchValue = this.columnsFilters[columnName]["equals"];
    
          if (!!searchValue && currentData != searchValue) {
            find = false;
            //exit loop
            return;
          }
    
          searchValue = this.columnsFilters[columnName]["greaterThan"];
    
          if (!!searchValue && currentData <= searchValue) {
            find = false;
            //exit loop
            return;
          }
    
          searchValue = this.columnsFilters[columnName]["lessThan"];
    
          if (!!searchValue && currentData >= searchValue) {
            find = false;
            //exit loop
            return;
          }
    
          searchValue = this.columnsFilters[columnName]["startWith"];
    
          if (!!searchValue && !currentData.startsWith("" + searchValue)) {
            find = false;
            //exit loop
            return;
          }
    
          searchValue = this.columnsFilters[columnName]["endWith"];
    
          if (!!searchValue && !currentData.endsWith("" + searchValue)) {
            find = false;
            //exit loop
            return;
          }
        }
        return find;
    
      };
    
      this.dataSource.filter = null;
      this.dataSource.filter = 'activate';
    
      if (this.dataSource.paginator) {
        this.dataSource.paginator.firstPage();
      }
    }
    
    /**
    
     * Create a filter for the column name and operate the filter action.
    
     */
    
    applyFilter(columnName: string, operationType: string, searchValue: string) {
    
      this.columnsFilters[columnName] = {};
      this.columnsFilters[columnName][operationType] = searchValue;
      this.filter();
    }
    
    /**
    
     * clear all associated filters for column name.
    
     */
    
    clearFilter(columnName: string) {
      if (this.columnsFilters[columnName]) {
        delete this.columnsFilters[columnName];
        this.filter();
      }
    }
    
  3. The end result should be like this:

Run Application

Try to download source code, and do the following steps:

  • Extract the source code, and navigate to the folder path using CMD commands.
  • Download npm packages by running npm install.
  • Run application by running npm start.

References

Points of Interest

I hope you appreciated this article. Thank you for viewing my post, try to download the source code and do not hesitate to leave your questions and comments.

History

  • v1 19th December, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)