Muhi Logo Text
AboutBlogWork With Me

[Part 1] Create an Editable Dynamic Table using Angular Material

Learn how to create inline editable cells using dynamic schema with Angular Material.

Last updated on April 08, 2022

angular
angular material
Angular Editable Table

This series focuses on implementing new features to the Angular Material table component that enable users to add, edit and delete data with dynamic and flexible schema.

This part will explain a simple approach to creating a dynamic editable table where users can read and modify fields.

The aim is to provide the data source with the columns schema, and the table can adapt accordingly. For example, if one of the column types changed from a number to a date type, the table will respond dynamically and populate a date input field instead of a number field.

Let’s take a basic scenario:

  • Data source:
[
    {"name": "John Smith", "occupation": "Advisor", "age": 36},
    {"name": "Muhi Masri", "occupation": "Developer", "age": 28},
    {"name": "Peter Adams", "occupation": "HR", "age": 20},
    {"name": "Lora Bay", "occupation": "Marketing", "age": 43}
]
  • Columns schema:
[
    {
        key: "name",
        type: "text",
        label: "Full Name"
    },
    {
        key: "occupation",
        type: "text",
        label: "Occupation"
    },
    {
        key: "age",
        type: "text",
        label: "Age"
    }
]

When the Material table consumes the data source and columns schema, we’ll get the following results:

As you can see above, editing a row will change the field to a text input. Now let’s add a new “dateOfBirth” column with a date type and change the “age” column to a number type:

[
  {"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},
]
[
    {
        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"
    }
]

The table should now update and behave as follows:

As demonstrated above, and without any changes to the code, the same table was able to dynamically display the proper input field type based on the schema provided.

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

Setup an Angular app with Material UI

Create a new Angular app using Angular CLI and install the latest Angular Material.

ng new angular-editable-table
ng add @angular/material

Create a basic read-only Material table

In the app.component.ts file, let’s add a fake USER_DATA array (this typically comes from the back-end via an API call):

import { Component } from '@angular/core';

const USER_DATA = [
  {"name": "John Smith", "occupation": "Advisor", "age": 36},
  {"name": "Muhi Masri", "occupation": "Developer", "age": 28},
  {"name": "Peter Adams", "occupation": "HR", "age": 20},
  {"name": "Lora Bay", "occupation": "Marketing", "age": 43}
];

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  displayedColumns: string[] = ['name', 'occupation', 'age'];
  dataSource: any = USER_DATA;
}

Then, in the app.component.html file, we will add the required columns and display the list from the dataSource property:

<article>
  <table mat-table [dataSource]="dataSource">
    <ng-container [matColumnDef]="col" *ngFor="let col of displayedColumns">
      <th mat-header-cell *matHeaderCellDef>
        {{col}}
      </th>
      <td mat-cell *matCellDef="let element">
        {{element[col]}}
      </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</article>

Create a dynamic table using columns schema

Now that the basic table is working let’s go through the steps for adding dynamic input fields based on columns schema. For now, we’re going to replace text fields with HTML input elements, but we’ll add an edit functionality to toggle between read-only and edit mode in the next section.

  1. Loop through the columns and display them dynamically instead of hard coding each one.
<article>
  <table mat-table [dataSource]="dataSource">
    <ng-container [matColumnDef]="col" *ngFor="let col of displayedColumns">
      <th mat-header-cell *matHeaderCellDef>
        {{col}}
      </th>
      <td mat-cell *matCellDef="let element">
        {{element[col]}}
      </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</article>
  1. Define the columns schema with key, type and label:
import { Component } from '@angular/core';

const USER_DATA = [
  {"name": "John Smith", "occupation": "Advisor", "age": 36},
  {"name": "Muhi Masri", "occupation": "Developer", "age": 28},
  {"name": "Peter Adams", "occupation": "HR", "age": 20},
  {"name": "Lora Bay", "occupation": "Marketing", "age": 43}
];

const COLUMNS_SCHEMA = [
  {
      key: "name",
      type: "text",
      label: "Full Name"
  },
  {
      key: "occupation",
      type: "text",
      label: "Occupation"
  },
  {
      key: "age",
      type: "number",
      label: "Age"
  }
]

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

displayColumns used to have a static list of column keys but now we changed it to read directly from COLUMNS_SCHEMA using the map function.

  1. Update the template to dynamically add input fields based on the column type
<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">
        <mat-form-field>
          <input [type]="col.type" matInput [(ngModel)]="element[col.key]">
        </mat-form-field>
      </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
</article>

We’re now looping through columnsSchema instead of the displayColumns array as it has the new required properties (key, type and label).

Using col.type, we can conveniently pass the input type (text, number, date…) to the HTML input element.

Enable inline row editing

In a real-life scenario, the user would want to have a read-only mode by default and edit a row when required.

To achieve this, we first need to add an edit button for each row. That will require adding a new column in the columnsSchema list. Let’s name it isEdit for now, but it could be anything as long as it does not conflict with other column names.

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

isEdit column has an empty label as it will not require a title in this scenario.

Now let’s add a condition in the template to toggle between read-only and edit mode based on the isEdit value.

<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 *ngIf="element.isEdit">
        <div class="btn-edit" *ngIf="col.key === 'isEdit'; else dataField">
          <button mat-button (click)="element.isEdit = !element.isEdit">Done</button>
        </div>
        <ng-template #dataField>
          <mat-form-field>
            <input
              [type]="col.type"
              matInput
              [(ngModel)]="element[col.key]"
            />
          </mat-form-field>
        </ng-template>
      </div>
    </td>
  </ng-container>
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>

By default, the isEdit value is undefined (or false). However, when clicking the button, the value is added to the element’s object as true.

Support other data types

What we just implemented supports basic HTML types such as input number, text…

One approach to supporting custom elements is simply adding new conditions in the Angular template. For example, let’s assume we want to add a Material date element instead of the built-in HTML one. We can add ngSwitch statements to support date and other custom types:

<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>

To test the new data type, we need to change the USER_DATA and COLUMNS_SCHEMA to include a date field:

[
  {"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},
]
[
    {
        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: ""
    }
]

Summary

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

  • Setup an Angular Material project
  • Populate data using Material Table component
  • Create dynamic columns
  • Enable inline row editing

In the next part, we are going to learn how to add and remove table rows.

You can access the complete repository here.

Bye for now 👋

If you enjoyed this post, I regularly share similar content on Twitter. Follow me @muhimasri to stay up to date, or feel free to message me on Twitter if you have any questions or comments. I'm always open to discussing the topics I write about!

Recommended Reading

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

angular
angular material

Discussion

Upskill Your Frontend Development Techniques 🌟

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

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

© 2024, Muhi Masri