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