The Angular Forms and ReactiveForms modules come with a set of embedded directives that make it very simple to link standard HTML elements like inputs, checkboxes, text areas, etc. to a form group.
Along with those standard HTML elements, we may also use custom form controls, such as dropdowns, selection boxes, toggle buttons, sliders, or many other types of commonly used custom form components.
Table of Content
- 1. How are standard form controls functioning?
- 2. What are accessors of control value?
- 3. Custom form control that we're going to build
- 4. Understanding the ControlValueAccessor interface
- 5. Implementing writeValue
- 6. Implementing registerOnChange
- 7. Implementing registerOnTouched
- 8. Implementing setDisabledState
- 9. Configuration of dependency injection for ControlValueAccessor
- 10. Introduction to the Validation interface
- 11. Conclusion
For these customized controls, we wish to be able to configure them like form fields using the same directives (ngModel, formControl, formControlName) that we use for standard input boxes.
In this blog, we will learn exactly how to take an existing customized form control component and make it fully compatible with the Angular Forms API, to allow the component to participate in the parent form validation and value monitoring mechanisms.
It means the following:
- If we are using template-driven forms, we will be competent to plug the
- And for reactive forms, we may add the custom component to the form using formControlName (or formControl).
We will build in this guide a simple volume selector component, which can be used to increment or decrement a value. The component will be part of a form and will be marked in error if the counter fails to match a valid range.
The new custom form control will be fully compliant with embedded angular form validators. required, max along with any other built-in or customized validator.
How are standard form controls functioning?
First, we need to understand how the built-in form controls work to determine how to create a custom form control.
Native HTML elements including inputs, text areas, checkboxes, etc. are targeted by the built-in form controls.
An example of a simple form with a few plain HTML form fields is here:
We have a lot of standard form controls here, as we can see, with thePropertyformControlName added to it. And this is how to bind the HTML element to thetemplate.
The form value and validity state will be automatically re-calculated whenever the user interacts with the form inputs.
Read More: Introduction To Custom Pipes In Angular
What are accessors of control value?
What happens is that the Angular Forms module under the hood applies a built-in Angular directive to each native HTML element, which will be responsible for monitoring the value of the field and communicating it back to the parent type.
This type of special directive is referred to as the Control Value Accessor Directive.
Consider the checkbox field in the above form, for example. There is a built-in directive that is part of a module of reactive forms designed to monitor the importance of a checkbox specifically, and nothing more.
Here is the detailed declaration of this directive:
As we can see from the selector, this Value Tracking Directive specifically targets HTML inputs of the checkbox type only, but only if the ngModel, the formControl, or the properties of FormControlName are related to it.
If only checkboxes are targeted by this directive, then what about all other types of form controls, such as text inputs or text areas?
Well, each of these control forms has its own value accessor directive, which is different from the CheckboxControlValueAccessor.
All of these directives are built-in into the Angular Forms module and manage only the standard HTML form controls.
This indicates that if we want to implement our custom form control, we will also have to implement a custom value accessor for it.
Custom form control that we're going to build
Let's assume that we want to build a custom form control that defines a numeric counter with an increase and decrease button, for example, to select an order quantity.
The counter should be increased or decremented by a configurable amount each time when the buttons are pressed.This control can be configured to define the maximum allowable value for the form field and mark it as invalid if the range is not reached.
This is what the numeric form control would look like:
And here's the implementation code of that simple component, with no form functionality yet added:
This component is not compatible in its present form with either template-driven or reactive forms.
In a very similar way to how we add a standard HTML input to a form, we would like to be able to add this component to a form by either adding the form formControlName or ngModel directives to it:
Understanding the ControlValueAccessor interface
Let's go over the ControlValueAccessor interface methods. Note, as these are framework callbacks, they are not supposed to be directly called by our code.
All of these methods are intended to be called at runtime only by the Forms module and are intended to facilitate communication between our form control and the parent form.
Here are the methods of this interface, and how it works:
- writeValue: This method is called to write a value into a form control module by the Forms module.
- registerOnChange: We need to report the value back to the parent form when a form value changes due to user input. This is achieved by calling a callback, which 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 touched the status that is useful for styling. To report to the parent form that the control has been touched, we need to use the registered callback method using the RegisterOnToched method.
- setDisabledState: Using the Forms API, form controls can be allowed and disabled. We can transmit this state to the form control through the setDisabledState process.
And here's the component, already implemented with the ControlValueAccessor interface:
Now, let's go one by one through each of the methods and explain how they have been implemented.
Whenever the parent form wants to set a value in the child control, the writeValue method is called by the Angular form’s module.
We will take the value in the case of our component and assign it directly to the internal quantity property:
The parent form can set a value using writeValue in the child control, but what about the other way around?
The new value must be communicated back to the parent form unless the user interacts with the form control and increases or decreases the counter value.
The child control may notify the parent form that a new value is available through the use of the callback function.
One Stop Solution for Angular Web Development in USA ?
Your Search ends here.
The first step for this to work is to register the callback function with the child control for the parent form, by using the registerOnChange method:
As we can see, we can receive our callback function when this method is called, where we also save for later in a member variable.
The onChange member variable is declared as a function and initialized with an empty function, that is a function with an empty body.
This way, if, for some reason, our program calls the function before that we have made a RegisterOnChange call, and we will not run into any errors.
If either clicking the increment or decrement buttons changes the counter value, then we need to notify the parent form that a new value is accessible.
By calling the callback function and reporting the new value, we will do so:
In addition to evaluating new values back to the parent form, when the child control has been considered to be touched by the user, we also need to remind the parent form.
Each form control (and the form group as well) is considered untouched when the form is initialized, and the ng-untouched CSS class is applied to the form group and also to each of its child controls.
But if the child control is touched by the user, which means that the user has tried to interact with it at least once, then the whole form is also considered touched, meaning that the ng-touched CSS class is applied to the form.
For styling error messages in a form, these touched/untouched CSS classes are important, so our custom form control wants to support this as well.
As before, we need to have a callback registered so that the child control could also report the touched status back to the parent form:
When the control is considered touched, we need to call this callback, and this will occur if the user clicks at least once on any of the increment or decrement buttons:
As we can see, we can call the onTouched callback once when one of the two buttons get clicked for the first time, and the form control will now be considered to be touched by the parent form.
The parent type is also able to enable or disable all of its child controls by calling the method setDisabledState. We can keep the disabled status in the disabled member variable and use it to turn on and off the increment/decrement functionality:
Configuration of dependency injection for ControlValueAccessor
Finally, the last piece of the puzzle for the proper implementation of the ControlValueAccessor interface is to register the custom form control in the dependency injection method as a known value accessor:
Our custom type control will not work correctly without this configuration in place.
So, what is this configuration doing? We are adding a component to the list of known value accessors that are all registered with the unique NG VALUE ACCESSOR dependency injection key (also known as an injection token).
Note that the multi-flag is set to true, which means that this dependence provides a list of values, not just a single value. This is normal since, in addition to ours, there are several value accessors registered with Angular Types.
For example, under NG VALUE ACCESSOR, all built-in value accessors for standard text inputs, checkboxes, etc. are also registered.
Moreover, the component is now capable of participating in the process of form validation and is already fully compatible with the necessary built-in and max validators, for example.
But what if the component needs to get its own built-in validation rules, which are still active independently of the form configuration for any instance of the component?
Introduction to the Validation interface
In the case of our custom form control, we want to make sure the quantity is positive for it. If it is not, then the form field should be marked as being in error, and this should always be valid for all component instances.
To implement this logic, we will have the Validator interface introduced by our component. There are only two methods involved in this interface:
Validate: This method is used to validate the form control's current value. This method is called whenever the parent form is informed of a new value. If no errors are found, the method must return null, or an error object containing all the details needed to display a meaningful error message correctly to the user.
registerOnValidatorChange: This will register a callback which will enable us to trigger the custom control validation on demand. When new values are emitted, we do not need to do this, as validation, in that case, is already triggered. This method only needs to be called if any other input that also affects validation behavior has changed.
Let's now see how this interface can be applied, and do a final demo of the working component.
We return null if the value is valid in this implementation, and an error object containing all the error details.
We don't need to implement in the case of our component,registerOnValidatorChange, as it is optional to implement this method.
We need this method only if, for example, our component has configurable validation rules that depend on some of the inputs of the component. If that is the case, if one of the validation inputs changes, we might cause a new validation on demand.
In our case, the validation implementation depends only on the control's current value, so we didn't have to implement registerOnValidatorChange and take the callback for further usage.
We will need to register our custom component with the NG-VALIDATORS injection token for the Validator interface to work properly:
Note that without properly registering this class in NG VALIDATORS, the validation method never gets called.
In this blog, we have discussed the Complete Guide of Angular Custom Form Control in which we have discussed Standard form control functioning, Accessors, ControlValueAccessor interfaces with its method implementation. We have configured dependency injection for ControlValueAccessors. We have also described a validation interface with its implementation.