In this post, we will see how to implement nested reactive forms using composite control value accessors(CVAs). Kara Erickson of the Angular Core Team presented this approach at the Angular Connect 2017 .

Three Ways to implement nested forms:#

We will see the most commonly used techniques to implement nested forms.

  1. Sub -Form Component Approach (providing ControlContainer)

It is built differently for template and reactive driven forms.

Best when you have a really small project and you are using one form module only and you just have to split out a really long form(Excerpt from the talk).

Pro: Quicker to setup and run.
Con: Limited to one form module

2. By passing a handle of the FormGroup to child components via Inputand referencing it in child templates. There are couple of good tutorials on it.

But the con of using this approach is that you are tightly binding the parent form group with that of child group.

3. Using Composite CVAs.

Pros: Highly Reusable, Portable. Better Encapsulation(Internal Form Controls of the component doesn’t necessarily need to be visible to parent components). This is best used when you have more number of form modules which is typically a large project.
Cons: Need to implement CVA interface results in boilerplate code.

This is what we are going to see now.

What is Control Value Accessor (CVA)?#

Here’s what the developers at Google - Angular have to say on this?

The ControlValueAccessor interface is actually what you use to build value accessors for radio buttons, selects, input elements etc in core. It just requires three methods:
writeValue(value: any): void : takes a value and writes it to the form control element (model -> view)
registerOnChange(fn: (value:any) => void): void: takes a function that should be called with the value if the value changes in the form control element itself (view -> model)
registerOnTouched(fn: () => void): takes a function to be called when the form control has been touched (this one you can leave empty if you don’t care about the touched property)

Implementing Composite CVAs#

This section assumes that you have a working knowledge on Reactive Forms mainly FormGroups, FormControls, Validations etc.

I have created a sample reactive form component called billing-info-unnested.

import { Component, OnInit } from '@angular/core';
import { FormGroup,FormControl, Validators,AbstractControl, ValidationErrors } from "@angular/forms";

@Component({
  selector: 'app-billing-info-unnested',
  template: `
<div class="container">
  <form [formGroup] ="nestedForm" (ngSubmit) = "onSubmit()">
<div class="row">
  <label for="Full Name"> Full Name </label>
    <input type="text" formControlName="fname" class="">
</div>
<div class="row">
  <label for="Email"> Email </label>
    <input type="text" formControlName="email" class="">
</div>
<div class="row">
  <label for="addressLine"> Street Address </label>
    <input type="text" formControlName="addressLine" class="">
</div>
<div class="row">
  <label for="Area"> Area Code </label>
    <input type="text" formControlName="areacode" class="">
</div>
<button type="submit" [disabled]="nestedForm.invalid">Place Order</button>
</form>
</div>`,
  styleUrls: ['./billing-info-unnested.component.css']
})
export class BillingInfoUnnestedComponent implements OnInit {

public nestedForm: FormGroup = new FormGroup({
  fname: new FormControl("", [Validators.required]),
  email: new FormControl("", [Validators.required, Validators.email]),
addressLine: new FormControl("", [Validators.required]),
areacode: new FormControl("", [Validators.required, Validators.maxLength(5)])
})
  constructor() { }

  ngOnInit() {
  }
public onSubmit(){
  // if(this.nestedForm.invalid){
  //   return
  // }

  console.log(" Billing Form", this.nestedForm);
}
}

Output:

nestedForm {
fname:"",
email: "",
addressLine: "",
areacode: ""
}

Now imagine, if our client introduces few more requirements and we need to reuse these controls along with few more form field additions such as.

For checkout form: Shipping types.
For signin/registration: Password, gender, age, etc.

In this scenario, We can reuse by grouping them into components and converting them as form controls. That is, we are going to build our component as a composite control value accessor. We can even provide validations at the component levels. This technique is amazing, trust me. :D

Lets move name and email into BasicInfoComponent , and addressLine and areacode into AddressComponent.

Here we are taking BasicInfoComponent as example.

<ng-container [formGroup]="basicInfoForm">
<div class="row">
  <label for="Full Name"> Full Name </label>
    <input type="text" formControlName="fname" class="">
</div>
<div class="row">
  <label for="Email"> Email </label>
    <input type="text" formControlName="email" class="">
</div>
</ng-container>

The .ts file looks like this

import { Component, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor,FormControl, FormGroup, Validators } from "@angular/forms";

@Component({
  selector: 'app-basic-info',
  templateUrl: './basic-info.component.html',
  styleUrls: ['./basic-info.component.css'],
 
})
export class BasicInfoComponent implements OnInit {

public basicInfoForm: FormGroup = new FormGroup(
  {
fname: new FormControl("",[Validators.required]),
email: new FormControl("", [Validators.required])
});
  constructor() { }

  ngOnInit() {
  }
}

Output:

basicInfoForm: {
fname: "",
email: ""
}

Similarly do the same for AddressComponent.

Now the parent form component BillingInfo will look like this

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl} from "@angular/forms";

@Component({
  selector: 'app-billing-component',
  template:`
<div class="container">
  <form [formGroup] ="nestedForm" (ngSubmit) = "onSubmit()">
<app-basic-info formControlName="basicInfo"></app-basic-info>
<app-address-info formControlName = "address"></app-address-info>
<button type="submit" [disabled]="nestedForm.invalid">Place Order</button>
</form>
</div>
`,
  styleUrls: ['./billing-info.component.css']
})
export class BillingInfoComponent implements OnInit {

public nestedForm: FormGroup = new FormGroup({
  basicInfo: new FormControl(""),
  address: new FormControl("")
});
  constructor() { }

  ngOnInit() {
  }

public onSubmit(){
  console.log("Billing Info", this.nestedForm.value);
}
}

Now lets try to run this. Here is the Stackblitz demo

Error !#

When we try to run the demo, we will encounter error unfortunately.

Error: No value accessor for form control with name: ‘basicInfo’

Lets take a look at the built-in value accessors provided by the Angular Core.

Since our form control (component) doesn’t falls in any these categories, angular compiler throws error stating no value accessor is found.

If you want your custom form control to integrate with Angular forms, it has to implement ControlValueAccessor

Integrate Custom Form Controls into Angular Forms#

Lets implement interface ControlValueAccessor and override all the methods in our BasicInfoComponent and AddressInfoComponent. This is what AddressComponent looks like

import { Component, OnInit } from '@angular/core';
import { ControlValueAccessor,NG_VALUE_ACCESSOR, NG_VALIDATORS, FormGroup,FormControl, Validator, Validators,AbstractControl, ValidationErrors } from "@angular/forms";

@Component({
  selector: 'app-address-info',
  templateUrl: './address-info.component.html',
  styleUrls: ['./address-info.component.css']
})
export class AddressInfoComponent implements OnInit, ControlValueAccessor {

public addressForm: FormGroup = new FormGroup({
  addressLine: new FormControl("",[Validators.required]),
  areacode: new FormControl('', [Validators.required, Validators.maxLength(5)])
});
  constructor() { }
  ngOnInit() {
  }

  public onTouched: () => void = () => {};

  writeValue(val: any): void {
    val && this.addressForm.setValue(val, { emitEvent: false });
  }
  registerOnChange(fn: any): void {
    console.log("on change");
    this.addressForm.valueChanges.subscribe(fn);
  }
  registerOnTouched(fn: any): void {
    console.log("on blur");
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    isDisabled ? this.addressForm.disable() : this.addressForm.enable();
  }
}

After implementing accessor, we need to tell angular, that for <app-address-info></app-address-info> form control element, this is its relevant control value accessor.

How can we achieve that?#

Lets take a look at how DefaultValueAccessor is provided in the Angular Forms Package.

export const DEFAULT_VALUE_ACCESSOR: any = {  
provide: NG_VALUE_ACCESSOR,  
useExisting: forwardRef(() => DefaultValueAccessor),  
multi: true
};

Lets dissect the syntax above.

  1. Here the DefaultValueAccessor is registered using the built-in token NG_VALUE_ACCESSOR.
  2. fowardRef() denotes refer to references which are not yet defined. Use the instance ofDefaultValueAccessor which will be later instantiated by Angular
  3. useExisting() is used to make sure there is only one instance of DefaultValueAccessor.
  4. The provider object has a third option, multi: true, which is used with DI Tokens to register multiple handlers for the provide event.This is useful if we want to register our custom CVAs to NG_VALUE_ACCESSOR token

Now we will do the same for our BasicInfoComponent and AddressInfoComponent and run the demo again.

StackBlitz demo after implementing CVAs

We see that our error is gone. Hooray!! We have successfully integrated our nested child form control into our Angular Core. But there is still one problem.

Even though our place order button should be disabled if the form is invalid, we see it is not.

Below is the html code

<button type=”submit” [disabled]=”nestedForm.invalid”>Place Order</button>

So lets Inspect in developer tools.

Validation Status of Child Components is failing#

On Inspect, we can see that custom form control(child component)<app-basic-info></app-basic-info> status is valid, while the form controls inside the basic-info component are still invalid.

In the previous section, we learnt that

If you want your custom form control to integrate with Angular forms, it has to implement ControlValueAccessor.

Now, if we want that integration to include validation, we need to implement the Validator interface as well and provide our custom control as a multi provider to built-in NG_VALIDATOR token.

Reason:#

For Re-validation, the validators will need to be on the top-level form, not at the child component, if you want it to be part of the parent form’s validation.

In our case we need to have validators at BillingInfoComponent level, so for this purpose we need to convert our component to act as a validator directives such as required, min, max etc.

So lets implement Validator interface in our child components.

Take a look at how required directive is provided in angular forms package.

export const REQUIRED_VALIDATOR: StaticProvider = {  
provide: NG_VALIDATORS,  
useExisting: forwardRef(() => RequiredValidator),  
multi: true };

We need to do the same for our custom form validator. We need to register a custom form validator using the built-in NG_VALIDATORS token, and provide multiples instance of our validator provider by using the multi: true property in the provider object. This tells Angular to add our custom validators to the existing collection.

import { Component, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor,FormControl, NG_VALUE_ACCESSOR,NG_VALIDATORS, FormGroup, Validator, AbstractControl, ValidationErrors } from "@angular/forms";

@Component({
  selector: 'app-basic-info',
  template: `
<ng-container [formGroup]="basicInfoForm">
<div class="row">
  <label for="Full Name"> Full Name </label>
    <input type="text" formControlName="fname" class="">
</div>
<div class="row">
  <label for="Email"> Email </label>
    <input type="text" formControlName="email" class="">
</div>
</ng-container>`,
  styleUrls: ['./basic-info.component.css'],
  providers: [
       {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => BasicInfoComponent),
      multi: true
    },
     {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => BasicInfoComponent),
      multi: true
    }
  ]
})
export class BasicInfoComponent implements OnInit, ControlValueAccessor, Validator {

public basicInfoForm: FormGroup = new FormGroup(
  {
fname: new FormControl(""),
email: new FormControl("")
});
  constructor() { }

  ngOnInit() {
  }

public onTouched: () => void = () => {};

  writeValue(val: any): void {
    val && this.basicInfoForm.setValue(val, { emitEvent: false });
  }
  registerOnChange(fn: any): void {
    console.log("on change");
    this.basicInfoForm.valueChanges.subscribe(fn);
  }
  registerOnTouched(fn: any): void {
    console.log("on blur");
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    isDisabled ? this.basicInfoForm.disable() : this.basicInfoForm.enable();
  }

  validate(c: AbstractControl): ValidationErrors | null{
    console.log("Basic Info validation", c);
    return this.basicInfoForm.valid ? null : { invalidForm: {valid: false, message: "basicInfoForm fields are invalid"}};
  }
}

So by doing this we get <app-basic-info></app-basic-info> and <app-address-info></app-basic-info>re-validated(by calling validate method which in turn validates all the form controls present inside that child component) and status is sent to the parent form component.

Bazinga!! we have accomplished it.

Here is the full stack blitz demo

Thanks for reading! Your feedback is most welcome. If you liked this article, hit that clap button.
Follow me twitter if you want to say “hi” or talk about music.