Search code examples
angulartypescriptformarrayng-zorro-antd

Angular: Why does my formArray not validate or update?


I'm trying to get an array of form fields to validate or update on my parent form group. Can someone tell me what I'm doing wrong?

What I basically want to achieve is to add a new 'Vendor Line' with more than one form field. And validate each of those fields too. Currently only the formGroup's outer field is validating. I wan't to validate each of the formArray fields too. I'm using ng-zorro framework from Ant, if that helps. Thanks in advance.

Here's the code:

new-project.component.html

<div nz-row nzGutter="16">
  <div nz-col>
    <h2>Create new Project</h2>
  </div>
</div>
<div nz-row nzGutter="16">
  <div nz-col nzSpan="6" nzOffset="18">
    <button nz-button nzType="primary" nzBlock>
      <i nz-icon nzType="download"></i>Primary
    </button>
  </div>
</div>
<form nz-form nzLayout="vertical" [formGroup]="validateForm" (ngSubmit)="submitForm()">
  <div nz-row nzGutter="16">
    <div nz-col nzSpan="6">
      <nz-form-item>
        <nz-form-label>Project Name</nz-form-label>
        <nz-form-control nzErrorTip="Please enter a project name">
          <input formControlName="projectName" nz-input placeholder="Project Name" />
        </nz-form-control>
      </nz-form-item>
    </div>
    <div nz-col nzSpan="6">
      <nz-form-item>
        <nz-form-label>Company Code</nz-form-label>
        <nz-form-control nzErrorTip="Please select a company code" nzHasFeedback>
          <app-company-select formControlName="selectedCompanyValue"></app-company-select>
        </nz-form-control>
      </nz-form-item>
    </div>
  </div>
  <div nz-row nzGutter="16">
    <div nz-col nzSpan="5">
      Vendor
    </div>
    <div nz-col nzSpan="4">
      Budget
    </div>
    <div nz-col nzSpan="3">
      Available Budget
    </div>
    <div nz-col nzSpan="4">
      Budget to use in Project
    </div>
    <div nz-col nzSpan="2">
      Currency
    </div>
    <div nz-col nzSpan="2">
      Layer 1
    </div>
    <div nz-col nzSpan="2">
      Layer 2
    </div>
    <div nz-col nzSpan="2">
      Layer 3
    </div>
  </div>
  <div formArrayName="projectHeaders">
    <ng-container *ngFor="let projectHeader of validateForm.get('projectHeaders').controls; let i = index" formGroupName="{{i}}">
      <div nz-row nzGutter="16">
        <div nz-col nzSpan="5">
          <nz-form-item>
            <nz-form-control nzErrorTip="Please select a vendor" nzHasFeedback>
              <app-vendor-search formControlName="selectedVendorValue"></app-vendor-search>
            </nz-form-control>
          </nz-form-item>
        </div>
        <div nz-col nzSpan="4">
          <nz-form-item>
            <nz-form-control nzErrorTip="Please select a budget" nzHasFeedback>
              <nz-select formControlName="selectedBudgetValue" nzAllowClear nzPlaceHolder="Budgets">
                <nz-option *ngFor="let o of listOfBudgets" [nzLabel]="o.text" [nzValue]="o.value"> </nz-option>
              </nz-select>
            </nz-form-control>
          </nz-form-item>
        </div>
        <div nz-col nzSpan="3">
          <nz-form-item>
            <nz-form-control>
              <input nz-input placeholder="" formControlName="availableBudget" [disabled]="true" />
            </nz-form-control>
          </nz-form-item>
        </div>
        <div nz-col nzSpan="4">
          <nz-form-item>
            <nz-form-control nzErrorTip="Make sure you have selected a valid budget amount.">
              <nz-input-number formControlName="budgetToUseInProject" [nzFormatter]="parseCurrencyEUR" style="width: 100%;">
              </nz-input-number>
            </nz-form-control>
          </nz-form-item>
        </div>
        <div nz-col nzSpan="2">
          <nz-form-item>
            <nz-form-control>
              <input nz-input placeholder="" formControlName="currency" [disabled]="true" />
            </nz-form-control>
          </nz-form-item>
        </div>
        <div nz-col nzSpan="2">
          <nz-form-item>
            <nz-form-control nzErrorTip="Please select a layer" nzHasFeedback>
              <nz-select formControlName="layer1" nzAllowClear nzPlaceHolder="Layer 1">
                <nz-option *ngFor="let o of layerOneOptions" [nzLabel]="o.text" [nzValue]="o.value"> </nz-option>
              </nz-select>
            </nz-form-control>
          </nz-form-item>
        </div>
        <div nz-col nzSpan="2">
          <nz-form-item>
            <nz-form-control nzErrorTip="Please select a layer" nzHasFeedback>
              <nz-select formControlName="layer2" nzAllowClear nzPlaceHolder="Layer 2">
                <nz-option *ngFor="let o of layerOneTwoOptions" [nzLabel]="o.text" [nzValue]="o.value"> </nz-option>
              </nz-select>
            </nz-form-control>
          </nz-form-item>
        </div>
        <div nz-col nzSpan="2">
          <nz-form-item>
            <nz-form-control nzErrorTip="Please select a layer" nzHasFeedback>
              <nz-select formControlName="layer3" nzAllowClear nzPlaceHolder="Layer 3">
                <nz-option *ngFor="let o of layerThreeOptions" [nzLabel]="o.text" [nzValue]="o.value"> </nz-option>
              </nz-select>
            </nz-form-control>
          </nz-form-item>
        </div>
      </div>
    </ng-container>
  </div>
  <!-- <div formArrayName="projectHeaders">
    <ng-container *ngFor="let projectHeader of validateForm.get('projectHeaders').controls; let i = index">
      <app-vendor-line [group]="projectHeader"></app-vendor-line>
    </ng-container>
  </div> -->
  <div nz-row nzGutter="16">
    <div nz-col nzSpan="24">
      <nz-form-item>
        <nz-form-control nzErrorTip="Please">
          <button nz-button nzSize="large" nzType="primary" nzBlock>Primary</button>
        </nz-form-control>
      </nz-form-item>
    </div>
  </div>
</form>

new-project.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControl, FormArray } from '@angular/forms';
import { formatNumber } from '@angular/common';

@Component({
  selector: 'app-new-project',
  templateUrl: './new-project.component.html',
  styleUrls: ['./new-project.component.css']
})
export class NewProjectComponent implements OnInit {
  validateForm: FormGroup;

  constructor(private fb: FormBuilder) {

    this.validateForm = this.fb.group({
      selectedCompanyValue: [null, [Validators.required]],
      projectName: [null, [Validators.required]],
      projectHeaders: this.fb.array([]),
      projectLines: []
    });
  }

  ngOnInit() {


    this.addHeaderLine();
  }

  submitForm(): void {
    // tslint:disable-next-line: forin
    for (const i in this.validateForm.controls) {
      this.validateForm.controls[i].markAsDirty();
      this.validateForm.controls[i].updateValueAndValidity();
      if (i === 'projectHeaders') {
        const control = this.validateForm.get('projectHeaders') as FormArray;
        for (const j of control.controls) {
          j.markAsDirty();
          j.updateValueAndValidity();
        }
      }
    }
  }

  initProjectHeader() {
    return this.fb.group({
      selectedVendorValue: [null, Validators.compose([Validators.required])],
      selectedBudgetValue: [null, Validators.compose([Validators.required])],
      availableBudget: [{value: 0, disabled: true }, Validators.compose([Validators.required])],
      budgetToUseInProject: [0, Validators.compose([Validators.required])],
      currency: [{value: 'EUR', disabled: true}, Validators.compose([Validators.required])],
      layer1: [null, Validators.compose([Validators.required])],
      layer2: [null, Validators.compose([Validators.required])],
      layer3: [null, Validators.compose([Validators.required])]
    });
  }

  addHeaderLine() {
    const control = this.validateForm.controls.projectHeaders as FormArray;
    control.push(this.initProjectHeader());
  }

  parseCurrencyEUR(value: number): string {
    return formatNumber(value, 'de_DE');
  }


}


Solution

  • I managed to figure what the issue is. Apparently you have to even mark the nested forms as dirty and update them so I modified the onSubmit function to do just this. Thanks to all the others who tried to help :)

    submitForm(): void {
      // tslint:disable-next-line: forin
      for (const i in this.validateForm.controls) {
        this.validateForm.controls[i].markAsDirty();
        this.validateForm.controls[i].updateValueAndValidity();
        if (i === 'projectHeaders') {
          const control = this.validateForm.get('projectHeaders') as FormArray;
          // tslint:disable-next-line: forin
          for (const j in control.controls) {
            const controlTwo = control.controls[j] as FormGroup;
            // tslint:disable-next-line: forin
            for (const k in controlTwo.controls) {
              controlTwo.controls[k].markAsDirty();
              controlTwo.controls[k].updateValueAndValidity();
            }
          }
        }
        if (i === 'projectLines') {
          const control = this.validateForm.get('projectLines') as FormArray;
          // tslint:disable-next-line: forin
          for (const j in control.controls) {
            const controlTwo = control.controls[j] as FormGroup;
            // tslint:disable-next-line: forin
            for (const k in controlTwo.controls) {
              controlTwo.controls[k].markAsDirty();
              controlTwo.controls[k].updateValueAndValidity();
            }
          }
        }
      }
    }