Search code examples
angulartypescriptangular-materialangular-reactive-formsangular-validation

Angular FormArray Input Reactive Form in Mat Table: required Validator works perfectly, but @rxweb/reactive-form-validators somehow not


The validator works perfectly with required. But I need the unique validator, to check if the labels has duplicate names.

When I switch to unique in inventory.component.ts the table isn't even showing up correctly.
inventory.component.ts:

this.formBuilder.control(item['inventoryName'], RxwebValidators.unique())
// this.formBuilder.control(item['inventoryName'], Validators.required)

https://stackblitz.com/edit/stackblitz-starters-bj8b6n?file=src%2Fapp%2Finventory%2Finventory.component.ts

I noticed, that all other examples on stackoverflow and docs are using RxwebValidators.unique() with an extra key.

    addSkill(){
        let skillsArray = <FormArray>this.employeeFormGroup.controls.skills;
        skillsArray.push(this.getSkillFormGroup());
      }
  
      getSkillFormGroup(){
        // extra skillName key
        return this.formBuilder.group({
          skillName:['',RxwebValidators.unique()]
        })
      }

Is this somehow a requirement? https://docs.rxweb.io/form-validations/unique/validators

Complete code:
inventory.html:

<p>Inventory Form Value: {{ inventoryForm.value | json }}</p>
<p>Form Status: {{ inventoryForm.status }}</p>

<div *ngIf="!inventoryForm.valid">
  <mat-card style="text-align: center; background-color:#ea8288">
    <mat-card-content>Inventory invalid</mat-card-content>
  </mat-card>
</div>
<form [formGroup]="inventoryForm">
  <div class="table-container">
    <table
      mat-table
      class="mat-elevation-z8"
      [dataSource]="inventorySource"
      formArrayName="inventoryFormArr"
    >
      <!-- ref.: https://stackoverflow.com/questions/51150193/angular-material-editable-table-using-formarray -->
      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr
        mat-row
        *matRowDef="let row; let i = index; columns: displayedColumns"
      ></tr>
      <!--- Note that these columns can be defined in any order.
                The actual rendered columns are set as a property on the row definition" -->

      <!-- Name Column -->
      <ng-container matColumnDef="inventoryName">
        <th mat-header-cell *matHeaderCellDef>Name</th>
        <td
          mat-cell
          *matCellDef="
            let inventoryFormArr of inventoryFormArr.controls;
            let i = index
          "
        >
          <mat-form-field appearance="outline" style="margin-bottom: -1.25em">
            <input
              matInput
              autocorrect="off"
              autocapitalize="off"
              spellcheck="off"
              type="text"
              required
              type="text"
              [formControlName]="i"
            />
          </mat-form-field>
        </td>
      </ng-container>

      <!-- Quantity Column -->
      <ng-container matColumnDef="quantity">
        <th mat-header-cell *matHeaderCellDef>Quantity</th>
        <td mat-cell *matCellDef="let element">{{ element.quantity }}</td>
      </ng-container>
    </table>
  </div>
</form>

inventory.component.ts:

import { Component } from '@angular/core';
import { FormArray, FormBuilder } from '@angular/forms';
import { Validators } from '@angular/forms';
import { RxwebValidators } from '@rxweb/reactive-form-validators';

export interface myDataArray {
  inventoryName: string;
  quantity: number;
}

const ELEMENT_DATA: myDataArray[] = [
  { inventoryName: 'Cars', quantity: 14 },
  { inventoryName: 'Books', quantity: 49 },
];

@Component({
  selector: 'inventory-table',
  templateUrl: './inventory.component.html',
  styleUrls: ['./inventory.component.scss'],
})
export class InventoryComponent {
  displayedColumns: string[] = ['inventoryName', 'quantity'];

  inventorySource = ELEMENT_DATA;

  inventoryForm = this.formBuilder.group({
    inventoryFormArr: this.formBuilder.array([]),
  });

  constructor(private formBuilder: FormBuilder) {}

  get inventoryFormArr() {
    return this.inventoryForm.get('inventoryFormArr') as FormArray;
  }

  ngOnInit(): void {
    this.inventorySource.forEach((item) => {
      this.inventoryFormArr.push(
        //this.formBuilder.control(item['inventoryName'], RxwebValidators.unique())
        this.formBuilder.control(item['inventoryName'], Validators.required)
      );
    });
  }
}

Solution

  • Its not able to handle an array of formControls, looks like a bug in the validator, not sure.

    In the meantime, as a workaround, you can just create a formArray of formGroups, and the validation seems to work fine!

    html

    <p>Inventory Form Value: {{ inventoryForm.value | json }}</p>
    <p>Form Status: {{ inventoryForm.status }}</p>
    
    <div *ngIf="!inventoryForm.valid">
      <mat-card style="text-align: center; background-color:#ea8288">
        <mat-card-content>Inventory invalid</mat-card-content>
      </mat-card>
    </div>
    <form [formGroup]="inventoryForm">
      <div class="table-container">
        <table
          mat-table
          class="mat-elevation-z8"
          [dataSource]="inventorySource"
          formArrayName="inventoryFormArr"
        >
          <!-- ref.: https://stackoverflow.com/questions/51150193/angular-material-editable-table-using-formarray -->
          <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
          <tr
            mat-row
            *matRowDef="let row; let i = index; columns: displayedColumns"
          ></tr>
          <!--- Note that these columns can be defined in any order.
                    The actual rendered columns are set as a property on the row definition" -->
    
          <!-- Name Column -->
          <ng-container matColumnDef="inventoryName">
            <th mat-header-cell *matHeaderCellDef>Name</th>
            <td
              mat-cell
              *matCellDef="
                let inventoryFormArr of inventoryFormArr.controls;
                let i = index
              "
              [formGroupName]="i"
            >
              <mat-form-field appearance="outline" style="margin-bottom: -1.25em">
                <input
                  matInput
                  autocorrect="off"
                  autocapitalize="off"
                  spellcheck="off"
                  type="text"
                  required
                  type="text"
                  formControlName="inventoryName"
                />
              </mat-form-field>
            </td>
          </ng-container>
    
          <!-- Quantity Column -->
          <ng-container matColumnDef="quantity">
            <th mat-header-cell *matHeaderCellDef>Quantity</th>
            <td mat-cell *matCellDef="let element">{{ element.quantity }}</td>
          </ng-container>
        </table>
      </div>
    </form>
    

    ts

    import { Component } from '@angular/core';
    import { FormArray, FormBuilder, FormGroup } from '@angular/forms';
    import { Validators } from '@angular/forms';
    import { RxwebValidators } from '@rxweb/reactive-form-validators';
    
    export interface myDataArray {
      inventoryName: string;
      quantity: number;
    }
    
    const ELEMENT_DATA: myDataArray[] = [
      { inventoryName: 'Cars', quantity: 14 },
      { inventoryName: 'Books', quantity: 49 },
    ];
    
    @Component({
      selector: 'inventory-table',
      templateUrl: './inventory.component.html',
      styleUrls: ['./inventory.component.scss'],
    })
    export class InventoryComponent {
      displayedColumns: string[] = ['inventoryName', 'quantity'];
    
      inventorySource = ELEMENT_DATA;
    
      inventoryForm = this.formBuilder.group({
        inventoryFormArr: this.formBuilder.array([]),
      });
    
      constructor(private formBuilder: FormBuilder) {}
    
      get inventoryFormArr() {
        return this.inventoryForm.get('inventoryFormArr') as FormArray;
      }
    
      ngOnInit(): void {
        this.inventorySource.forEach((item) => {
          this.inventoryFormArr.push(
            //this.formBuilder.control(item['inventoryName'], RxwebValidators.unique())
            new FormGroup({
              inventoryName: this.formBuilder.control(item['inventoryName'], [
                Validators.required,
                RxwebValidators.unique(),
              ]),
            })
          );
        });
      }
    }
    

    stackblitz