Angular: Nested Reactive Forms Using ControlValueAccessors(CVAs)

Post Editor

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 .

8 min read
post

Angular: Nested Reactive Forms Using ControlValueAccessors(CVAs)

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 .

post
post
8 min read

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:
Link to this section

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)?
Link to this section

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
Link to this section

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.

<>Copy
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:

<>Copy
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.

<>Copy
<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

<>Copy
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:

<>Copy
basicInfoForm: { fname: "", email: "" }

Similarly do the same for AddressComponent.

Now the parent form component BillingInfo will look like this

<>Copy
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 !
Link to this section

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
Link to this section

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

<>Copy
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?
Link to this section

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

<>Copy
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

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

So lets Inspect in developer tools.

Validation Status of Child Components is failing
Link to this section

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:
Link to this section

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.

<>Copy
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.

<>Copy
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.

Share

About the author

author_image

Full Stack Developer. Loves Angular and Akka. Torn between the love for Java and JavaScript. Founder of band:- Seven Hills Up To The Moon(stylized as 7hu2tm)

author_image

About the author

Gugan Arumugan

Full Stack Developer. Loves Angular and Akka. Torn between the love for Java and JavaScript. Founder of band:- Seven Hills Up To The Moon(stylized as 7hu2tm)

About the author

author_image

Full Stack Developer. Loves Angular and Akka. Torn between the love for Java and JavaScript. Founder of band:- Seven Hills Up To The Moon(stylized as 7hu2tm)

Looking for a JS job?
Default logo
Looking for Angular Developer

Great Software Laboratory Private Limited

India
Remote
Job logo
AngularJS Developer

VariQ Corporation

United States
Remote
Job logo
Senior Full Stack Angular Developer

Plus One Health Management

Remote
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
8 June 202118 min read
Techniques to style component host element in Angular

Styling the host element is a crucial ability. Understanding that can completely change the way you develop reusable components, making their code clear and easier to maintain. This article will concentrate on techniques that use CSS styles and reduce Typescript logic.