angularangular-reactive-forms

How to mark nested angular form as touched implementing ControlValueAccessor?


There is an example form Angular University how to use nested forms with ControlValueAccessor.

They create a separate form component and use it as a sub-form:

<div [formGroup]="form">
  ... other form controls
  <address-form formControlName="address" legend="Address"></address-form>
</div>

I want to have all the fields of the nested form marked as touched if I call the markAllAsTouched method on the parent form.

<form [formGroup]="form">
  <app-address-form formControlName="address"></app-address-form>      
</form>
<button (click)="markAsTouched()">Mark as touched</button>
export class AppComponent {
  public form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = fb.group({ address: fb.control({ city: '' }) });
  }

  public markAsTouched() {
    this.form.markAllAsTouched();
  }
}

Is it possible using this approach?

I have built a simplified version on StackBlitz.

What actually happens when you click the button is marking the app-address-form as touched:

<app-address-form formcontrolname="address" class="ng-pristine ng-valid ng-touched">
...
</app-address-form>

But I want to propogate it to the child form and its fields.


Solution

  • I found the following solution for propagating markAsTouched to child controls.

    The following recursive function helps:

    const updateFormControlTree = (abstractControl: AbstractControl): void => {
      const forEachChildControl = (
        control: AbstractControl,
        callbackFunction: (abstractControl: AbstractControl) => void): void => {
          const childControls = (control as AbstractControl & { controls?: [] }).controls;
    
          if (!childControls) {
            return;
          }
    
          if (typeof childControls === 'object') {
            const extractedChildControls: AbstractControl[] =
              Object.values(childControls);
    
            extractedChildControls.forEach((childControl) => {
              callbackFunction(childControl);
            });
          }
        };
    
      forEachChildControl(abstractControl, (control: AbstractControl) =>
        updateFormControlTree(control)
      );
      abstractControl.markAsTouched();
    };
    

    I have to override markAsTouched of my control with the function above. For this, I need FormControl instead of the formControlName attribute:

    <app-address-form [formControl]="addressControl"></app-address-form>
    

    And this is how I can override the function:

    export class AddressFormComponent implements ControlValueAccessor, Validator, OnInit
    {
      @Input() formControl!: FormControl;
    
      public ngOnInit(): void {
        this.formControl.markAsTouched = () => updateFormControlTree(this.form);
      }
      ...
    }
    

    The last thing to do is to mark my control as touched from the parent form:

    export class AppComponent  {
      public addressControl = new FormControl();
      public form: FormGroup = this.fb.group({
        address: this.addressControl,
      });
    
      constructor(private fb: FormBuilder) {    
      }
    
      markAsTouched() {
        this.addressControl.markAsTouched();
      }
    }
    

    I have built a working example on StackBlitz.

    P.S. The solution above was inspired by the method for updating values and validity found in the source code of Angular Framework. This method is private. Would be nice to have it public though.