There are many things that Angular helps us out with when creating forms. We've covered several topics on Forms in Angular 2, like model-driven forms and template-driven forms. If you haven't read those articles yet, we highly recommend you to do so as this one is based on them. Almero Steyn, one of our training students, who later on contributed to the official documentation as part of the Docs Authoring Team for Angular 2, has also written a very nice introduction to creating custom controls.
His article inspired us and we would like to take it a step further and explore how to create custom form controls that integrate nicely with Angular's form APIs.
Want to see things in action first?
Custom form control considerations
Before we get started and build our own custom form control, we want to make sure that we have an idea of the things come into play when creating custom form controls.
First of all, it's important to realise that we shouldn't just create custom form controls right away, if there's a native element (like
<input type="number">) that perfectly takes care of the job. It seems native form elements are often underestimated in what they are capable of. While we often just see a text box that we can type into, it does way more work for us. Every native form element is accessible, some inputs have built-in validation and some even provide an improved user experience on different platforms (e.g. mobile browsers).
So whenever we think of creating a custom form control we should ask ourselves:
- Is there a native element that has the same semantics?
- If yes, can we simply rely on that element and use CSS and/or progressive enhancement to change its appearance/behaviour to our needs?
- If not, what will the custom control look like?
- How can we make it accessible?
- Does it behave differently on different platforms?
- How does it validate?
There are probably more things to consider, but these are the most important ones. If we do decide to create a custom form control (in Angular 2), we should make sure that:
- It properly propagates changes to the DOM/View
- It properly propagates changes to the Model
- It comes with custom validation if needed
- It adds validity state to the DOM so it can be styled
- It's accessible
- It works with template-driven forms
- It works with model-driven forms
- It needs to be responsive
We will discuss different scenarios through out this article to demonstrate how these things can be implemented. We will not cover accessibility in this article though, as there'll be follow-up articles to talk about that in-depth.
Creating a custom counter
Let's start off with a rather simple counter component. The idea is to have a component that lets us increment and decrement a model value. And yes, if we think about the things to consider, we probably realise that an
<input type="number"> would do the trick.
However, in this article we want to demonstrate how to implement a custom form control and a custom counter component seems trivial enough to make things look not too complicated. In addition, our counter component will have a different appearance that should work the same across all browsers, which is where we might reach the boundaries of a native input element anyways.
We start off with the raw component. All we need is a model value that can be changed and two buttons that cause the change.
Nothing special going on here.
CounterInputComponent has a model
counterValue that is interpolated in its template and can be incremented or decremented by the
decrement() methods respectively. This component works perfectly fine, we can already use it as it is by putting it into another component like this:
Okay cool, but now we want to make it work with Angular's form APIs. Ideally, what we end up with is a custom control that works with template-driven forms and model-driven forms. For example, in the most simple scenario, we should be able to create a template-driven form like this:
If that syntax is new to you, check out our article on Template-Driven forms in Angular 2. Okay but how do we get there? We need to learn what a
ControlValueAccessor is, because that's the thing that Angular uses to build a bridge between a form model and a DOM element.
While our counter component works, there's currently no way we can connect it to an outer form. In fact, if we try to bind any kind of form model to our custom control, we'll get an error that there's a missing
ControlValueAccessor. And that's exactly what we need to enable proper integration with forms in Angular.
So, what is a
ControlValueAccessor? Well, remember the things we talked about earlier that are needed to implement a custom form control? One of the things we need to make sure is that changes are propagated from the model to the view/DOM, and also from the view, back to the model. This is what a
ControlValueAccessor is for.
ControlValueAccessor is an interface that takes care of:
- Writing a value from the form model into the view/DOM
- Informing other form directives and controls when the view/DOM changes
The reason why Angular has such an interface, is because the way how DOM elements need to be updated can vary across input types. For example, a normal text input has a
value property that needs to be written to, whereas a checkbox comes with a
checked property that needs to be updated. If we take a look under the hood, we realise that there's a
ControlValueAccessor for every input type which knows how to update its view/DOM.
DefaultValueAccessor that takes care of text inputs and textareas, the
SelectControlValueAccessor that handles select inputs, or the
CheckboxControlValueAccessor, which, surprise, deals with checkboxes, and many more.
Our counter component needs a
ControlValueAccessor that knows how to update the
counterValue model and inform the outside world about changes too. As soon as we implement that interface, it'll be able to talk to Angular forms.
ControlValueAccessor interface looks like this:
writeValue(obj: any) is the method that writes a new value from the form model into the view or (if needed) DOM property. This is where we want to update our
counterValue model, as that's the thing that is used in the view.
registerOnChange(fn: any) is a method that registers a handler that should be called when something in the view has changed. It gets a function that tells other form directives and form controls to update their values. In other words, that's the handler function we want to call whenever
counterValue changes through the view.
registerOnTouched(fn: any) Similiar to
registerOnChange(), this registers a handler specifically for when a control receives a touch event. We don't need that in our custom control.
ControlValueAccessor needs access to its control's view and model, which means, the custom form control itself has to implement that interface. Let's start with
writeValue(). First we import the interface and update the class signature.
Next, we implement
writeValue(). As mentioned earlier, it takes a new value from the form model and writes it into the view. In our case, all we need is updating the
counterValue property, as it's interpolated automatically.
This method gets called when the form is initialized, with the form model's initial value. This means it will override the default value
0, which is fine but if we think about the simple form setup we talked about earlier, we realise that there is no initial value in the form model:
This will cause our component to render an empty string. As a quick fix, we only set the value when it's not undefined:
Now, it only overrides the default when there's an actual value written to the control. Next, we implement
registerOnChange() has access to a function that informs the outside world about changes. Here's where we can do special work, whenever we propagate the change, if we wanted to.
registerOnTouch() registers a callback that is excuted whenever a form contorl is "touched". E.g. when an input element blurs, it fire the touch event. We don't want to do anything at this event, so we can implement the interface with an empty function.
Great, our counter input now implements the
ControlValueAccessor interface. The next thing we need to do is to call
propagateChange() with the value whenever
counterValue changes through the view. In other words, if either the
decrement() button is clicked, we want to propagate the new value to the outside world.
Let's update these methods accordingly.
We can make this code a little better using property mutators. Both methods,
counterValue changes. Let's use getters and setters to get rid off the redudant code:
CounterInputComponent is almost ready for prime-time. Even though it implements the
ControlValueAccessor interface, there's nothing that tells Angular that it should be considered as such. We need to register it.
Registering the ControlValueAccessor
Implementing the interface is only half of the story. As we know, interfaces don't exist in ES5, which means once the code is transpiled, that information is gone. So after all, it happens that our component implements the interface, but we still need to make Angular pick it up as such.
In our article on multi-providers in Angular 2 we learned that there are some DI tokens that Angular uses to inject multiple values, to do certain things with them. For example there's the
NG_VALIDATORS token that gives Angular all registered validators on a form control, and we can add our own validators to it.
In order to get hold of a
ControlValueAccessor for a form control, Angular internaly injects all values that are registered on the
NG_VALUE_ACCESSOR token. So all we need to do is to extend the multi-provider for
NG_VALUE_ACCESSOR with our own value accessor instance (which is our component).
Let's do that right away:
If this code doesn't make any sense to you, you should definitely check out our article on multi-providers in Angular 2, but the bottom line is, that we're adding our custom value accessor to the DI system so Angular can get an instance of it. We also have to use
CounterInputComponent will be already created as a directive dependency in the component that uses it. If we don't do that, we get a new instance as this is how DI in Angular works. The
forwardRef() call is explained in this article.
Awesome, our custom form control is now ready to be used!
Using it inside template-driven forms
We've already seen that the counter component works as intended, but now we want to put it inside an actual form and make sure it works in all common scenarios.
Activating form APIs
As discussed in our article on template-driven forms in Angular 2, we need to activate the form APIs like this:
Special Tip: In Angular 2 version >= RC5 we don't need
Without model initialization
That's pretty much it! Remember our
AppComponent from ealier? Let's create a template-driven form in it and see if it works. Here's an example that uses the counter control without initializing it with a value (it will use its own internal default value which is
Special Tip: Using the json pipe is a great trick to debug a form's value.
form.value returns the values of all form controls mapped to their names in a JSON structure. That's why
JsonPipe will out put an object literal with a
counter field of the value that the counter has.
Model initialization with property binding
Here's another example that binds a value to the custom control using property binding:
Two-way data binding with ngModel
And of course, we can take advantage of
ngModel 's two-way data binding implementation simply by changing the syntax to this:
How cool is that? Our custom form control works seamlessly with the template-driven forms APIs! Let's see what that looks like when using model-driven forms.
Using it inside model-driven forms
The following examples use Angular's reactive form directives, so don't forget to add
AppComponent as discussed in this article.
Binding value via formControlName
Once we've set up a
FormGroup that represents our form model, we can bind it to a form element and associate each control using
formControlName. This example binds a value to our custom form control from a form model:
Adding custom validation
One last thing we want to take a look at is how we can add validation to our custom control. In fact, we've already written an article on custom validators in Angular 2 and everything we need to know is written down there. However, to make things more clear we'll add a custom validator to our custom form control by example.
Let's say we want to teach our control to become invalid when its
counterValue is greater than
10 or smaller than
0. Here's what it could look like:
We register a validator function that returns
null if the control value is valid, or an error object when it's not. This already works great, we can display an error message accordingly like this:
Making the validator testable
We can do a little bit better though. When using model-driven forms, we might want to test the component that has the form without the DOM. In that case, the validator wouldn't exist, as it's provided by the counter input component. This can be easily fixed by extracting the validator function into its own declaration and exporting it, so other modules can import it when needed.
Let's change our code to this:
Special Tip: To make validator functions available to other modules when building reactive forms, it's good practice to declare them first and reference them in the provider configuration.
Now, the validator can be imported and added to our form model like this:
This custom control is getting better and better, but wouldn't it be really cool if the validator was configurable, so that the consumer of the custom form control can decide what the max and min range values are?
Making the validation configurable
Ideally, the consumer of our custom control should be able to do something like this:
Thanks to Angular's dependency injection and property binding system, this is very easy to implement. Basically what we want to do is to teach our validator to have dependencies.
Let's start off by adding the input properties.
Next, we somehow have to pass these values to our
validateCounterRange(c: FormControl), but per API it only asks for a
FormControl. That means we need to create that validator function using a factory that creates a closure like this:
Great, we can now create the validator function with the dynamic values we get from the input properties inside our component, and implement a
validate() method that Angular will use to perform validation:
This works but introduces a new problem:
validateFn is only set in
ngOnInit(). What if
counterRangeMin change via their bindings? We need to create a new validator function based on these changes. Luckily there's the
ngOnChanges() lifecycle hook that, allows us to do exactly that. All we have to do is to check if there are changes on one of the input properties and recreate our validator function. We can even get rid off
ngOnInit() again, because
ngOnChanges() is called before
Last but not least, we need to update the provider for the validator, as it's now no longer just the function, but the component itself that performs validation:
Believe or not, we can now configure the max and min values for our custom form control! If we're building template-driven forms, it simply looks like this:
This works also with expressions:
If we're building model-driven forms, we can simply use the validator factory to add the validator to the form control like this:
Angular 2 Master Class in Helsinki
Learn Angular 2 in our upcoming public training!