This part will focus on adding input field validations for table rows; we’ll leverage the built-in HTML form validation to identify input validity.
Before we begin, the complete repository is accessible here, and a working example is available below:
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.
Required validation
The required
attribute is a built-in HTML input validation, and it specifies that an input field must be filled out before submitting the form. Given that matInput
already has a style rule for a required input field, it should turn red if the user leaves it empty.
Let’s add it to the input element in app.component.html
and look at the results:
<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 *ngSwitchCase="'date'">
{{ element[col.key] | date: 'mediumDate' }}
</span>
<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)="editRow(element)"
[disabled]="valid[element.id] === false"
>
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
required
[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>
As demonstrated above, not specifying a value in the input field will change it to red, indicating that there is an error.
There is a slight problem with that approach; since we added the required attribute to all columns, every input field is validated even though it might not be required. Hence, the validation must come from the user model where we define the columns, which will ensure to validate only the specified fields.
Let’s modify user.ts
and add a new validation definition only for the firstName
column:
export interface User {
isSelected: boolean;
id: number;
firstName: string;
lastName: string;
email: string;
birthDate: string;
isEdit: boolean;
}
export const UserColumns = [
{
key: 'isSelected',
type: 'isSelected',
label: '',
},
{
key: 'firstName',
type: 'text',
label: 'First Name',
required: true,
},
{
key: 'lastName',
type: 'text',
label: 'Last Name',
},
{
key: 'email',
type: 'email',
label: 'Email',
},
{
key: 'birthDate',
type: 'date',
label: 'Date of Birth',
},
{
key: 'isEdit',
type: 'isEdit',
label: '',
},
];
Now in the template, we can pass the required
attribute the value from UserColumns
to determine whether it’s required or not:
<mat-form-field class="form-input" *ngSwitchDefault>
<input
[required]="col.required"
[type]="col.type"
matInput
[(ngModel)]="element[col.key]"
/>
</mat-form-field>
Below we can see how firstName
is the only column being validated:
Pattern Validation
Another type of validation is the pattern attribute that specifies a regular expression where the input value is checked against. That’s especially useful if the value entered by the user has to contain certain characters or strings.
A good use case is the email address field. For example, we can add a pattern validation to UserColumns
that requires an email to have ’@’ character:
export const UserColumns = [
{
key: 'isSelected',
type: 'isSelected',
label: '',
},
{
key: 'firstName',
type: 'text',
label: 'First Name',
required: true
},
{
key: 'lastName',
type: 'text',
label: 'Last Name',
},
{
key: 'email',
type: 'email',
label: 'Email',
required: true,
pattern: '.+@.+',
},
{
key: 'birthDate',
type: 'date',
label: 'Date of Birth',
},
{
key: 'isEdit',
type: 'isEdit',
label: '',
},
];
Then, similar to the previous approach, the pattern
property will be passed as an attribute value to the input field:
<mat-form-field class="form-input" *ngSwitchDefault>
<input
[required]="col.required"
[pattern]="col.pattern"
[type]="col.type"
matInput
[(ngModel)]="element[col.key]"
/>
</mat-form-field>
Now we should have the email input field validating if it doesn’t match the specified criteria:
Enable/disable row submit
So far, we’ve validated input fields and showed the red error from Material input. But that doesn’t stop the user from submitting the row with invalid values. So we need a way to keep track of invalid rows and fields to disable the submit (“Done”) button and prevent users from proceeding with invalid changes.
Since we have ids for every row, we can create a key/value object representing the id as key and the validated fields as value. For example:
{
"1": {
"firstName": false,
"lastName": true
},
"2": {
"email": false
}
}
The idea is to start with an empty object and dynamically add the validated fields while input changes. Let’s go through the required steps:
- Add an input
change
listener in the template and pass it the event, id, and column key:
<mat-form-field class="form-input" *ngSwitchDefault>
<input
matInput
[required]="col.required"
[pattern]="col.pattern"
[type]="col.type"
[(ngModel)]="element[col.key]"
(change)="inputHandler($event, element.id, col.key)"
/>
</mat-form-field>
- Create a new empty
valid
object and aninputHandler
function in the component class. The function will use the validity value from the event’s target object to attach it to the field:
valid: any = {};
inputHandler(e: any, id: number, key: string) {
if (!this.valid[id]) {
this.valid[id] = {};
}
this.valid[id][key] = e.target.validity.valid;
}
The key
parameter represents the current modified field (e.g. firstName). Also, we need first to check if the id
does not exist in the object so we can instantiate a value and avoid an uncaught reference error.
- Create a function that conditionally disables the submit button based on whether or not the row id has any invalid values:
disableSubmit(id: number) {
if (this.valid[id]) {
return Object.values(this.valid[id]).some((item) => item === false);
}
return false;
}
The function above converts the valid
object with the specified id to a list of values and checks if there is any invalid field.
Finally, we need to attach that function to the disable
attribute in the template:
<div class="btn-edit" *ngSwitchCase="'isEdit'">
<button
mat-button
(click)="editRow(element)"
[disabled]="disableSubmit(element.id)"
>
Done
</button>
</div>
Below we can see how the button is disabled when the field is invalid:
Here is the entire template and component class we did for this section:
import { Component, DefaultIterableDiffer, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { User, UserColumns } from './model/user';
import { UserService } from './services/user.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
displayedColumns: string[] = UserColumns.map((col) => col.key);
columnsSchema: any = UserColumns;
dataSource = new MatTableDataSource<User>();
valid: any = {};
constructor(public dialog: MatDialog, private userService: UserService) {}
ngOnInit() {
this.userService.getUsers().subscribe((res: any) => {
this.dataSource.data = res;
});
}
editRow(row: User) {
if (row.id === 0) {
this.userService.addUser(row).subscribe((newUser: User) => {
row.id = newUser.id;
row.isEdit = false;
});
} else {
this.userService.updateUser(row).subscribe(() => (row.isEdit = false));
}
}
addRow() {
const newRow: User = {
id: 0,
firstName: '',
lastName: '',
email: '',
birthDate: '',
isEdit: true,
isSelected: false,
};
this.dataSource.data = [newRow, ...this.dataSource.data];
}
removeRow(id: number) {
this.userService.deleteUser(id).subscribe(() => {
this.dataSource.data = this.dataSource.data.filter(
(u: User) => u.id !== id
);
});
}
removeSelectedRows() {
const users = this.dataSource.data.filter((u: User) => u.isSelected);
this.dialog
.open(ConfirmDialogComponent)
.afterClosed()
.subscribe((confirm) => {
if (confirm) {
this.userService.deleteUsers(users).subscribe(() => {
this.dataSource.data = this.dataSource.data.filter(
(u: User) => !u.isSelected
);
});
}
});
}
inputHandler(e: any, id: number, key: string) {
if (!this.valid[id]) {
this.valid[id] = {};
}
this.valid[id][key] = e.target.validity.valid;
}
disableSubmit(id: number) {
if (this.valid[id]) {
return Object.values(this.valid[id]).some((item) => item === false);
}
return false;
}
}
<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 *ngSwitchCase="'date'">
{{ element[col.key] | date: 'mediumDate' }}
</span>
<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)="editRow(element)"
[disabled]="disableSubmit(element.id)"
>
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
matInput
[required]="col.required"
[pattern]="col.pattern"
[type]="col.type"
[(ngModel)]="element[col.key]"
(change)="inputHandler($event, element.id, 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>
Summary
If you made it to this section, pat yourself on the back! You just learned how to:
- Add input field validation for table rows
- Leverage built-in HTML form validations for adding custom validations
- Define dynamic validations through the column schema
You can access the complete repository here.
Bye for now 👋