Angular inline editing and validation with material table and template driven form

  Hey guys, in our angular application, mostly we only display the data in a material table but, nowadays, there are many users who want to edit data in a table itself instead of going on a separate page or popup. So as an angular developer we must have to provide them a way to do that, and while adding edit functionality in a table we also need to tack care of validation as well.
   In this post, we are going to see how to do inline editing and validations with angular's material table and we are going to use a template-driven form for the same. Let’s do it step by step.


[If you have any problem related to the Angular material table please check our following posts:
Click here for a simple material table with searching, sorting, and pagination,
Click here for multiple expandable rows in the angular material table,
Click here for add/remove columns dynamically in the angular material table,
Click here for a table inside a row and multiple paginators in the angular material table,
Click here for adding custom and common filter/search in the angular material table]


Now come to the main part, here are the extended files from our previous example.
In the HTML part, we have created a table with basic functionality like searching, pagination, and sorting. For inline editing, we have added text boxes and ngModels instead of interpolation. And for validation purposes, we have added form and given name dynamically to each text box, so we can have separate validation of each row.

home.component.html
<div class="body">
    <div class="row">
        <div class="align-items-center col-lg-12 btn-outline-light">
            Material Table Demo
       </div><br><br>
    </div>
    <form #testForm="ngForm" (ngSubmit)="testForm.form.valid" novalidate>
        <div class="row">
            <div class="col-lg-2"></div>
            <div class="col-lg-8 center">
                <div class="spinner-container"   
                    *ngIf="userListMatTabDataSource.loading$ | async">
                    <mat-spinner></mat-spinner>
                </div>
                <div class="card">
                    <div style="display: inline;" class="col-lg-12">
                        <label for="Table">Demo Table</label>
                        <button (click)="addRow(selectedRow)"
                                style="float:right"
                                class="btn btn-sm btn-primary top-margin">
                           Add
                        </button>
                        <mat-form-field style="float:right">
                            <input matInput            
                                   (keyup)="applyFilter($event.target.value)"
                                   placeholder="search for">
                        </mat-form-field>
                    </div>
                    <mat-table class="mat-elevation-z8"  
                               [dataSource]="userListMatTabDataSource"
                               matSort matSortActive="userName"
                               matSortDirection="asc" matSortDisableClear>
                        <ng-container matColumnDef="action">
                        <mat-header-cell *matHeaderCellDef>Action
                        </mat-header-cell>
                        <mat-cell *matCellDef="let user; let i = index;">
                          <i class="fas fa-pencil" id="{{i}}"  
                            (click)="edited(user)">edit</i>
                            </mat-cell>
                        </ng-container>
                       
                  <ng-container matColumnDef="userName">
                      <mat-header-cell *matHeaderCellDef mat-sort-header>
                             User Name
                      </mat-header-cell>
                      <mat-cell *matCellDef="let user; let i = index;">
                           <div class="form-group"
                                [class.text-danger]="userName.invalid">
                           <input required type="text" #userName="ngModel"   
                                  class="form-control"
                                  name="userName-{{i}}"  
                                  [(ngModel)]="user.userName"
                                  [disabled]="!(user.edited || user.id ==  
undefined)">
                                    <i class="help-block"  
                                       *ngIf="userName.invalid">
                                        please enter name
                                    </i>
                                </div>
                            </mat-cell>
                        </ng-container>
                        <ng-container matColumnDef="email">
                            <mat-header-cell *matHeaderCellDef mat-sort-header>
                                 Email
                            </mat-header-cell>
                            <mat-cell *matCellDef="let user; let i = index;">
                                <div class="form-group"
                                     [class.text-danger]="email.invalid">
                                     <input type="text"  #email="ngModel"  
                                            class="form-control"
                                            name="email-{{i}}"   
                                        [(ngModel)]="user.email" email required
                             [disabled]="!(user.edited || user.id == undefined)">
                                      <div *ngIf="email.invalid"
                                     class="has-error control-label help-block">
                                      <div *ngIf="email.errors.email">
                                             email is not valid
                                      </div>
                        <div *ngIf="email.errors.required">
                             email is  required
                          </div>
                    </div>
                </div>
             </mat-cell>
          </ng-container>
          <ng-container matColumnDef="contactNo">
               <mat-header-cell *matHeaderCellDef mat-sort-header>
                  Contact No
               </mat-header-cell>
               <mat-cell *matCellDef="let user; let i = index;">
                   <div class="form-group"
                        [class.text-danger]="contactNo.invalid">
                       <input type="number" class="form-control"  
                              #contactNo="ngModel"
                              name="contactNo{{i}}"
                              [(ngModel)]="user.contactNo"
                              [disabled]="!(user.edited || user.id == undefined)"
                              number required pattern="[0-9]{10}">
                           <div *ngIf="contactNo.invalid"
                                 class="has-error control-label help-block">
                            <div *ngIf="contactNo.errors.number">
                                 Contact No is not valid
                            </div>
                            <div *ngIf="contactNo.errors.required">
                                  Contact No is required
                            </div>
                            <div *ngIf="contactNo.errors.pattern">
                                 invalid Number
                            </div>
                        </div>
                      </div>
                 </mat-cell>
              </ng-container>
              <ng-container matColumnDef="address">
                  <mat-header-cell *matHeaderCellDef mat-sort-header>
                          Address
                   </mat-header-cell>
                   <mat-cell *matCellDef="let user; let i = index;">
                      <div class="form-group"
                           [class.text-danger]="address.invalid">
                         <input required type="text" #address="ngModel"  
                                class="form-control"
                                            name="address-{{i}}" class="form-control" [(ngModel)]="user.address"
                                            [disabled]="!(user.edited || user.id == undefined)">
                                            <i class="control-label help-block" *ngIf="address.invalid">
                                                please enter Address
                                            </i>
                                    </div>
                            </mat-cell>
                        </ng-container>
                        <mat-header-row *matHeaderRowDef="columnList"></mat-header-row>
                        <mat-row *matRowDef="let row; columns: columnList; let i = index;" [class.selected]="selectedRow == row.id"
                            [class.edited]="row.edited || row.id == undefined" (click)="rowClick(row.id)">
                        </mat-row>
                    </mat-table>
                    <mat-paginator [pageSizeOptions]="[3, 5, 10]" showFirstLastButtons></mat-paginator>
                    <div class="col-lg-12"><input class="btn btn-sm btn-primary pull-right"   type="submit" (click)="save();"></div>
                </div>
            </div>
        </div>
      
    </form>
</div>

Here is explanation of one field, as we said above, if we don’t use editing in the table, we can use simple interpolation to display the content in the table. But now we are going to edit the content, so in the table, we have inserted input type and while editing, we have used HTML 5 validations with angular, so added validations like required, pattern and validated it by angular.

<div class="form-group"
                        [class.text-danger]="contactNo.invalid">
                       <input type="number" class="form-control"  
                              #contactNo="ngModel"
                              name="contactNo{{i}}"
                              [(ngModel)]="user.contactNo"
                              [disabled]="!(user.edited || user.id == undefined)"
                              number required pattern="[0-9]{10}">
                           <div *ngIf="contactNo.invalid"
                                 class="has-error control-label help-block">
                            <div *ngIf="contactNo.errors.number">
                                 Contact No is not valid
                            </div>
                            <div *ngIf="contactNo.errors.required">
                                  Contact No is required
                            </div>
                            <div *ngIf="contactNo.errors.pattern">
                                 invalid Number
                            </div>
                        </div>
                      </div>

Here we have added required and pattern validator of HTML 5 and validated those using template reference also here we have added some logic to enable disable editing ([disabled]="!(user.edited || user.id == undefined)")


Here in the typescript file, we have done all basic steps to bind data to the material table and assign pagination, sorting, and searching. What additional things we have done here are, on the add button we pushed the new object in the array/data source of the material table to insert a new row in the material table. Managed edited data separately so we can process it as per our need and managed table row selection as well.

home.component.ts
import { Component, OnInit, Input, ViewChild } from '@angular/core';
import { User } from '../user/user';
import { MatPaginator, MatTableDataSource, MatSort } from '@angular/material';
import { environment } from 'src/environments/environment';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-homepage',
  templateUrl: './homepage.component.html',
  styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {

  userList: User[];
  selectedRow: Number;
  editedRows: Boolean[];
  editedData: User[];
  private columnList = ["action", "userName", "email", "contactNo", "address"];
  userListMatTabDataSource = new MatTableDataSource<User>(this.userList);

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;
  @ViewChild('testForm') testForm: NgForm;

  constructor() { }

  ngOnInit() {
    console.log(environment.test);

    this.editedRows = [];
    this.editedData = [];
    this.getAlldata();
    console.log(this.userList);
    this.userListMatTabDataSource.paginator = this.paginator;
    this.userListMatTabDataSource.sort = this.sort;
  }

  applyFilter(filterValue: string) {
    this.userListMatTabDataSource.filter = filterValue.trim().toLowerCase();
  }

  getAlldata() {
    this.userList = [
      { id: 1, userName: "Ramesh", password: "rebqwtye", email: "ramesh@gmail.com", contactNo: "9788235466", address: "123 RG Road, XYCity", edited: undefined },
      { id: 2, userName: "Suresh", password: "rebqwtye", email: "suresh@gmail.com", contactNo: "9788235466", address: "123 RG Road, XYCity", edited: undefined },
      { id: 3, userName: "Ganesh", password: "rebqwtye", email: "ganesh@gmail.com", contactNo: "9788235466", address: "123 RG Road, XYCity", edited: undefined },
      { id: 4, userName: "Bhavesh", password: "rebqwtye", email: "bhavesh@gmail.com", contactNo: "9788235466", address: "123 RG Road, XYCity", edited: undefined },
      { id: 5, userName: "Bhavesh", password: "rebqwtye", email: "bhavesh@gmail.com", contactNo: "9788235466", address: "123 RG Road, XYCity", edited: undefined },
      { id: 6, userName: "Bhavesh", password: "rebqwtye", email: "bhavesh@gmail.com", contactNo: "9788235466", address: "123 RG Road, XYCity", edited: undefined },
      { id: 7, userName: "Bhavesh", password: "rebqwtye", email: "bhavesh@gmail.com", contactNo: "9788235466", address: "123 RG Road, XYCity", edited: undefined }
    ];
    this.userListMatTabDataSource.data = this.userList;
  }

  rowClick(rowId) {
    this.selectedRow = rowId;
  }

  edited(user) {
    user.edited = true;
    this.editedData.push(user);
  }

  addRow(index) {
    let index2 = this.userList.indexOf(this.userListMatTabDataSource.filteredData[index]);
    let tempUser = new User();
    this.userList.splice(index2, 0, tempUser);
    this.editedData.push(tempUser);
    this.userListMatTabDataSource.data = [];
    this.userListMatTabDataSource.data = this.userList;
  }

  save() {
    if ((this.testForm.touched || this.editedData.length > 0) && !this.testForm.valid) {
      console.log("push this array to server this.editedData" + this.editedData);
      // this.editedData <- push on server
      alert('not valid');
    } else {
      alert("data submitted");
      this.getAlldata();
    }
  }

}


Following is the code to manage row selection and edited users.

rowClick(rowId) {
    this.selectedRow = rowId;
  }

  edited(user) {
    user.edited = true;
    this.editedData.push(user);
  }


To add a new row, we pushed a new user’s object into an array.
And while submitting the form checked for a valid form

addRow(index) {
    let index2 = this.userList.indexOf(this.userListMatTabDataSource.filteredData[index]);
    let tempUser = new User();
    this.userList.splice(index2, 0, tempUser);
    this.editedData.push(tempUser);
    this.userListMatTabDataSource.data = [];
    this.userListMatTabDataSource.data = this.userList;
  }

  save() {
    if ((this.testForm.touched || this.editedData.length > 0) && !this.testForm.valid) {
      console.log("push this array to server this.editedData" + this.editedData);
      // this.editedData <- push on server
      alert('not valid');
    } else {
      alert("data submitted");
      this.getAlldata();
    }
  }

This is the module file to let you know the dependencies and imports required for this project

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FirstPageComponent } from './first-page/first-page.component';
import { SecondPageComponent } from './second-page/second-page.component';
import { FormsModule } from '@angular/forms';
import { HomepageComponent } from './homepage/homepage.component';
import { MatTableModule, MatPaginatorModule, MatProgressSpinnerModule, MatFormFieldModule, MatInputModule, MatSortModule } from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
declarations: [
AppComponent,
FirstPageComponent,
SecondPageComponent,
HomepageComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
MatTableModule,
MatPaginatorModule,
MatProgressSpinnerModule,
MatInputModule,
MatFormFieldModule,
BrowserAnimationsModule,
MatSortModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

output

All done.
I hope you like the post.
Here is the demo project, click on the below link to download.



Comments

  1. I like your post very much. It is very much useful for my research. I hope you to share more info about this. Keep posting angularjs online training india

    ReplyDelete
  2. I have a problem with editable fields, I saw that for editable fileds with input if I use <form tag I saw always the same value, which can be the problem ?

    ReplyDelete
    Replies
    1. when you are dealing with form, you can either set name field or you can mark that field standalone.
      you can check here
      https://angularsixeasily.blogspot.com/2019/01/multiple-drop-downs-not-working-with.html

      Delete
  3. onClick of submit after edit, the edited data is not impacting in the table , can you please help me on this

    ReplyDelete
    Replies
    1. In Save(), instead of calling getAllData(), I replace this.userListMatTabDataSource.data = this.userList; ,
      Is it correct?

      Delete
  4. that's actually come to my use. Thank you for sharing.

    ReplyDelete
  5. Template validation in table has bothered me for a long time, thanks so much for sharing this!

    ReplyDelete
  6. Thanks for Sharing This Article.It is very so much valuable content. I hope these Commenting lists will help to my website
    angular js online training
    best angular js online training
    top angular js online training

    ReplyDelete
  7. Thanks for Sharing This Article.It is very so much valuable content. I hope these Commenting lists will help to my website
    angular js online training
    best angular js online training
    top angular js online training

    ReplyDelete
  8. Why you don't have a live demo? to be more easier to follow the code with no install locally needed....

    ReplyDelete
  9. Hello Vikasratna! I find inline edit risky as there can be numerous alternate scenarios you need to handle if the user starts triggering multiple row updates and the design would start wobbling due to the inline fields. Alternatively, I have designed a table which will focus on one row at a time using a drawer because using dialogs for your form will detach the form and the table and logically the form should be a part of the table. So, its better not to use dialogs for preserving context.

    I have made a guide for the same. You can find it here -
    https://owrrpon.medium.com/editable-an-editable-table-using-angular-material-7a4c4210bb71

    ReplyDelete

Post a Comment