Reducing the forms boilerplate — make your Angular forms reusable
Reduce the forms boilerplate by creating reusable custom forms controls with the ControlValueAccessor interface. Learn how to create reusable forms, subforms and how to leverage the DI to make everything much cleaner!
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
At the end of this blog, you can find a Stackblitz project with all the examples.
One of the most powerful packages in Angular is definitely the Forms package. I think every developer who created forms without Angular will admire the simplicity and the functionality Angular Forms provide.
Even though, naturally forms often require a lot of boilerplate. In this piece, we will leverage Angular forms to create a generic forms solution.
Custom reusable forms controls
Understanding The Problem
In one of my projects, I required to create an authentication module for a company in the e-commerce industry. It might sound simple but then I realize there are 8 different pages in such module — Login, Register, reset password, social login, merge account and more. Most of the pages were sharing the same forms elements, with the same UI, same validations and also the same error messages.
Let’s take the email input as a use case:
Email input initial in an empty state, When the input is valid we want to indicate it to the user with a V sign, and we want to show an error message when it’s not.
I’ve counted, and, in a simple authentication project like this one, I’ve used the same email input 12 times!
The way to the solution
We want to create a Reusable generic form control.
Usually, when we think about reusing something with Angular, the first thing comes to our minds is wrap it in a component.
The requirements are:
- Should fit for type text and password
- Should validate on required and pattern
- Should show indication whether the control is valid or not
- Should show error messages
Let’s start by creating the component and use the @Input()
decorator to pass the configuration we need for our custom input:
And the template:
As you can see, the template has 4 sections. The input itself, which going to have a ngModel directive on it,V
section if the input is valid, *
section if the control is required and error message if the input is not valid.
Now, let’s try our custom input:
And the result (Check our `First Try` page — on the example section):
We get an exception right away.
What’s wrong? we’re trying to attach a form directive to something Angular doesn’t recognize as a form control.
The solution — The ControlValueAccessor interface:
We actually need a bridge between the Angular forms API and our native custom element. Angular provides us with an interface which let the framework know you can attach any form directive to it. The ControlValueAccessor
interface.
The ControlValueAccessor
interface has 4 methods, 3 of them are mandatory:
Let’s implement our first custom input control using the ControlValueAccessor
interface:
And the generic-input.component
template:
Now, implementing the interface is not enough, we also have to provide the NG_VALUE_ACCESSOR
token in the component metadata:
Validators?
We’ve created a custom form control, now let’s handle the validators as well. I’m going to implement the Validator
interface:
And our GenericComponent
will look like this:
Of course, don’t forget to add the provider for the NG_VALIDATORS
token:
Let’s try it again:
We not really showing an indication of whether the control is valid or not.
Getting the control reference
We want to show the user an indication of whether the control is valid or not, but, we don’t have the control instance.
Maybe we can use the DI to inject it? Yes, we can!
Few important things here:
- We’re injecting the
NgControl
which is the super class of bothformControlName
andngModel
, with that, we’re not coupling our form control to any of the template or reactive module. - We’re decorating our control with the
@Self()
decorator. That way we’re ensuring our control will not be overwritten by the injector tree. - We’re setting the control
valueAccessor
to point to theGenericComponent
class.
Now let’s update our template and use the control reference:
One thing we have to keep in mind is that NgControl
already provides the NG_VALUE_ACCESSOR
and the NG_VALIDATOR
tokens. If we provide them in the GenericComponent
class we might run into circular dependency issues, so — we better remove them:
We also need to set the validators for the control:
And the result (Checkout Login &Register pages in the examples sections):
Now everything Works!
Custom reusable forms
Understanding The Problem
Imagine a crazy scenario in which you want to use the same exact form in several places. Probably most of you needed to fill the address form twice in e-commerce websites if your billing address is different than your shipping address, right?
We don’t want to implement the same form functionality over and over again. We want to implement only one address form and reuse it.
Reusing? right, component!
The ControlValueAccessor, again:
Let’s first create our AddressComponent
:
And the template:
Now, let’s leverage the ControlValueAccessor
interface again, but now, to wrap the whole form and not a specific control:
Of course, don’t forget to provide the NG_VALUE_ACCESSOR
token:
Let’s try it (Checkout Reusable forms — ControlValueAccessor page in the examples sections):
Works!
Maybe there is a shorter way?
Tired of implementing the ControlValueAccessor
interface? me too.
In that case of reusing a complete form, we can actually use the Angular DI to inject the parent ControlContainer
:
A thing to notice here is that we using the viewProviders
and not the providers
. The reason is Angular using the @Host()
decorator on the ControlContainer
injection on both FormControlName
and NgModel
implementation. (Checkout Angular source code: FormControlName Directive, NgModel Directive). For more information about the @Host()
decorator I really recommend on this blog by Max Koretskyi aka Wizard.
After providing the ControlContainer
, we can use the DI to inject it into our AddressComponent
, and to set our address form to equal theControlContainer
form ref:
And the Result (Checkout Reusable forms — SubForms page in the examples sections):
Works!
Simple, shorter, but restrictive:
We have to remember that once we provided the FormGroupDirective
or the ngModelGroup
we’re creating a coupling to only one of the forms implementations (Template or Reactive).
Working demos:
Summary
Let’s summarize what we just learned:
- The
ControlValueAccessor
is a bridge between our components and the Angular Forms API, and will allow us to create reusable custom controls and custom reusable forms. - The Angular DI can help us inject the
NgControl
and set hisvalueAccessor
so we can have easy access to our control in the template. - We can leverage the DI again to inject the
FormGroupDirective
or thengModelGroup
to create a sub-form.
More References
If you looking for more in-depth content about the ControlValueAccessor
I highly recommend this blog by Max Koretskyi aka Wizard and Kara Erickson talk from AngularConnect.
Thanks for reading!
Visit my website for more content.
Contact me on Twitter.
Special thanks for reviewing: Uri Shaked, Itay Oded, Denis Levkov