Home > Net >  Reactive Form, with FormArray of child components returning error
Reactive Form, with FormArray of child components returning error

Time:05-30

I am figuring out how to refactor a complex form into smaller forms.

Essentially there is a parent form that will have a an array of child components which contain their owner forms.

Parent TS

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  form: FormGroup = this.fb.group({
    mainAddress: [null, Validators.required],
    addresses: this.fb.array([]),
  });
  constructor(private fb: FormBuilder) {}

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

  addAddress() {
    this.addresses.push(
      this.fb.control({
        address: [null, Validators.required],
      })
    );
  }
}
<form [formGroup]="form">
  <app-address-form formControlName="mainAddress"></app-address-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>

Child Address TS

@Component({
  selector: 'app-address-form',
  templateUrl: './address-form.component.html',
  styleUrls: ['./address-form.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: AddressFormComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: AddressFormComponent,
    },
  ],
})
export class AddressFormComponent
  implements ControlValueAccessor, OnDestroy, Validator
{
  @Input()
  legend: string;

  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[] = [];

  constructor(private fb: FormBuilder) {}

  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 = {};

    errors = this.addControlErrors(errors, 'addressLine1');
    errors = this.addControlErrors(errors, 'addressLine2');
    errors = this.addControlErrors(errors, 'zipCode');
    errors = this.addControlErrors(errors, 'city');

    return errors;
  }

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

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

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

    return errors;
  }
}

Child Address 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>
Error i get is : 

    Error: NG01002: Must supply a value for form control with name: 'addressLine1'

Please keep in mind i am trying to avoid duplication of code by separating out as much as i can (DRY principle).

Appreciate any feedback

CodePudding user response:

This has a similar solution here.

If you're using the setValue() method on a FormGroup you will need to update all of the controls within that group. If you're only updating a single control, you need to use patchValue() instead. Otherwise you will get the NG01002 error.

Change this:

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

To this, adding the control you want to update as parameter:

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

Hope this helps.

CodePudding user response:

You has an array of FormControls (*) (each FormControl "store" a complex object with properties: addressLine1,addressLine2,zipCode and city, but the FormArray is still a FormArray of FormControls)

So, your function addAddress should be like

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

(*)Exists two approach when we work with nested forms. One is your approach: create a custom formControl that store a complex object and the "sub-formGroup" is created inside the component. The another is create the "sub-formGroup" in main component and pass the formGroup to a component (in this last case you not create a custom form control that implements ControlValueAccessor else a simple component with an @Input

  • Related