Home > Enterprise >  Parent Form returning incorrect validation from nested form
Parent Form returning incorrect validation from nested form

Time:05-31

I am building up from previous question that i asked on StackOverflow and am trying to figure out a very weird problem.

Problem is that parent form is not having its valid status updated, it seems like validation doesn't propagate all the way up breaking the whole delegation principle.

Screenshot with child form with all data

enter image description here

This is as expected and returns all true, as it should be.

Screenshot with child form with nested form invalid:

enter image description here

This should show both as false, yet as you can see parent is showing true, its as if delegation stopped at person form.

Easiest way to replicate this is:

  • ng new test
    • Up to you if you choose routing, non-css, it really doesn't matter
  • ng add @angular/material
  • ng g c person-form
  • ng g c address-form

Update the imports of App Module

@NgModule({
  declarations: [
    AppComponent,
    PersonFormComponent,
    AddressFormComponent
  ],
  imports: [
    BrowserModule,
    FormsModule, // ADD
    ReactiveFormsModule, // ADD
    AppRoutingModule,
    BrowserAnimationsModule,
    MatFormFieldModule, //ADD
    MatInputModule, //ADD
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Make the following updates to Person component

HTML

<fieldset [formGroup]="form">
    <mat-form-field>
        <input matInput placeholder="First name" formControlName="firstName" (blur)="onTouched()" />
    </mat-form-field>
    <mat-form-field>
        <input matInput placeholder="Last name" formControlName="lastName" (blur)="onTouched()" />
    </mat-form-field>
    <ng-container formArrayName="addresses">
        <ng-container *ngFor="let addressForm of addresses.controls; index as i">
            <app-address-form [formControlName]="i"></app-address-form>
            <button (click)="removeAddressAtIndex(i)">Remove Address</button>
        </ng-container>
    </ng-container>
</fieldset>
<button (click)="addAddress()">Add Address</button>
<h3>The person form is valid: </h3><h2>{{form.valid}}</h2>

TS

@Component({
  selector: 'app-person-form',
  templateUrl: './person-form.component.html',
  styleUrls: ['./person-form.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: forwardRef(() => PersonFormComponent),
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: forwardRef(() => PersonFormComponent),
    },
  ],
})
export class PersonFormComponent
  implements ControlValueAccessor, OnDestroy, Validator {
  form: FormGroup = this.fb.group({
    firstName: [null, Validators.required],
    lastName: [null, Validators.required],
    addresses: this.fb.array([]),
  });
  constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }

  get addresses() {
    return this.form.controls['addresses'] as FormArray;
  }

  addAddress() {
    this.addresses.push(this.fb.control(null));
    this.cdr.detectChanges();
  }
  removeAddressAtIndex(i: number) {
    this.addresses.removeAt(i);
    this.cdr.detectChanges();
  }

  onTouched: Function = () => { };
  onChangeSubs: Subscription[] = [];
  onValidationChange: any = () => { };

  registerOnValidatorChange?(fn: () => void): void {
    this.onValidationChange = fn;
  }

  ngOnDestroy() {
    for (let sub of this.onChangeSubs) {
      sub.unsubscribe();
    }
  }

  registerOnChange(onChange: any) {
    const sub = this.form.valueChanges.subscribe(onChange);
    this.onChangeSubs.push(sub);
  }

  registerOnTouched(onTouched: Function) {
    this.onTouched = onTouched;
  }

  setDisabledState(disabled: boolean) {
    if (disabled) {
      this.form.disable();
    } else {
      this.form.enable();
    }
  }

  writeValue(value: any) {
    if (value) {
      console.log(value);
      this.form.setValue(value, { emitEvent: false });
    }
  }

  validate(control: AbstractControl) {
    if (this.form.valid) {
      return null;
    }

    let errors: any = {};

    Object.keys(this.form.controls).forEach((e: any) => {
      errors = this.addControlErrors(errors, e);
    });

    return errors;
  }

  addControlErrors(allErrors: any, controlName: string) {
    const errors = { ...allErrors };

    const controlErrors = this.form.controls[controlName].errors;

    if (controlErrors) {
      errors[controlName] = controlErrors;
    }

    return errors;
  }
}

Make the following changes to address component

HTML

<fieldset [formGroup]="form">
  <mat-form-field>
    <input matInput placeholder="Address Line 1" formControlName="addressLine1" (blur)="onTouched()" />
  </mat-form-field>
  <mat-form-field>
    <input matInput placeholder="Address Line 2" formControlName="addressLine2" (blur)="onTouched()" />
  </mat-form-field>
  <mat-form-field>
    <input matInput placeholder="Zip Code" formControlName="zipCode" (blur)="onTouched()" />
  </mat-form-field>
  <mat-form-field>
    <input matInput placeholder="City" formControlName="city" (blur)="onTouched()" />
  </mat-form-field>
</fieldset>

TS

@Component({
  selector: 'app-address-form',
  templateUrl: './address-form.component.html',
  styleUrls: ['./address-form.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: forwardRef(() => AddressFormComponent),
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: forwardRef(() => AddressFormComponent),
    },
  ],
})
export class AddressFormComponent
  implements ControlValueAccessor, OnDestroy, Validator {
  form: FormGroup = this.fb.group({
    addressLine1: [null, Validators.required],
    addressLine2: [null, Validators.required],
    zipCode: [null, Validators.required],
    city: [null, Validators.required],
  });

  onTouched: Function = () => { };
  onChangeSubs: Subscription[] = [];
  onValidationChange: any = () => { };

  constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }

  registerOnValidatorChange?(fn: () => void): void {
    this.onValidationChange = fn;
  }

  ngOnDestroy() {
    for (let sub of this.onChangeSubs) {
      sub.unsubscribe();
    }
  }

  registerOnChange(onChange: any) {
    const sub = this.form.valueChanges.subscribe(onChange);
    this.onChangeSubs.push(sub);
  }

  registerOnTouched(onTouched: Function) {
    this.onTouched = onTouched;
  }

  setDisabledState(disabled: boolean) {
    if (disabled) {
      this.form.disable();
    } else {
      this.form.enable();
    }
  }

  writeValue(value: any) {
    if (value) {
      console.log(value);
      this.form.setValue(value, { emitEvent: false });
    }
  }

  validate(control: AbstractControl) {
    if (this.form.valid) {
      return null;
    }

    let errors: any = {};

    Object.keys(this.form.controls).forEach((e: any) => {
      errors = this.addControlErrors(errors, e);
    });

    return errors;
  }

  addControlErrors(allErrors: any, controlName: string) {
    const errors = { ...allErrors };

    const controlErrors = this.form.controls[controlName].errors;

    if (controlErrors) {
      errors[controlName] = controlErrors;
    }

    return errors;
  }
}

Finishing touch needs to be applied to App Component

HTML

<form [formGroup]="form">
  <app-person-form formControlName="person"></app-person-form>
  <hr />
  <ng-container formArrayName="addresses">
    <ng-container *ngFor="let addressForm of addresses.controls; index as i">
      <app-address-form [formControlName]="i"></app-address-form>
      <button (click)="removeAddress(i)">Remove Address</button>
    </ng-container>
  </ng-container>
</form>
<button (click)="addAddress()">Add Address</button>

<h3>The parent form is valid: </h3><h2>{{form.valid}}</h2>

TS

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  form: FormGroup = this.fb.group({
    person: [null, Validators.required],
    totalQuantity: [
      0,
      [Validators.required, Validators.min(0), Validators.max(100)],
    ],
    addresses: this.fb.array([]),
  });
  constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}

  get addresses() {
    return this.form.controls['addresses'] as FormArray;
  }
  removeAddress(i: number) {
    this.addresses.removeAt(i);
    this.cdr.detectChanges();
  }

  addAddress() {
    this.addresses.push(this.fb.control(null));
    this.cdr.detectChanges();
  }
}

This is doing my head in as i cannot figure out why the propagation has effectively stopped.

CodePudding user response:

Complementary my comment. You need replace your validate function in person.component:

validate(control: AbstractControl) {
    if (this.form.valid) {
      return null;
    }

    let errors: any = {};

    Object.keys(this.form.controls).forEach((e: any) => {

      //see that if e=='adresses' this.form.controls['adresses'].errors is null

      if (e== 'addresses')
      {
        this.addresses.controls.forEach((x,i) => {
          errors = this.addControlErrors(errors, e '.' i);
        });
      }
      else
        errors = this.addControlErrors(errors, e);
    });

    return errors;
  }

  addControlErrors(allErrors: any, controlName: string) {
    const errors = { ...allErrors };

    //see that you use this.form.get(controlName)
    //NOT this.form.controls[controlName]
    //this allow you pass as name some like 'addresses.0'
    const controlErrors = this.form.get(controlName).errors;

    if (controlErrors) {
      errors[controlName] = controlErrors;
    }

    return errors;
  }

See the forked stackblitz (in leyend of the .html I wrote the "errors" of the control)

  • Related