Muhi Masri

[Part 4] Validate Table Rows and Fields with Angular Material

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

Last updated on April 08, 2022

angular
angular material
Angular Editable Table

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>

Validate Angular 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:

Validate Angular Table

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:

Validate Angular Table

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:

  1. 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>
  1. Create a new empty valid object and an inputHandler 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.

  1. 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:

Validate Angular Table

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. 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 load, add, update and delete table rows with API services using Angular Material

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