The Angular Forms and ReactiveForms modules come with a series of built-in directives that make it very simple to bind standard HTML elements like inputs, checkboxes, text areas, etc. to a form group.
Besides these standard HTML elements, we might also want to use custom form controls, like dropdowns, selection boxes, toggle buttons, sliders, or many other types of commonly used custom form components.
For these custom controls, we would like to have the ability to configure them as form fields using the exact same directives (ngModel
, formControl
,
formControlName
) as we use for standard input boxes.
In this guide, we are going to learn exactly how to take an existing custom form control component and make it fully compatible with the Angular Forms API, so that the component can participate in the parent form validation and value tracking mechanisms.
This means that:
- if we are using template driven forms, we will be able to plug the custom component to the form simply by using
ngModel
- and in the case of reactive forms, we will be able to add the custom component to the form using
formControlName
(orformControl
)
We will be building in this guide a simple quantity selector component, that can be used to increment or decrement a value. The component will be part of a form and will be marked as in error in case that the counter does not match a valid range.
The new custom form control will be fully compatible with the built-in Angular Forms Validators required
, max
as well as any other built-in or custom validators.
We will also learn in this guide how to create reusable nested forms, which are form sections that be reused in many different forms.
The typical example of such type of nested forms is an address sub-form, and we are going to build one in this post.
Table Of Contents
In this post, we will cover the following topics:
- How do standard form controls work?
- What are form control value accessors?
- Introduction to the
ControlValueAccessor
interface - Implementing the
ControlValueAccessor
interface step-by-step - Implementing the
Validator
interface - Demo of a fully functional custom form control
- How to implement nested form groups (an address nested form)
- Summary
This post is part of our ongoing series on Angular Forms, you can find all the articles available here.
So without further ado, let's get started learning everything that we need to know for creating our own custom form controls and nested forms!
How do standard form controls work?
In order to know how to build a custom form control, we need to first understand how the built-in form controls work.
The built-in form controls target native HTML elements such as inputs, text areas, checkboxes, etc.
Here is an example of a simple form with a couple of plain HTML form fields:
As we can see, we have here a couple of standard form controls, with the
formControlName
property applied to it. And this is how the HTML element gets binded to the form.
Whenever the user interacts with the form inputs, the form value and validity state will be automatically re-calculated.
So how does this all work under the hood then?
What are control value accessors?
What happens is that, under the hood, the Angular forms module is applying to each native HTML element a built-in Angular directive, which will be responsible for tracking the value of the field, and communicate it back to the parent form.
This type of special directive is known as a control value accessor directive.
Take for example the checkbox field of the form above. There is a built-in directive which is part of the reactive forms module that is designed to specifically track the value of a checkbox, and nothing more.
Here is the simplified declaration of this directive:
As we can see by the selector, this value tracking directive specifically targets HTML inputs of type checkbox only, but only if the ngModel
, formControl
or
formControlName
properties are applied to it.
If this directive only targets checkboxes, then what about all the other types of form controls, like text inputs or text areas?
Well, each of those control types has their own value accessor directive, which is different than CheckboxControlValueAccessor
.
All of those directives are built-in into the Angular Forms module and cover only the standard HTML form controls.
This means that if we want to implement our own custom form control, we are going to have to implement a custom value accessor for it as well.
The custom form control that we will build
Let's say that we want to build a custom form control that represents a numeric counter with increase and decrease buttons, for selecting an order quantity for example.
Each time that the buttons are pressed, the counter should be incremented or decremented by a configurable amount.
We can configure this control to specify a maximum value allowed for the form field, and mark it as invalid if the range is not met.
Here is what the numeric form control will look like:
And here is the implementation code for this simple component, without any form functionality added yet:
In its current form, this component is not compatible with either template-driven or reactive forms.
We would like to be able to add this component to a form in a very similar way to how we add a standard HTML input to a form, by either adding the
formControlName
or ngModel
directives to it:
We would also like for this component to be compatible with the built-in validators, and use them to make the field mandatory and set a maximum value:
But in the case of the current version of our control, if we would try to do so we would get an error:
ERROR Error: Must supply a value for form control with name: 'totalQuantity'.
at forms.js:2692
at forms.js:2639
at Array.forEach (<anonymous>)
at FormGroup._forEachChild (forms.js:2639)
at FormGroup._checkAllValuesPresent (forms.js:2690)
at FormGroup.setValue (forms.js:2490)
at CreateCourseStep1Component.ngOnInit (create-course-step-1.component.ts:51)
at callHook (core.js:3405)
at callHooks (core.js:3375)
at executeInitAndCheckHooks (core.js:3327)
In order to fix this error, and make the choose-quantity
component compatible with Angular Forms, we need to give this form control a value accessor, just like the case of native HTML elements like text inputs, checkboxes, etc.
In order to do that, we are going to make the component implement the
ControlValueAccessor
interface.
Understanding the ControlValueAccessor interface
Let's go over the methods of the ControlValueAccessor interface. Remember, they are not meant to be called directly by our code as these are framework callbacks.
All of these methods are meant to be called only by the Forms module at runtime, and they are meant to facilitate communication between our form control and the parent form.
Here are the methods of this interface, and how they work:
- writeValue: this method is called by the Forms module to write a value into a form control
- registerOnChange: When a form value changes due to user input, we need to report the value back to the parent form. This is done by calling a callback, that was initially registered with the control using the registerOnChange method
- registerOnTouched: When the user first interacts with the form control, the control is considered to have the status touched, which is useful for styling. In order to report to the parent form that the control was touched, we need to use a callback registered using the registerOnToched method
- setDisabledState: form controls can be enabled and disabled using the Forms API. This state can be transmitted to the form control via the setDisabledState method
And here is the component, with the ControlValueAccessor
interface already implemented:
Let's now go through each of the methods one by one, and explain how they were implemented.
Implementing writeValue
The writeValue
method is called by the Angular forms module whenever the parent form wants to set a value in the child control.
In the case of our component, we will take the value and assign it directly to the internal quantity
property:
Implementing registerOnChange
The parent form can set a value in the child control using writeValue
, but what about the other way around?
If the user interacts with the form control and increments or decrements the counter value, then the new value needs to be communicated back to the parent form.
The child control can notify the parent form that a new value is available via the use of a callback function.
The first step for this to work, is for the parent form to register the callback function with the child control, bt using the registerOnChange
method:
As we can see, when this method is called, we are going to receive our callback function, which we then save for later in a member variable.
The onChange
member variable is declared as a function, and initialized with an empty function, meaning a function with an empty body.
This way, if our program by some reason calls the function before the
registerOnChange
call was made, we won't run into any errors.
When the value of the counter is changed by either clicking on the increment or decrement buttons, then we need to notify the parent form that a new value is available.
We are going to do so by calling the callback function and reporting the new value:
Implementing registerOnTouched
Besides reporting new values back to the parent form, we also need to inform the parent form when the child control has been considered to be touched by the user.
When the form is initialized, every form control (and the form group as well) is considered to be in status untouched, and the ng-untouched
CSS class is applied to the form group and also to each of its individual child controls.
But when a child control gets touched by the user, meaning the user has tried to interact with it at least once, then the whole form is considered touched as well, meaning that the ng-touched
CSS class gets applied to the form.
These touched/untouched CSS classes are important for styling error messages in a form, so our custom form control needs to support this as well.
Like before, we need to have a callback registered, so that the child control can report its touched status back to the parent form:
We now need to call this callback when the control is considered touched, and this will happen whenever the user clicks any of the increment or decrement buttons at least once:
As we can see, when one of the two buttons gets clicked for the first time, we are going to call the onTouched
callback once, and the form control will now be considered as touched by the parent form.
The custom form control will have the ng-touched
CSS class applied to it as expected:
Implementing setDisabledState
Its also possible for the parent form to enable or disable any of its child controls, by calling the setDisabledState
method. We can keep the disabled status in the member variable disabled
, and use it in order to turn on and off the increment/decrement functionality:
Dependency injection configuration for ControlValueAccessor
Finally, the last piece of the puzzle to implement the ControlValueAccessor
interface correctly, is to register the custom form control as a known value accessor in the dependency injection system:
Without this configuration in place, our custom form control will not work correctly.
So what does this configuration do? We are adding the component to the list of known value accessors, that are all registered with the NG_VALUE_ACCESSOR
unique dependency injection key (also known as an injection token).
Notice the multi
flag set to true, this means that this dependency provides a list of values, and not only one value. This is normal because there are many value accessors registered with Angular Forms besides our own.
For example, all the built-in value accessors for standard text inputs, checkboxes, etc. are also registered under NG_VALUE_ACCESSOR
.
This way, whenever the Angular forms module needs the full list of all the value accessors available, all it has to do is to inject NG_VALUE_ACCESSOR
.
With this, our component is now capable of setting the value of a property in a form.
More than that, the component is now capable of participating in the form validation process and is already fully compatible with for example the built-in required
and max
validators.
But what if the component needs to have its own built-in validation rules, that are always active for every instance of the component, independently of the form configuration?
Introduction to the Validator interface
In the case of our custom form control, we would like for it to ensure that the quantity is positive. If it's not, then the form field should be marked as in error, and this should be true for all instances of the component, always.
In order to implement this logic, we are going to have our component implement the Validator
interface. This interface contains only two methods:
- validate: This method is used to validate the current value of the form control. This method will be called whenever a new value is reported to the parent form. The method needs to return null if no errors are found, or an error object containing all the details needed to correctly display a meaningful error message to the user.
- registerOnValidatorChange: This will register a callback that will allow us to trigger the validation of the custom control on demand. We don't need to do this when new values are emitted, as validation is already triggered in that case. We only need to call this method if some other input that also affects the behavior of
validate
has changed
Let's now see how to implement this interface, and do a final demo of the working component.
Implementing the Validator interface
The only method of Validator
that we really have to implement is the validate
method:
In this implementation, we return null
if the value is valid, and an error object containing all the details about the error.
In the case of our component, we didn't need to implement
registerOnValidatorChange
, as implementing this method is optional.
We only need this method if for example, our component had configurable validation rules, that depended on some of the component inputs. If that would be the case, we could trigger a new validation on demand in case one of the validation inputs changes.
In our case, the validate
implementation only depends on the current value of the control, so we didn't need to implement registerOnValidatorChange
and grab that callback for further use.
In order for the Validator interface to work properly, we also need to register our custom component with the NG_VALIDATORS
injection token:
Notice that without registering this class properly in NG_VALIDATORS
,
the validate
method will never get called.
Demo of a fully functional custom form control
And with the two interfaces ControlValueAccessor
and Validator
, we now have a fully functional custom form control, compatible with both reactive and template-driven forms, and capable of both setting the value of a form property and participating in the form validation process.
Here is the final code:
Let's now test this component at runtime, by adding it to a form with a couple of standard validators:
As we can see, we are making the field mandatory, and setting a maximum value of 100. The initial value of the control is 60, which is a valid value.
But what happens if we set the value to for example 110? The form will then become invalid, and the totalQuantity
control will have an error associated with it.
We can see the error by checking the value of the errors
property of
form.controls['totalQuantity']
:
As we can see, the Validators.max(100)
built-in validator kicked in and marked our custom form control in error as expected.
But what if instead, we set the quantity value to for example a negative -10 value? Here is what the errors
property of our control will now look like:
As we can see, now the validate
method created a ValidationErrors
object, which was then set as part of the errors of the form control.
We now have a fully functional custom form control, compatible with template-driven forms, reactive forms, and all built-in validators.
How to implement nested form groups (an address nested form)
A very common forms use case that can be implemented with what we have learned so far are nested form groups, that can be reused across multiple forms.
A good example of this is an address form, with all the usual address fields:
Now imagine that your application has many different forms where an address is required. We wouldn't want to repeat all the code needed for displaying and validating those fields across every form.
Instead, what we would like to do is to create a reusable form section under the form of an Angular component, that we could then plug into multiple forms, sort of a nested reusable sub-form.
Here is how we would like to use such an address form component:
As we can see, we would like to make our address form component fully compatible with Angular forms, meaning that it should support the ngModel
,
formControl
and formControlName
directives, and be able to participate in the validation of the parent form.
Sounds familiar?
Indeed, all we have to do for that is to implement just like we did before the
ControlValueAcessor
and Validator
interfaces. So how will this work?
First, we need to define the view of our nested address form component:
As we can see, our nested address form is also a from group itself, and it also uses Angular forms internally to collect the values of every address field and validate their values.
The form
object already contains all the information about the values and validity state of this subform. We can now use this information to quickly implement both the ControlValueAccessor
and Validator
interfaces:
Here are some important notes about this implementation:
- the form address component uses itself a form internally for doing all the form validation using a series of built-in validators
- we are using the principle of delegation as much as possible here. For example, we are getting all the information that we need from the
form
object - As an example of the delegation principle, we are implementing
writeValue
usingform.setValue
, and we are implementingsetDisabledState
by usingform.enable()
andform.disable()
- We are using the
valueChanges
Observable to know when a new value is emitted by the the address form, and calling theonChange
callback to notify the parent form. - As we are manually subscribing to the
valueChanges
Observable, we are also unsubscribing from it usingOnDestroy
, to avoid any memory leaks - we are implementing the
validate
method by checking if the embedded form controls have any errors and passing them to theValidationErrors
object
Demo of the nested address form
If we try to fill in the values of the address form, we are going to see that they are reported back to the parent form, and show up under the address
property.
This is value
property of the parent form containing address-form
, after typing in an address:
Summary
Every form control has linked to it a control value accessor, which is responsible for the interaction between the form control and the parent form.
And this includes all the standard HTML form controls like text inputs, checkboxes, etc., for which the forms module provides built-in control value accessors.
For a custom form control, we will have to build our own control value accessor by implementing the ControlValueAccessor
interface, and if we want the control to do custom value validation then we need to implement the Validator
interface.
We can also use the same technique to implement nested form groups (like an address sub-form), that can be reused across multiple forms.
I hope that you have enjoyed this post, if you would like to learn a lot more about Angular Forms, we recommend checking the Angular Forms In Depth course, where validators, custom form controls, and many other advanced form topics are covered in detail.
Also, if you have some questions or comments please let me know in the comments below and I will get back to you.
To get notified of upcoming posts on Angular, I invite you to subscribe to our newsletter:
And if you are just getting started learning Angular, have a look at the Angular for Beginners Course: