Note: This post is part of an eleven-part series on Angular development. You can find the series overview here.

Overview

Forms are a critical component to most any web application. While most forms can be completed with the standard browser inputs, sometimes a more dynamic or thematic input can make the difference in a user interface. Angular provides the ControlValueAccessor interface to provide developers the tools to seamlessly insert custom built components that can function just as a standard input. But as we will see, the ControlValueAccessor can provide much more than just custom inputs – it can serve as a gateway to better form organization and reusability within an application.

To follow along with the articles there a two repositories created: a starting point and final solution.

A Rating Input Component

Let’s start by creating a component that we want to function as a custom form input.  In this example, we will build a rating input that allows a user to select a value from 0 to a maximum rating by clicking a star.  We can use the FontAwesome library to get the star icons and change between empty and filled versions.  Below is the starting point for the component:

class RatingStar {
	icon: IconDefinition;
	index: number;
	selected: boolean;

	constructor(index: number, rating: number) {
		this.index = index;
		this.selected = index < rating;
		this.icon = this.selected ? faStarSolid : faStarEmpty;
	}
}

@Component({
	selector: 'app-rating-control',
	template: `
        <div>
            <fa-icon
                    *ngFor="let star of ratingStars; let i = index"
					size="lg"
                    [icon]="star.icon"
					[ngClass]="{'selected': star.selected}"
                    (click)="setRating(i)">
            </fa-icon>
        </div>`,
	styles: [
			`
            fa-icon {
                cursor: pointer;
            }

            fa-icon.selected {
                color: GoldenRod;
            }` ]
})
export class RatingControlComponent implements OnChanges {
	@Input() maxRating = 10;
	@Input() rating: number;
	@Output() ratingChange = new EventEmitter<number>();

	public ratingStars: RatingStar[] = [];

	ngOnChanges(): void {
		this.calculateRating();
	}

	private calculateRating(): void {
		this.ratingStars = [];
		for (let i = 0; i < this.maxRating; i++) {
			this.ratingStars.push(new RatingStar(i, this.rating));
		}
	}

	setRating(index: number): void {
		this.rating = index + 1;
		this.calculateRating();
		this.ratingChange.emit(this.rating);
	}
}
The high level overview of this component is:

  1. It uses an array of a RatingStar class that is used to determine which icon to show for a given star
  2. The rating array is recalculated whenever an input changes or the user clicks on a star
  3. The user’s click on a star also emits the new value to enable two way data binding

The resulting component is used in a parent component bound to a max rating input and displays the output rating value to verify functionality.

Implementing ControlValueAccessor

Using the Component without ControlValueAccessor

We have a component that gives us some data, let’s see if we can use it in a form.  Below is the form test bed we will use for testing, it includes the following features:

  1. A native input for comparison to the custom control we are building
  2. The custom rating control – note that the two way binding to rating is no longer included or needed
  3. Default values for each control
  4. Options to enable or disable the entire form
  5. A button to reset the state of the form
  6. Output of the form value and touched status
@Component({
	selector: 'app-control-value-accessor',
	template: `
        <form [formGroup]="testForm" autocomplete="off" class="w-25 was-validated">
            <div class="form-group">
                <label>Test Input</label>
                <input class="form-control" type="text" formControlName="testInput">
            </div>

            <div class="form-group">
                <label>Rating</label>
                <app-rating-control [maxRating]="5" [(rating)]="rating"></app-rating-control>
            </div>
        </form>
        <button class="btn btn-secondary" (click)="testForm.disable()">Disable Form</button>
        <button class="btn btn-success" (click)="testForm.enable()">Enable Form</button>
        <button class="btn btn-danger" (click)="testForm.reset()">Reset Form</button>
        <div style="white-space: pre">
            Rating: {{ rating }}<br/>
            {{testForm.value | json}}<br/>
            Touched: {{testForm.touched | json}}<br/>
            Valid: {{testForm.valid}}<br/>
            Errors: {{testForm.controls['rating']?.errors | json}}<br/>
        </div>`
})
export class ControlValueAccessorComponent implements OnInit {
	public testForm: FormGroup;
	public rating;

	constructor(private formBuilder: FormBuilder) { }

	ngOnInit(): void {
		this.testForm = this.formBuilder.group({
			testInput: 'abc',
			rating: 3
		});
	}
}
When we open this page the following error appears in the console:

The error specifies that for the element we specified as the ‘rating’ form control, Angular isn’t able to find a value accessor to know how to interact with the component.  So what exactly is a value accessor?

The ControlValueAccessor Interface

ControlValueAccessor is an interface than Angular uses to keep the view in sync with forms data models and vice-versa.  There are 4 methods on the interface, one of which is optional.  We will walk through each method to better understand how Angular handles forms under the hood:

interface ControlValueAccessor {
    writeValue(obj: any): void
    registerOnChange(fn: any): void
    registerOnTouched(fn: any): void
    setDisabledState(isDisabled: boolean)?: void
}

WriteValue

The writeValue method is responsible for updating the view when there are programmatic changes to the form.  In the example of our rating component, this would be when the Input value changes for the rating and we need to recalculate which stars are shown.

RegisterOnChange

The registerOnChange method is called at the initialization of the form and registers a callback function to notify Angular when our control view has changed and the data needs to propagate to the form data model.  The parameter of this method is a function that the control will need to save the reference and then call at the appropriate times.  In our example, this should fire whenever a user clicks a star.

RegisterOnTouch

The registerOnTouch method is similar to the registerOnChange method as it also registers a callback function.  This function is to notify the forms data model that the control has been touched.  In a standard input, this is the blur event when a user clicks or tabs into the input but has not yet made a change.  In our example, there is no blur method so we won’t have anything to implement.  This is not uncommon and the touched property will get updated along with the change callback.

SetDisabledState

The setDisabledState method is an optional method for the interface.  The method receives a boolean parameter that specifies if the control should be disabled or enabled.  This is used to set the appropriate styling on the component to signify the state.

Implementing ControValueAccessor on a Component

There are two necessary steps to set a component to serve as a form value accessor, we must implement the ControlValueAccessor interface and also identify the component as a control value provider.

Step 1 – Interface Implementation

We can implement each interface method one by one to see how they function.

Write Value
To implement writeValue we will need a way to store the current value of the control within the component.  We can use the rating property, but we no longer need to set it as an input to the control.  When writeValue is called, we should set the rating to the provided value and update the view:

public rating: number;

writeValue(obj: any): void {
    this.rating = obj;
    this.calculateRating();
}

RegisterOnChange

To implement registerOnChange we need a property to save a handle to the function provided.  This method can replace the ratingChange event we had implemented as it serves the same exact purpose.  All we need to do is save the method passed into the function and call it with the new value every time the control changes:

public onChangeFn;

setRating(index: number): void {
   this.rating = index + 1;
   this.calculateRating();
   this.onChangeFn(this.rating);
}

registerOnChange(fn: any): void { this.onChangeFn = fn; }

RegisterOnTouched

We could leave registerOnTouched as an empty function as we don’t really have a touch event for the form control.  However, for demonstration, we can consider the control touched when the user hovers over the rating.  All we need to do is call the method whenever the mouseleave event occurs on the wrapping element:

template: `<div (mouseleave)="onTouchFn()">
             <fa-icon ...></fa-icon>
           </div>`

SNIP...
public onTouchFn;

registerOnTouched(fn: any): void {
    this.onTouchFn = fn;
}

SetDisabledState

The setDisabledState method is optional, but we can fairly easily implement it for this control.  All we need to do is apply some styling to gray out the control and prevent the user click’s from changing the value.  First – in the component we need to track if the control is disabled and make a change to setRating to stop any changes from happening when disabled:

public isDisabled: boolean;

setRating(index: number): void {
    if (!this.isDisabled) {
        this.rating = index + 1;
        this.calculateRating();
        this.onChangeFn(this.rating);
    }
}

setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
}

And create a new CSS class – the entire component styling is included for context:

fa-icon {
    cursor: pointer;
}

.selected {
    color: GoldenRod;
}

.disabled {
    color: gray !important;
    cursor: default;
}

Lastly – we apply the disabled class to the icons when disabled:

<fa-icon ...SNIP... [ngClass]="{'selected': star.selected, 'disabled': isDisabled}"></fa-icon>

Step 2 – Control Provider

Even with the ControlValueAccessor interface implementation we still get the same error as before when testing the form.  The reason is that once the code is compiled, Angular has no way to identify that a component implements an interface.  Instead, we need to register the component as a provider so Angular can easily obtain a reference to the class it will use to communicate with the form view.  To do so, we can add a provider to the RatingControlComponent:

providers: [
    {
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => RatingControlComponent),
        multi: true
    }
]

NG_VALUE_ACCESSOR is the token Angular uses to get the ControlValueAccessor implementations in the application.  If you have not seen forwardRef before, it simply allows us to refer to an item that is declared but not yet created.  We also need to use the multi option – in short this adds the binding to an array of items to be injected instead of overriding a single value. 

Testing The Form

With our custom control set up we can now test the form and make sure everything works.  Our test form contains the JSON data of the form model itself and the value of the touched flag:

You can see the touched value get updated when the cursor leaves the component, data is bound to the form just like the native input, the disabled status is applied, and the control resets with the form reset command. In summary, the custom control acts just as the native input and there is no difference from the form’s perspective on how to interact with the control.

Control Validation

When talking about forms, we must always consider validation as well.  There are two avenues of validation to consider with custom elements – control defined and user defined.

Control Defined Validation

Custom controls may have internal validation that is always active – for example a date picker control may validate that the input is a valid date.  For this example we can implement a minimum rating option that we define as an input to the component. To prepare we can add an input to the component to control this functionality:

@Input() minRating = 3;
We will also add a control to change the value of the minimum rating. Take note that this is outside of the form we have defined, so any changes to this data value will not trigger any changes to the form on it’s own. Below is a highlight of the relevant changes:
<form [formGroup]="testForm">
    <app-rating-control [maxRating]="5" [minRating]="minRating" formControlName="rating"></app-rating-control>
</form>

SNIP...

<div class="form-group">
    <label>Min Rating</label>
    <input name="minRating" class="form-control" type="number" [(ngModel)]="minRating">
</div>

// In Component default the value
public minRating = 3;

For our custom control to communicate to the Angular forms API that it is invalid we need to implement the Validator interface.   There are two methods in this interface, one of which is optional.

Validate

The required method to implement is the validate method.  It provides the control as a parameter (as an AbstractControl) to the method and expects a null (valid) or a key-value pair (invalid) result.  This signature is very similar to implementing any custom form validator.  For our example, we just want to make sure that the value is above or equal to the minimum rating:

validate(control: AbstractControl): ValidationErrors {
    return control.value >= this.minRating ? null : { tooLow: 'It's not that bad...'};
}

RegisterOnValidatorChange

The registerOnValidatorChange method is optional and registers a callback to call whenever the logic to determine if the control is valid changes.  If the validation is a static check then this is not required.  Going back to the date picker example we would only want Angular to check the validation whenever the control value changed.  In our example, a change from outside the form (the minRating input value) could impact the valid status of the control so we want to execute the callback to trigger Angular to recheck the control.  It’s sufficient for this control to just put the callback in the ngOnChanges method.

public onValidatorChangeFn;

registerOnValidatorChange?(fn: () => void): void {
    this.onValidatorChangeFn = fn;
}
ngOnChanges(): void {
    this.calculateRating();
    if (this.onValidatorChangeFn) {
        this.onValidatorChangeFn();
    }
}

Notice the check on the function.  This is generally required as the registration of the callback happens after the initial setting of the input values which would throw an error when creating the component.  Angular runs an initial validation check on form creation so we only need to be concerned with changes after the callback is registered.

Provider Registration

We also need to register the component as a Validator, this is nearly identical to the process for ControlValueAccessor just using a different token:

providers: [
    {
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => RatingControlComponent),
        multi: true
    },
    {
        provide: NG_VALIDATORS,
        useExisting: forwardRef(() => RatingControlComponent),
        multi: true
    }
]

User Defined Validation

Custom controls can have validation also applied the way you would for any other input.  Here we can make the rating required so we can see how the two methods of validation interact:

this.testForm = this.formBuilder.group({
    testInput: 'abc',
    rating: [3, Validators.required]
});

Visualizing Status

We want to be able to see when the component is invalid, so we can add some simple CSS to the form component to change the stars to red when invalid.  Angular applies a ng-invalid class to controls automatically when they are not valid that we can utilize:

app-rating-control.ng-invalid {
    color: red;
}

We can also add some data binding to the sample form to see the status and any errors on our control:

Valid: {{testForm.valid}}<br/>
Errors: {{testForm.controls['rating'].errors | json}}<br/>

Testing Validation

With this in place, we can test the validation and see how the control reacts.

We can see that when the rating goes below the minimum the control is invalid. Also, when it is reset the required error is triggered and the control is also invalid.  There is no difference between the two methods for how the control displays as both will add the ng-invalid class to be used for styling.

 

Conclusion

Getting comfortable with ControlValueAccessor can significantly increase your ability to create unique and relevant ways for users to interact with your application.  As we will see in the next article – Reusable Sub-Forms – we can leverage this functionality for groups of fields as well.  It is an excellent way to isolate the complex issues that can arise in a complicated control from the implementation in a form.

About Intertech

Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us. 

 

Related Articles In This Series