Monday, June 22, 2020

The Best Way to build reactive sub-forms with Angular

In this article we’re going to explore various approaches how to implement reactive sub-forms and discuss their trade-offs. At the end you will be able to make informed decision based on the particular use case to deliver best possible experience for your users and great developer experience for your colleagues!

Forms represent one of the most important parts of almost all frontend applications. From a single input field to a multi step wizards, forms enable dialog with the users by providing them with a way to submit many different kinds of data!

When developing Angular application, we will rarely end up with just a single form. It is much more common to implement many different forms for the individual features based on user requirements.

It is quite usual that some of the forms (or their parts) will feel repetitive as we keep re-implementing them over and over again!

An Example

Let’s say we’re implementing a customer form. There will most likely be a part of that customer form which will hold address data. We will use Angular reactive forms and a nested FormGroup to describe address object with properties (and inputs) for stuff like street, city or zip code.

The implementation using provided FormBuilder will look something like this…

Notice how we define nested form group using the FormBuilder .group() method to define address sub-form

The address sub-form is implemented inline using formGroupName directive on the <div> which allows us to access nested form controls like fromControlName="street" which are children of that element directly instead of writing "street.address".

We could achieve nice look and feel by using great Angular Material component library which provides nicely styled inputs (together with other form controls like selects, slides, …)

Cool, we have our customer form and it works just as we expected, users are happy and life is great!

Our app is getting successful and users would appreciate some new features. They want to be able to manage additional entities by themselves and as you may have already guessed it, we will want to store address for those entities too…

Let’s explore what are our options to make this happen

⚠️ Disclaimer: In this guide we’re going to explore many intermediate solutions and discuss their trade-offs before showing how to do it “The Best Way” so feel free to navigate to that section if you’re in hurry! Anyway, you can learn a lot by going through the whole content, the choice is yours😉

Requirements

Before we dive into proposed solutions, let’s quickly think about the requirements based on which we will evaluate them once finished!

  • minimize code duplication

1. Duplication

First of all, we could duplicate the address sub-form Typescript definition and HTML template for every parent form which needs to use it. This will work and it will be also easy to understand which is great but…

Parts of the code we would have to duplicate for every parent form which would use the address sub-form

As we will be adding address form to more and more parent forms, once we need to make a change it will get pretty time consuming. Let’s imagine that the backend service renamed the house number property to houseNumber.

We would have to find all the occurrences in every form definition and corresponding template and make the adjustments…

This does NOT sound very developer friendly or even safe for the users, we could miss something and it could lead even to nasty run-time errors!

⚖️ Sometimes, duplication is the correct answer

On the other hand, it might be possible to expect that the entity like Address could diverge based on particular use case as the application evolves.

For example an address of a building may need additional GEO data versus address of a person which would stay more “standard”. In that case having duplicated address form implementation would prove beneficial because we could adjust them individually. As always, think about your particular use case!


2. Dedicated sub-form component

As we have seen duplication is rarely the answer. It’s time to make some improvements!

The code and logic duplication can be improved by extracting sub-form into dedicated stand alone Angular component which then can be re-used in the every parent form which needs to manage given entity.

The first thing we could do is to extract sub-form template into dedicated component and then pass parent FormGroup through the sub-form components @Input() to bind it in its template…

Let’s have a look at the new extracted AddressForm component …

We’re reducing code duplication by extracting sub-form template in the dedicated component. The component has to receive FormGroup from the parent form through @Input() decorator…

Finished component can be integrated in the parent form…

We’re passing sub-form form group to the new child component

Our solution works but unfortunately is fragile. We have reduced template code duplication but the sub-form information is now located in two different places…

  • template in the extracted sub-form component

The problem is we are left to pure hope that no parent form will ever adjust the structure of the corresponding address form group in its reactive form definition.

It would be much better if everything which is important to the sub-form was also implemented in that extracted sub-form component…

Follow me on Twitter to get notified about the newest Angular blog posts and interesting frontend stuff!🐤

Better sub-form component

As we have seen, the problem is that we’re still duplicating form group definition in every parent form which makes our solution fragile.

What if instead of receiving form group defined in parent we could define desired form group in the child sub-form and make it available for the parent instead?

Instead of receiving address form group from the parent form through component @Input() property we’re creating the form group in the child sub-form instead!

Compared to previous solution we have done couple of changes

  1. remove the @Input() binding for the addressFormGroup

With this implementation ready, let’s see how we can integrate this in the parent customer form!

  1. remove [addressFormGroup] binding from the <my-org-address-form> as we’re no longer receiving the group definition from the parent

That’s it! We have created fully isolated reusable address sub-form which can be integrated in any parent Angular reactive form!

Even better, parent still keeps the full control over what should be the name of the field in which it wants to store that sub-form! The concerns were separated, isolation was achieved, just epic!

All the code that is related to the sub-form is isolated in the dedicated reusable component. Parent forms only use the component in their template and use its instance to create and assign form group definition.

We have ZERO code duplication and pretty minimalist implementation which should be easy to understand and maintain!

Still, there is one more thing we can do even better to make our sub-form even more robust…

👑 “The Best Way” to implement dedicated sub-form component

A sub-form is usually responsible for editing one type of entity (address in our case) and such entity is most likely retrieved from a backend to start with…

What would happen in a situation when the interface describing an Address object would change to accommodate for the backend API change? For example the a number property could change to a houseNumber …

Our nice sub-form implementation would compile just fine and we will most likely encounter runtime errors because Angular reactive forms are NOT type safe by default!

We can solve this problem by introducing a tiny generic helper interface!

Let’s unpack what is going on here! We’re defining a type FormGroupConfig which will accept some other generic type T, in our case it will be the Address interface.

The resulting type will have all the properties of an address because we’re iterating over them using [P in keyof T].

The value of each property (the reactive form config) will be an array with value which has to be of the same type as in Address interface because of T[P] . This is followed by optional any? to pass in validators array or even a full blown reactive form control config as we’re used to when implementing reactive forms.

The FormGroupConfig helper type forces us to correctly implement sub-form config based on the provided entity interface so we will get compilation errors any times these two get out of sync which is great!

The helper type can be made even more explicit to support all the possible form control configuration options in a type safe manner…

Fully featured FormGroupConfig helper type that enables use of all possible FormBuilder form control configuration options!

TIP: Such an interface can be implemented in the core module to make it available for the whole application

With our interface ready let’s adjust the implementation of our address sub-form component to make it type safe!

Yaas! Now we’re guaranteed that whenever there will be a change to an entity interface we will have to make appropriate changes to the form implementation or we will get compilation errors which is great!

Let’s summarize why this is the best way to implement sub-forms for Angular reactive forms…

  • sub-form is implemented in isolation

3. Custom ControlValueAccessor implementation

You might have heard about thing called ControlValueAccessor which enables us to implement custom inputs and use them seamlessly with the standard ones inside of Angular forms (both reactive and template driven).

The ControlValueAccessor may come in mind when thinking about possible ways to implement sub-forms but it is NOT really a good fit for the occasion…

Appropriate use cases

Let’s speak about some of the correct use cases for this concept first to highlight the differences and see why it is not a good fit for the sub-forms.

  1. rating widget

What do these custom inputs have in common?

All of these custom inputs, however complicated in their “view” part are only responsible for a single value like a rating or number of claps.

Compare this with an address sub-form which works with a collection of stand alone values like street or city.

We could argue that address can be viewed as a single value but it is not really how is it used, be it from user perspective or how it is handled on the backend. The sub-values of address like street or city are well defined concepts that have meaning on their own…

Example

To illustrate it even more realistically, address should be implemented as a sub-form when we want our user to populate multiple input fields like street, city or country…

Address could be implemented as a CustomValueAccessor if it was a map widget where you get the whole address by clicking on some point on the map.

The CustomValueAccessor should be only used to implement re-usable custom inputs widgets which provide great user experience when filling that type of single value

On the other hand, we should always implement sub-forms when dealing with a composite values like address or credit card…

Technical difficulties

What would happen if we ignored previous advice and decided to implement address sub-form as a ControlValueAccessor anyway?

We would encounter various inefficiencies and problems like:

  • much more verbose and complicated implementation

As we can see, CustomValueAccessor is really NOT a good way to implement sub-forms…

No comments:

Post a Comment