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…
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"
.
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
- easy to use (re-use) in parent forms, simple interface
- robustness / type-safety
- easy to implement (we do NOT want to spend weeks on this)
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…
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 …
Finished component can be integrated in the parent form…
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
- form group definition in the every parent 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?
Compared to previous solution we have done couple of changes
- remove the
@Input()
binding for theaddressFormGroup
- inject
FormBuilder
in the constructor - add
createGroup()
method in which we create form group with all the necessary controls for the sub-form (eg street, number, city, … for the address sub-form) - store created form group in the sub-form component
addressFormGroup
property to be able to use it in the sub-form template - use created form group as a return value of the
createGroup()
method which will enable us to bind it in the parent form
With this implementation ready, let’s see how we can integrate this in the parent customer form!
- remove
[addressFormGroup]
binding from the<my-org-address-form>
as we’re no longer receiving the group definition from the parent - use
@ViewChild()
to get hold of the sub-form component instance, we’re using{ static: true }
option to make sure that our property has value already inside of thengOnInit
life-cycle hook which is where we’re creating our form definition usingFormBuilder
- inside of
ngOnInit
use the retrieved sub-form instance and it’screateGroup()
method to create and assignaddress
group definition
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 helper type can be made even more explicit to support all the possible form control configuration options in a type safe manner…
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
- there is no code duplication (smaller payload, easier to maintain)
- parent uses sub-form “as is” in its template
- sub-form defines its own reactive form group so parent can NOT get out of sync
- sub-form configuration is made type safe using helper type so it can NOT get out of sync with the entity it manages
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.
- rating widget
- claps widget (as here on the Medium, claps is basically a custom input👏)
- like widget
- date input widget (eg with 3 inputs for day, month and year)
- …
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
- manual propagation of the parent / child events
- no easy way to show validation inside of the sub-form on parent form submit event (we would have to track state ourselves by using
ngDoCheck
life-cycle hook or similar)
As we can see, CustomValueAccessor
is really NOT a good way to implement sub-forms…
No comments:
Post a Comment