Angular Custom Form Controls & Nested Form Groups Made Easy

Angular Custom Form Controls & Nested Form Groups Made Easy

Giraffe with swim belt going to the ocean.Photo by ALLVISIONN

Forms can be as easy as a breeze and a piece of cake for simple cases. But things can get complicated pretty fast once you start building anything more complex than a registration form.

When things get tough with forms, there are a few things that will make your life easier, such as custom form controls and nested form groups. This is going to be the focus of this article. How to make our forms more reusable, how to split them, and make them more dynamic.

So, let’s get started!

Custom Form Controls (Control Value Accessors)

A custom form control is a directive that’s compatible with the Angular Forms API. By compatible, we mean that it can be used in a form just like any other native form control element.

Or equivalently with template-driven forms.

How neat and clean does this look?

On top of that, this custom form control can be implemented to be form-module agnostic. It can be used with template-driven forms and reactive forms alike.

Now, imagine that our application uses a lot of forms, which in turn have a lot of text fields. The majority of these text fields are identical and need to have a consistent display and behavior.

Let’s start by building a simple custom form control, called required-field. As its name suggests, it’s going to be an input field that’s required, displays an appropriate validation message, and adjusts its style based on its state (valid or invalid).

To do this, we need to implement 3 things.

1. Support everything a native text element does

To support everything a native form element does, we need to build a ControlValueAccessor (CVA). This interface acts as a bridge between the view and model of a form control.

Control Value Accessor (CVA) acts as a bridge between the view and the model

These two need to be in sync for the form control to work properly. When you type something in the field, the value needs to be sent back to the model. Respectively, when you set a value programmatically, that change needs to be reflected in the view.

Implementing the ControlValueAccessor interface is simple. We just need to implement 3 mandatory methods:

  • writeValue(value) — called by the forms API to write to the view when programmatic changes from model to view are requested
  • registerOnChange(fn) — provides a callback function fn that should be called whenever our input value changes
  • registerOnTouched(fn) — provides a callback function fn that should be called when our input is considered touched or blurred itself

and optionally 1 more:

  • setDisabledState(value) — called by the forms API when the control status changes to or from ‘DISABLED’

A few things take place in the previous snippet. We use writeValue to pass the new value to the display view. We use setDisabledState to store the disabled state. Lastly, we store the callback functions provided by registerOnChange and registerOnTouched.

In the template HTML file, we call the stored callback functions on (input) and on (blur) to allow the forms API to update itself.

Our CVA is ready for use and we need to provide it as such. We need to do this so that form control directives like NgModel and FormControlName can recognize our component as a value accessor.

2. Apply the required validation

Your first thought might have been to add the required attribute to the internal text field. But this wouldn’t work the way you expected it to.

First of all, we don’t know and shouldn’t make assumptions about what’s underneath the component view. It could be anything such as divs and spans that don’t support form validation attributes.

Most importantly, recall how we needed to implement a specific communication “protocol”, the CVA interface, to support native form element functionality? Well, we need to use a respective protocol to support validations as well.

We need to implement the Validator interface, which has 1 mandatory method:

  • validate(ctrl: AbstractControl) — performs synchronous validation against the provided control

That’s it! Our Validator is ready for use and we need to provide it as such. We need to do this so that form control directives like NgModel and FormControlName can recognize our component as a validator.

3. Display an error message and adjust its style when the control is invalid

In our previous step, the user could have added any custom validation on top of this control that we don’t know about. In this case, we need to get a hold of the model underneath and check whether it is valid or invalid.

The obvious way to do this is to create an input and pass through the form control instance itself.

That would work just fine. Except it would beat the purpose of creating the custom form control in the first place. If you have to pass through the control each time, it starts to become a little annoying after a while.

Instead, we want a way to access the form control internally without requiring the user to pass it in. But, how? Enter Dependency Injection!

We also said that our custom form control would be form-module agnostic, didn’t we?

To achieve this we need to inject NgControl instead of NgModel for template-driven forms or FormControlName for reactive forms. We can do this because both NgModel and FormControlName are subclasses of NgControl and provide themselves as NgControl.

And they call this the simple example? What is this @Self() decorator?

We use the @Self() decorator to limit the scope to just the element we are currently on. That is necessary because if our custom form control is wrapped in someone else’s form control, we might reach and get a false form control instance.

Another thing is that NgControl is injecting value accessors and validators. Recall how we said we need to provide our component as a value accessor and validator?

Well, we must not do both at the same time!

If we inject NgControl and NgControl is injecting value accessors, among which is our provided component, that’s a circular dependency right there! Since we need to get a hold of the model, we do need to inject NgControl. Therefore, we should remove the providers to avoid the circular dependency.

So now it’s our job to set up the NgControl correctly with the right value accessor and validators. Luckily for us, this is very easy to do.

For the value accessor part, we simply have to set the valueAccessor property of the control directive to this.

For the validator part, we can add validators programmatically.

We also need to modify the implementation of the writeValue method so it uses the injected control directive.

Finally, in the template file, we can now determine when to display a validation error message. In our case, if the control is touched and not valid.

Note: Although there is an invalid property as well, we’d still want to stick with the !valid option, so we can support asynchronous validators. That’s because asynchronous validators have a pending state while they’re processing. So while the control is pending, it’s neither valid nor invalid. So we need to say “not valid” specifically.

Nested Form Groups (Composite Value Accessors)

We’ve examined how to create a custom form control with a single input field. But what if we need to use multiple inputs, a group of fields, over and over? For example, what if we need the fields for personal information in different places in our application?

Do we copy-paste? Never!

Instead, we can use what we’ve learned to create a Composite CVA. Let’s create a new component called personal. We can also use our previous custom form control required-field inside our new component to make things even easier!

We made two modifications to our custom form control. We introduced an input named placeholder and an output named blur to the component. The output is necessary for bubbling up the onTouched event of the input field from within the required-field to its parent.

There are quite some changes in the TypeScript file of this component as well. We’ll walk through them one by one.

Firstly, we need to define the form and its validations.

All fields are required and if the user chooses the “Other” option for the gender field, s/he needs to specify the gender through an extra field, which is also required.

Next, we need to implement the CVA interface methods.

We modify the methods to use the form variable. The major difference is in the registerOnChange method. Recall that this method provides us with the callback that should be called whenever our input value changes. The difference is that now the input is the entire form. Thus, our best option is to call the callback whenever the valueChanges emits.

We also need to implement the Validator interface.

Now, you might be thinking, is this necessary? Haven’t we already included the validations in our form? The answer is we did, but only for the form that lives inside this component. If we don’t implement the interface, the parent form will have no way of telling what’s the state of this nested form.

Since we are not injecting NgControl this time, we also need to provide this component as a value accessor using the NG_VALUE_ACCESSOR token and as a validator using the NG_VALIDATORS token.

That’s it! We’ve implemented a form group to collect the personal information of a user. The form group is reusable and can be nested in existing reactive forms.

You can find a working demo at the StackBlitz below. Don’t forget to subscribe to my newsletter to stay tuned for more content like this!

**UPDATE**: We’ve implemented the PersonalComponent using NgControl injection. You can find the enhanced version of the previous demo in this StackBlitz link.

Conclusions

In this article, we examined a few ways to make our life easier when dealing with Angular Forms. We saw how we can create a custom form control that’s form-module agnostic. Then we used this knowledge to create a reusable nested form group that consists of multiple input fields.

You can learn more about Angular Forms in Kara Erickson’s amazing presentation in AngularConnect 2017.

Thanks for reading! I hope that you liked this article and that you’ve learned something new.

Happy coding!

More content at PlainEnglish.io. Sign up for our free weekly newsletter. Follow us on Twitter and LinkedIn. Check out our Community Discord and join our Talent Collective.

Alternate Text Gọi ngay