Angular: Nested Reactive Forms Using ControlValueAccessors(CVAs)

Gugan Arumugam
Jan 9, 2019 · 6 min read
Photo by Danka & Peter on Unsplash

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.

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)

If you want to know more in-depth or if you are still confused about implementing one , read this article by Max, from Angular-In-Depth


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.

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.

The .ts file looks like this

Output:

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

Similarly do the same for AddressComponent.

Now the parent form component BillingInfo will look like this

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.


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

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

For more info about forwardRef() there is again this great article by Max, from Angular-in-depth

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.

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.

Angular In Depth

The place where advanced Angular concepts are explained

Gugan Arumugam

Written by

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)

Angular In Depth

The place where advanced Angular concepts are explained

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade