Search code examples
javascriptarraysangularformarray

Angular-FormArray: Creating a table form from 2-dimensional Javascript array


  • I need to create a table form with Angular which is based on a simple 2-dimensional array of arrays.
  • The data structure is simple but the data in the cells is interdependent. The single cells are accessed by indices alone e.g., row:1, col:0.
  • Custom validators need to be defined on on cell-level. In addition, there may be validators for rows and columns.

I tried various ways to define FormArray holding an array of FormControls. But I am unsure how to access the respective FormControl by row and column indices alone in the Angular template.

Model

[
  ['a', 'b'],
  ['c', 'd']
]

FormGroup

  form = new FormGroup({
    rows: new FormArray([...])
  });

Expected result

I am tried various things similar to this:

  <form [formGroup]="form"">
    <div formArrayName="rows">
      <div 
        *ngFor="let row of rows.controls; let rowIndex = index" [formGroupName]="rowIndex">
        <div formArrayName="cols">
          <div
            *ngFor="let col of form.get('cols').controls; let colIndex = index"
            [formGroupName]="colIndex">
            <input [formControlName]="colIndex" />
          </div>
        </div>
      </div>
    </div>
  </form>

Solution

  • Dabbel, if you has an Array of Array, create a FormArrays of FormArrays (sorry for the joke)

    Well, imagine you has data=[ ['a', 'b'], ['c', 'd'] ]

    You can in ngOnInit create the formArray of FormArray like

    //At firs a empty FormArray
    this.formArray = new FormArray([]);
    //with each element of data
    this.data.forEach(x => {
      //x is e.g.['a','b']
      //we create a emptt FormArray
      const obj = new FormArray([]);
      //add a FormControl
      x.forEach(y => {
        obj.push(new FormControl(y));
      });
      //and push in the formArray
      this.formArray.push(obj);
    });
    

    or abreviated using map like

    this.formArray=new FormArray(
      this.data.map(x=>new FormArray(
        x.map(y=>new FormControl(y))))
    )
    

    Well, How mannage a FormArray outside a FormGroup? If our FormArray is a FormArray of FormGroup, we make in general

     <!--yes we can use [formGroup] with a FormArray-->
    <form [formGroup]="formArray">
       <!--iterate over the formArray.controls, that is a formGroup-->
      <div *ngFor="let group of formArray.controls;let i=index">
         <div [formGroup]="group">
           <input formControlName="name">
            ...
         </div>
      </div>
    </form>
    

    Well, our formArray is a FormArray of FormArray, but remember that we using [formGroup] with an array and iterate over formArray.controls.

    <form [formGroup]="formArray">
        <div *ngFor="let subarray of formArray.controls;let i=index">
            <div [formGroup]="subarray">
                <ng-container *ngFor="let control of subarray.controls;let j=index">
                    <input [formControl]="control">{{control.invalid?"*":""}}
           </ng-container>
            </div>
        </div>
    </form>
    

    NOTE: I use <ng-container> nor <div> to not create addicionals divs. (We can not put the *ngFor in the own input because, then, we can not have access to control.invalid

    Well, as you want create Validators, we are changing a bit when we create the formGroup to include the validators, I put a "fool" example

    this.formArray=new FormArray(
      this.data.map(x=>new FormArray(
        x.map(y=>new FormControl(y,Validators.required)),
        this.rowValidator())),this.arrayValidator()
    )
    

    And ours validators can be like

      rowValidator()
      {
        return (array:FormArray)=> {
          const invalid:boolean=array.value.
                 filter((x,index)=>array.value.indexOf(x)!=index).length>0
          return invalid?{error:'must be different'}:null
        }
      }
      arrayValidator()
      {
          return (array:FormArray)=> {
            let arrayJoin="";
            array.value.forEach(x=>arrayJoin+=x.join(''))
            return arrayJoin=="abcd"?null:{error:'must be a,b,c,d'}
          }
      }
    

    You can see in the stackblitz

    NOTE: In a real application, we neen't use so many Validators. Take account the cost of this validator on the perfomance of the app