Search code examples
javascriptangularangular7angular-reactive-formsangular-material-7

mat table with dynamically adding rows and editable fields in a row with validatons


I have just started working on the Angular reactive forms and i was trying to build a table which is inside a form.

The table has add new feature by clicking it new empty row will be inserted in the table. the existing rows will be in edit mode by default and they are validated. The table data will be saved with a single save button which is out of the table but inside the form. I tried the below code

constructor(private router: Router, private fb: FormBuilder) { }
  columnsToDisplay: string[];
  dataList;
  copyDataList;
  rows: FormArray = this.fb.array([]);
  formGroup: FormGroup = this.fb.group({ actualsVolumeData: this.rows });

  ngOnInit() {
    this.columnsToDisplay = ['id', 'code', 'desc'];

    this.formGroup = this.fb.group({
      columns: this.columnsToDisplay,
    });
    this.copyDataList = [];
    this.dataList = [];
    let list = [
      {
        code: 'one',
        desc: 'One1',
        id: 1
      },
      {
        code: 'two',
        desc: 'Two1',
        id: 2
      },
      {
        code: 'three',
        desc: 'Three1',
        id: 3
      },
    ];
    this.copyDataList = new MatTableDataSource(list);
    this.dataList = new MatTableDataSource(list);
  }

  onAdd() {
    let newRow = {
      id: this.dataList.data.length + 1,
      code: undefined,
      desc: undefined
    }
    this.copyDataList.data.push(newRow);
    this.dataList = new MatTableDataSource(this.copyDataList.data);
  }

  onSubmit() {

  }
<form [formGroup]=`formGroup`>
  <button mat-button (click)=`onAdd()`>Add</button>
  <table mat-table [dataSource]=`dataList` [formArrayName]=`actualsVolumeData` class=`mat-elevation-z8`>

    <ng-container matColumnDef=`id`>
      <th mat-header-cell *matHeaderCellDef> ID </th>
      <td mat-cell *matCellDef=`let element, let i = index` [formGroupName]=`i`> {{element.id}} </td>
    </ng-container>

    <ng-container matColumnDef=`code`>
      <th mat-header-cell *matHeaderCellDef> Code </th>
      <td mat-cell *matCellDef=`let element, let i = index` [formGroupName]=`i`> {{element.code}}
        <mat-form-field>
          <input matInput formControlName='code'>
        </mat-form-field>
      </td>
    </ng-container>

    <ng-container matColumnDef=`desc`>
      <th mat-header-cell *matHeaderCellDef> Description </th>
      <td mat-cell *matCellDef=`let element, let i = index` [formGroupName]=`i`> {{element.desc}}
        <mat-form-field>
          <input matInput formControlName=`desc`>
        </mat-form-field>
      </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef=`columnsToDisplay`></tr>
    <tr mat-row *matRowDef=`let row; columns: columnsToDisplay;`></tr>
  </table>
<button mat-button (click)=`formGroup.valid && onSubmit()`>submit</button>
</form>

but am getting this error this.validator is not a function


Solution

  • How make a formArray from data

    Imagine you has a data like

    [
     {code: 'one',desc: 'One1',id: 1},
     {code: 'two',desc: 'Two1',id: 2},
     {code: 'three',desc: 'Three1',id: 3}
    ]
    

    To create a formArray it's useful has a function that, received an object and return a FormGroup

    createFormGroup(data):FormGroup
    {
       data=data|| {code:'',desc:'',id:0}
       return new FormGroup({
           code:new FormControl(data.code,Validators.required),
           desc:new FormControl(data.desc,Validators.required),
           id:new FormControl(data.id,Validators.required)
       })
    }
    

    if we call to the function with an object return a formGroup, if we call the function with null, return also a FormGroup with the elements empty

    When you has the data you can do simple

    this.myFormArray=new FormArray(this.data.map(x=>this.createFormGroup(x)))
    

    That's each element of data convert to a formGroup, the formArray will be an array with this elements. map transform each element "x" in "this.createFormGroup(x)"

    If you has a service that return the data you subscribe

    this.myService.getData().subscribe(res=>{
         this.myFormArray=new FormArray(res.map(x=>x.this.createFormGroup(x)))
    })
    

    //your service has a method like

    getData()
    {
         return this.httpClient("http:myUrl")
    }
    

    The good of this aproach is that to add a new element to the FormArray you only need make

    this.formArray.push(this.createFormGroup(null)) 
    

    To remove

    this.formArray.removeAt(index)
    

    In stackblitz has a little example of all this. Well, in service I use the rxjs operator of normally was a httpClient.get(url). Only I put the part of create the formArray,

    NOTE: I use the contructor of FormGroup and the constructor of FormArray, but you can use BuilderForm like

    createFormGroup(data):FormGroup
    {
       data=data|| {code:'',desc:'',id:0}
       return this.fb.group({
           code:[data.code,Validators.required],
           desc:[data.desc,Validators.required],
           id:[data.id,Validators.required]
       })
    }
    

    And use

    myFormArray=this.fb.array(res.map(x=>x.this.createFormGroup(x)))