Splitting Angular Forms with ControlValueAccessor

By | 2017-03-18

With Angular, it is easy to split things up into small manageable components. However, sometimes you have large forms that make your templates large and cumbersome. Let’s take a look at an example:

<form #myForm="ngForm">

  <h2>Personal Information</h2>
  <div class="form-group">
    <label for="firstName">First Name</label>
    <input type="text" class="form-control" id="firstName" name="firstName" [(ngModel)]="person.firstName">
  </div>
  <div class="form-group">
    <label for="lastName">Last Name</label>
    <input type="text" class="form-control" id="lastName" name="lastName" [(ngModel)]="person.lastName">
  </div>

  <h2>Address</h2>
  <div class="form-group">
    <label for="addressLine2">Address Line 1</label>
    <input type="text" class="form-control" id="addressLine1" name="addressLine1" [(ngModel)]="person.address.addressLine1">
  </div>
  <div class="form-group">
    <label for="addressLine1">Address Line 2</label>
    <input type="text" class="form-control" id="addressLine2" name="addressLine2" [(ngModel)]="person.address.addressLine2">
  </div>
  <div class="form-group">
    <label for="city">City</label>
    <input type="text" class="form-control" id="city" name="city" [(ngModel)]="person.address.city">
  </div>
  <div class="form-group">
    <label for="state">State</label>
    <input type="text" class="form-control" id="state" name="state" [(ngModel)]="person.address.state">
  </div>

</form>

This template is backed by a basic component:

import { Component } from '@angular/core';

interface Person {
  firstName: string;
  lastName: string;
  address: Address;
}

interface Address {
  addressLine1: string;
  addressLine2: string;
  city: string;
  state: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  person: Person = <Person>{ address: <Address>{}};

  constructor() { }
}

In this form, we have two distinct sets of data combined in a single model. We have the first and last name, and then we have the address as it’s own model. Forms may become much larger than this, but our data can still be chopped up into smaller sections. The question is, how can we split up a form and still maintain a cohesive model with #myForm?

If we split up the form and put the address inputs in their own component like this:

@Component({
    selector: 'address-form',
    templateUrl: './address-form.component.html'
})
export class AddressFormComponent  { 
    address: Address = <Address>{};
}

Template:

<h2>Address</h2>
<div class="form-group">
  <label for="addressLine2">Address Line 1</label>
  <input type="text" class="form-control" id="addressLine1" name="addressLine1" [(ngModel)]="address.addressLine1">
</div>
<div class="form-group">
  <label for="addressLine1">Address Line 2</label>
  <input type="email" class="form-control" id="addressLine2" name="addressLine2" [(ngModel)]="address.addressLine2">
</div>
<div class="form-group">
  <label for="city">City</label>
  <input type="text" class="form-control" id="city" name="city" [(ngModel)]="address.city">
</div>
<div class="form-group">
  <label for="state">State</label>
  <input type="text" class="form-control" id="state" name="state" [(ngModel)]="address.state">
</div>

And update our main form to include the new component:

<form #myForm="ngForm">
  ...
<address-form></address-form>
</form>

This breaks our form. The form visually displays, but if we fill in our form with some values and output the value of #myForm:

<div>{{myForm.value | json}}</div>

We will see our form controls are broken and the address values on #myForm, don’t get any values.
Output:

{ "firstName": "Tyler", "lastName": "Jennings", "address": {} }

How do we fix this?

That is where the ControlValueAccessor steps in. With ControlValueAccessor, we can bind the model to the component and allow two-way data binding with our model.

First, our AddressFormComponent, needs to implement the ControlValueAccessor interface.

import { ControlValueAccessor } from '@angular/forms';
export class AddressFormComponent implements ControlValueAccessor {
    private _address: Address = <Address>{};

    writeValue(value: any) {
        this._address = value;
    }

    propagateChange = (_: any) => { };

    registerOnChange(fn) {
        this.propagateChange = fn;
    }

    registerOnTouched() { }
    
   ...
}

We use writeValue() to populate our model with an incoming value. We register a function with the OnChange event by providing propogateChange to the registerOnChange() function. This means, when we call propogateChange and provide a value, it is registered to send the value out through ngModel. And lastly, registerOnTouched() provides a way to register a callback function to handle touch events. If we wanted blur events or something, we could register that here. But since we don’t really care in this scenario about touch events, we are leaving registerOnTouched() empty.

Then we need to register our ControlValueAccessor with Dependency Injection to let the Angular runtime know about our ControlValueAccessor. To do this, we modify the @Component decorator for our AddressFormComponent”

import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
    selector: 'address-form',
    templateUrl: './address-form.component.html',
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AddressFormComponent),
            multi: true
        }
    ]
})

If you aren’t sure what a multi provider is in Angular, check out the article on Thoughtram.

Now that we’ve implemented the ControlValueAccessor, we need to update our form to fire the propogateChange() function when a value changes to update the model on our parent form. The easiest way to do this is to change the ngModel on our address properties from [(ngModel)]="address.addressLine1" to [(ngModel)]="addressLine1" and provide getter and setter properties to handle the changes. The resulting AddressFormComponent looks like this:

import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

interface Address {
    addressLine1: string;
    addressLine2: string;
    city: string;
    state: string;
}

@Component({
    selector: 'address-form',
    templateUrl: './address-form.component.html',
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AddressFormComponent),
            multi: true
        }
    ]
})
export class AddressFormComponent implements ControlValueAccessor {
    private _address: Address = <Address>{};

    writeValue(value: any) {
        this._address = value;
    }

    propagateChange = (_: any) => { };

    registerOnChange(fn) {
        this.propagateChange = fn;
    }

    registerOnTouched() { }

    constructor() { }

    get addressLine1() {
        return this._address.addressLine1;
    }
    set addressLine1(value) {
        this._address.addressLine1 = value;
        this.propagateChange(this._address);
    }

    get addressLine2() {
        return this._address.addressLine2;
    }
    set addressLine2(value) {
        this._address.addressLine2 = value;
        this.propagateChange(this._address);
    }

    get city() {
        return this._address.city;
    }
    set city(value) {
        this._address.city = value;
        this.propagateChange(this._address);
    }

    get state() {
        return this._address.state;
    }
    set state(value) {
        this._address.state = value;
        this.propagateChange(this._address);
    }
}
<h2>Address</h2>
<div class="form-group">
  <label for="addressLine2">Address Line 1</label>
  <input type="text" class="form-control" id="addressLine1" name="addressLine1" [(ngModel)]="addressLine1">
</div>
<div class="form-group">
  <label for="addressLine1">Address Line 2</label>
  <input type="email" class="form-control" id="addressLine2" name="addressLine2" [(ngModel)]="addressLine2">
</div>
<div class="form-group">
  <label for="city">City</label>
  <input type="text" class="form-control" id="city" name="city" [(ngModel)]="city">
</div>
<div class="form-group">
  <label for="state">State</label>
  <input type="text" class="form-control" id="state" name="state" [(ngModel)]="state">
</div>

Lastly, we can update our address-form instance in our parent form:

<form #myForm="ngForm">
  <div class="form-group">
    <label for="firstName">First Name</label>
    <input type="text" class="form-control" id="firstName" name="firstName" [(ngModel)]="person.firstName">
  </div>
  <div class="form-group">
    <label for="lastName">Last Name</label>
    <input type="text" class="form-control" id="lastName" name="lastName" [(ngModel)]="person.lastName">
  </div>

  <address-form name="address" [(ngModel)]="person.address"></address-form>
</form>

<div>{{myForm.value | json}}</div>

Now, if we fill out our form, we can see the #myForm value gets fully populated all the way down just as if it was still all in the parent form:
Output:

{ "firstName": "my first name", "lastName": "last name", "address": { "addressLine1": "street adress", "addressLine2": "line 2", "city": "nowhere", "state": "somewhere" } }

It does take some work to implement the ControlValueAccessor and setup all the getter/setters for properties, however, with this we can split up really long forms into small chunks where our data model permits and make everything more manageable in the long run.