Muhi Masri

[Part 2] Add and Remove Table Rows using Angular Material

Learn how to add and remove multiple rows with a confirmation dialog using Angular Material

Last updated on April 08, 2022

angular
angular material
Angular Editable Table

In the previous part, we did a deep dive into editing a table and creating a dynamic schema to generate different form elements for the user input such as text, number, date…

In this part, we will go through the required steps to add and remove multiple rows with a confirmation dialog. The final results will look as follows:

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:

Add a new row

Adding a new row is as simple as inserting a new object to the dataSource array.

A quick recap from the previous tutorial, dataSource is the primary data list used in the table element. We assign it directly to the USER_DATA array, which is just static dummy data. That will come from the back-end via an API call in a real-life scenario.

Let’s go through the steps required:

  1. Add a new button with a click event just before the table component. We will wrap it in an article container to be able to add more action buttons in later steps:
<article class="table-header">
  <button class="button-add-row" mat-button (click)="addRow()">Add Row</button>
</article>
<table mat-table [dataSource]="dataSource">
  <ng-container [matColumnDef]="col.key" *ngFor="let col of columnsSchema">
    <th mat-header-cell *matHeaderCellDef>
      {{ col.label }}
    </th>
    <td mat-cell *matCellDef="let element">
      <div [ngSwitch]="col.type" *ngIf="!element.isEdit">
        <div class="btn-edit" *ngSwitchCase="'isEdit'">
          <button mat-button (click)="element.isEdit = !element.isEdit">
            Edit
          </button>
        </div>
        <span *ngSwitchDefault>
          {{ element[col.key] }}
        </span>
      </div>
      <div [ngSwitch]="col.type" *ngIf="element.isEdit">
        <div class="btn-edit" *ngSwitchCase="'isEdit'">
          <button mat-button (click)="element.isEdit = !element.isEdit">Done</button>
        </div>
        <mat-form-field *ngSwitchCase="'date'" appearance="fill">
          <mat-label>Choose a date</mat-label>
          <input matInput [matDatepicker]="picker" [(ngModel)]="element[col.key]" />
          <mat-datepicker-toggle
            matSuffix
            [for]="picker"
          ></mat-datepicker-toggle>
          <mat-datepicker #picker></mat-datepicker>
        </mat-form-field>
        <mat-form-field *ngSwitchDefault>
          <input [type]="col.type" matInput [(ngModel)]="element[col.key]" />
        </mat-form-field>
      </div>
    </td>
  </ng-container>
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
  1. Implement addRow function to insert a new object into the dataSource array with empty values. Make sure it contains the isEdit property that is set to true to make it editable for the user:
import { Component } from '@angular/core';

const USER_DATA = [
  {"name": "John Smith", "occupation": "Advisor", "dateOfBirth": "1984-05-05", "age": 36},
  {"name": "Muhi Masri", "occupation": "Developer", "dateOfBirth": "1992-02-02", "age": 28},
  {"name": "Peter Adams", "occupation": "HR", "dateOfBirth": "2000-01-01", "age": 20},
  {"name": "Lora Bay", "occupation": "Marketing", "dateOfBirth": "1977-03-03", "age": 43},
]

const COLUMNS_SCHEMA = [
  {
      key: "name",
      type: "text",
      label: "Full Name"
  },
  {
      key: "occupation",
      type: "text",
      label: "Occupation"
  },
  {
    key: "dateOfBirth",
    type: "date",
    label: "Date of Birth"
},
  {
      key: "age",
      type: "number",
      label: "Age"
  },
  {
    key: "isEdit",
    type: "isEdit",
    label: ""
  }
]

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

  addRow() {
    const newRow = {"name": "", "occupation": "", "dateOfBirth": "", "age": 0, isEdit: true}
    this.dataSource = [...this.dataSource, newRow];
  }
}

For Angular to trigger the OnPush change detection, we need to clone the array. That’s why we are using the spread operator on the dataSource.

In some instances, the table can be long with many rows, and adding a new entry at the bottom will require the user to scroll down every time. We can solve this matter by simply swapping the entries in the new array we just created:

this.dataSource = [newRow, ...this.dataSource];

Here is a demo to how the table behaves so far:

Remove a row

As with adding a new row, all we need to do is remove the object from the sourceData array but with a slight change to the model. Let’s take a closer look:

  1. Deleting an object requires a unique id so let’s add a new one for every object. In a real-world scenario, ids will come from the backend:
const USER_DATA = [
  {
    id: 1,
    name: 'John Smith',
    occupation: 'Advisor',
    dateOfBirth: '1984-05-05',
    age: 36,
  },
  {
    id: 2,
    name: 'Muhi Masri',
    occupation: 'Developer',
    dateOfBirth: '1992-02-02',
    age: 28,
  },
  {
    id: 3,
    name: 'Peter Adams',
    occupation: 'HR',
    dateOfBirth: '2000-01-01',
    age: 20,
  },
  {
    id: 4,
    name: 'Lora Bay',
    occupation: 'Marketing',
    dateOfBirth: '1977-03-03',
    age: 43,
  },
];
  1. Add a delete button with a click event in every row, right next to the edit button:
<article class="table-header">
  <button class="button-add-row" mat-button (click)="addRow()">Add Row</button>
</article>
<table mat-table [dataSource]="dataSource">
  <ng-container [matColumnDef]="col.key" *ngFor="let col of columnsSchema">
    <th mat-header-cell *matHeaderCellDef>
      {{ col.label }}
    </th>
    <td mat-cell *matCellDef="let element">
      <div [ngSwitch]="col.type" *ngIf="!element.isEdit">
        <div class="btn-edit" *ngSwitchCase="'isEdit'">
          <button mat-button (click)="element.isEdit = !element.isEdit">
            Edit
          </button>
          <button
            mat-button
            class="button-remove"
            (click)="removeRow(element.id)"
          >
            Delete
          </button>
        </div>
        <span *ngSwitchDefault>
          {{ element[col.key] }}
        </span>
      </div>
      <div [ngSwitch]="col.type" *ngIf="element.isEdit">
        <div class="btn-edit" *ngSwitchCase="'isEdit'">
          <button mat-button (click)="element.isEdit = !element.isEdit">
            Done
          </button>
        </div>
        <mat-form-field
          class="form-input"
          *ngSwitchCase="'date'"
          appearance="fill"
        >
          <mat-label>Choose a date</mat-label>
          <input
            matInput
            [matDatepicker]="picker"
            [(ngModel)]="element[col.key]"
          />
          <mat-datepicker-toggle
            matSuffix
            [for]="picker"
          ></mat-datepicker-toggle>
          <mat-datepicker #picker></mat-datepicker>
        </mat-form-field>
        <mat-form-field class="form-input" *ngSwitchDefault>
          <input [type]="col.type" matInput [(ngModel)]="element[col.key]" />
        </mat-form-field>
      </div>
    </td>
  </ng-container>
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
  1. Implement removeRow function that filters the dataSource array to take out the object we need to remove:
import { Component } from '@angular/core';

const USER_DATA = [
  {
    id: 1,
    name: 'John Smith',
    occupation: 'Advisor',
    dateOfBirth: '1984-05-05',
    age: 36,
  },
  {
    id: 2,
    name: 'Muhi Masri',
    occupation: 'Developer',
    dateOfBirth: '1992-02-02',
    age: 28,
  },
  {
    id: 3,
    name: 'Peter Adams',
    occupation: 'HR',
    dateOfBirth: '2000-01-01',
    age: 20,
  },
  {
    id: 4,
    name: 'Lora Bay',
    occupation: 'Marketing',
    dateOfBirth: '1977-03-03',
    age: 43,
  },
];

const COLUMNS_SCHEMA = [
  {
    key: 'name',
    type: 'text',
    label: 'Full Name',
  },
  {
    key: 'occupation',
    type: 'text',
    label: 'Occupation',
  },
  {
    key: 'dateOfBirth',
    type: 'date',
    label: 'Date of Birth',
  },
  {
    key: 'age',
    type: 'number',
    label: 'Age',
  },
  {
    key: 'isEdit',
    type: 'isEdit',
    label: '',
  },
];

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

  addRow() {
    const newRow = {
      id: Date.now(),
      name: '',
      occupation: '',
      dateOfBirth: '',
      age: 0,
      isEdit: true,
    };
    this.dataSource = [newRow, ...this.dataSource];
  }

  removeRow(id: number) {
    this.dataSource = this.dataSource.filter((u) => u.id !== id);
  }
}

We also modified the addRow function to generate a random id so that the remove works on new rows.

Here is a demo of the remove functionality we just did:

Remove multiple rows

This section will introduce a new feature to support removing multiple rows. That requires a slight change to the model to keep track of the selected rows. Let’s go through the necessary steps:

  1. Add a new isSelected column to the COLUMNS_SCHEMA. That will help us determine which rows are selected:
const COLUMNS_SCHEMA = [
  {
    key: 'isSelected',
    type: 'isSelected',
    label: '',
  },
  {
    key: 'name',
    type: 'text',
    label: 'Full Name',
  },
  {
    key: 'occupation',
    type: 'text',
    label: 'Occupation',
  },
  {
    key: 'dateOfBirth',
    type: 'date',
    label: 'Date of Birth',
  },
  {
    key: 'age',
    type: 'number',
    label: 'Age',
  },
  {
    key: 'isEdit',
    type: 'isEdit',
    label: '',
  }
];
  1. Add a checkbox element as a new column only within the none-edit mode section because we don’t need it to appear while editing the row

  2. Define a change event to insert the isSelected property into the row dynamically. $event.checked determines the value(true or false)

<div [ngSwitch]="col.type" *ngIf="!element.isEdit">
  <ng-container *ngSwitchCase="'isSelected'">
    <mat-checkbox (change)="element.isSelected = $event.checked"></mat-checkbox>
  </ng-container>
  <div class="btn-edit" *ngSwitchCase="'isEdit'">
    <button mat-button (click)="element.isEdit = !element.isEdit">Edit</button>
    <button mat-button class="button-remove" (click)="removeRow(element.id)">
      Delete
    </button>
  </div>
  <span *ngSwitchDefault>
    {{ element[col.key] }}
  </span>
</div>
  1. Add a new remove button with a click event. Let’s place it right next to the add button created earlier:
<article class="table-header">
  <button class="button-remove-rows" mat-button (click)="removeSelectedRows()">
    Remove Rows
  </button>
  <button class="button-add-row" mat-button (click)="addRow()">Add Row</button>
</article>
  1. Create a new removeSelectedRows function that filters and returns the non-selected rows from the sourceData array:
removeSelectedRows() {
  this.dataSource = this.dataSource.filter((u: any) => !u.isSelected);
}

Let’s clarify a bit more; when the user selects a row, the object will attach isSelected: true to the object as follows:

{id: 1, "name": "John Smith", "occupation": "Advisor", "dateOfBirth": "1984-05-05", "age": 36, isSelected: true}

Then we filter the dataSource by isSelected values to remove them and return a new array.

Let’s demonstrate what we have so far:

Confirmation dialog

If the remove rows function triggers an API call to delete selected rows, it’s good to add a confirmation dialog before proceeding. That requires a few simple steps:

  1. Create a new ConfirmDialog component. We can use the CLI command:
ng generate component confirm-dialog
  1. Add the required template for the confirmation dialog. In our case, it’s just a simple confirm/cancel dialog:
<h1 mat-dialog-title>Are you sure you want to delete?</h1>
<div mat-dialog-actions>
    <button mat-button [mat-dialog-close]>Cancel</button>
    <button class="delete-button" mat-button [mat-dialog-close]="true">Delete</button>
</div>
  1. In app.component.ts, subscribe to the afterClosed event and remove all the selected rows if the action was “Yes” otherwise just close the dialog:
 removeSelectedRows() {
    this.dialog
      .open(ConfirmDialogComponent)
      .afterClosed()
      .subscribe((confirm) => {
        if (confirm) {
          this.dataSource = this.dataSource.filter((u: any) => !u.isSelected);
        }
      });
  }

We don’t need to add any code to the confirm-dialog.component.ts file because the cancel/delete buttons in the confirm dialog template contain a mat-dialog-close directive that automatically closes the dialog for us, and that’s all that we need for now.

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

Below is the complete HTML template and component class code:

<article class="table-header">
  <button class="button-remove-rows" mat-button (click)="removeSelectedRows()">
    Remove Rows
  </button>
  <button class="button-add-row" mat-button (click)="addRow()">Add Row</button>
</article>
<table mat-table [dataSource]="dataSource">
  <ng-container [matColumnDef]="col.key" *ngFor="let col of columnsSchema">
    <th mat-header-cell *matHeaderCellDef>
      {{ col.label }}
    </th>
    <td mat-cell *matCellDef="let element">
      <div [ngSwitch]="col.type" *ngIf="!element.isEdit">
        <ng-container *ngSwitchCase="'isSelected'">
          <mat-checkbox
            (change)="element.isSelected = $event.checked"
          ></mat-checkbox>
        </ng-container>
        <div class="btn-edit" *ngSwitchCase="'isEdit'">
          <button mat-button (click)="element.isEdit = !element.isEdit">
            Edit
          </button>
          <button
            mat-button
            class="button-remove"
            (click)="removeRow(element.id)"
          >
            Delete
          </button>
        </div>
        <span *ngSwitchDefault>
          {{ element[col.key] }}
        </span>
      </div>
      <div [ngSwitch]="col.type" *ngIf="element.isEdit">
        <div *ngSwitchCase="'isSelected'"></div>
        <div class="btn-edit" *ngSwitchCase="'isEdit'">
          <button mat-button (click)="element.isEdit = !element.isEdit">
            Done
          </button>
        </div>
        <mat-form-field
          class="form-input"
          *ngSwitchCase="'date'"
          appearance="fill"
        >
          <mat-label>Choose a date</mat-label>
          <input
            matInput
            [matDatepicker]="picker"
            [(ngModel)]="element[col.key]"
          />
          <mat-datepicker-toggle
            matSuffix
            [for]="picker"
          ></mat-datepicker-toggle>
          <mat-datepicker #picker></mat-datepicker>
        </mat-form-field>
        <mat-form-field class="form-input" *ngSwitchDefault>
          <input [type]="col.type" matInput [(ngModel)]="element[col.key]" />
        </mat-form-field>
      </div>
    </td>
  </ng-container>
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';

const USER_DATA = [
  {
    id: 1,
    name: 'John Smith',
    occupation: 'Advisor',
    dateOfBirth: '1984-05-05',
    age: 36,
  },
  {
    id: 2,
    name: 'Muhi Masri',
    occupation: 'Developer',
    dateOfBirth: '1992-02-02',
    age: 28,
  },
  {
    id: 3,
    name: 'Peter Adams',
    occupation: 'HR',
    dateOfBirth: '2000-01-01',
    age: 20,
  },
  {
    id: 4,
    name: 'Lora Bay',
    occupation: 'Marketing',
    dateOfBirth: '1977-03-03',
    age: 43,
  },
];

const COLUMNS_SCHEMA = [
  {
    key: 'isSelected',
    type: 'isSelected',
    label: '',
  },
  {
    key: 'name',
    type: 'text',
    label: 'Full Name',
  },
  {
    key: 'occupation',
    type: 'text',
    label: 'Occupation',
  },
  {
    key: 'dateOfBirth',
    type: 'date',
    label: 'Date of Birth',
  },
  {
    key: 'age',
    type: 'number',
    label: 'Age',
  },
  {
    key: 'isEdit',
    type: 'isEdit',
    label: '',
  },
];

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

  constructor(public dialog: MatDialog) {}

  addRow() {
    const newRow = {
      id: Date.now(),
      name: '',
      occupation: '',
      dateOfBirth: '',
      age: 0,
      isEdit: true,
    };
    this.dataSource = [newRow, ...this.dataSource];
  }

  removeRow(id: number) {
    this.dataSource = this.dataSource.filter((u) => u.id !== id);
  }

  removeSelectedRows() {
    this.dialog
      .open(ConfirmDialogComponent)
      .afterClosed()
      .subscribe((confirm) => {
        if (confirm) {
          this.dataSource = this.dataSource.filter((u: any) => !u.isSelected);
        }
      });
  }
}

Summary

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

  • Add and remove table rows with Angular Material
  • Enable checkbox column for multi-row selection
  • Add confirmation dialog before removing rows

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