How I strongly typed my Angular forms. And you can too!
Originally published in Angular-In-Depth here.
This is part of the Angular Forms Story series:
- Angular Forms Story: Dev Tooling (link)
- Angular Forms Story: Strong Types (this one)
- Angular Forms Story: The Whole Story (coming up)
Angular Reactive Forms are not strongly typed! AbstractControl and it’s implementations FormControl FormGroup and FormArray do not support strong typing their value or changes or any other property/method. For the longest time, I accepted and worked around that, considering it just one of life’s things — so it goes…
In this article, I want to share the process I went through while gradually adding strong types to my forms. If you just want to see the end result see ngx-forms-typed. It’s strong type, backward compatible (works with Angular 2.4.0 !) and reduces boilerplate.
This work draws inspiration and ideas from “Working with Angular forms in an enterprise environment” and “Building scalable robust and type safe forms with Angular” and others. I would encourage you to check those articles out.
For my job, I had to do a reasonably large form, enough so to make me fear the business logic mixing with the form logic and sub-parts of the form. I wanted to have a strongly-typed , easy to use and @angular/forms_-y_ way to extract sub-forms components from a large form. Much like we extract components to handle their own concerns. The (sub)form component would need to conform to the following requirements:
- have a strongly typed model
- @angular/forms — compatible
- use existing abstraction for communication (the AbstractControl and the ControlValueAccessor) to handle validation, status changes, etc.
In short, I wanted to have a component that could be used as a single control in a larger form AND be a form in its own right with multiple fields, validation, status changes. And all the while keeping the parent form happily unaware of implementation details and communicating via the AbstractControl API.
For example Address as a sub-form in a Person form and in an Order form. But the full form in a NewAddress form.
In the beginning, I was decorating my FormGroup-s and their constituent FormControl-s with types, like this:
Now I could rely on strong typing during refactoring or adding/removing features. Any Person type change would trickle down to the form — for example, if the property address was to be added I would get an error immediately (vs run time and maybe):
Then I suffered the form.controls.get('name') way of reaching my controls. I did not like the pattern of creating a public getter for it. Mainly, I wanted type safety. So:
Now I had intellisense for my controls! Oh joy and productivity 😂⚒👷♂️. And with a bit more finesse (and beating Typescript into submission) I had a type-dependent form group (or model-dependent if you will). See this little change trickling down:
Did you notice the as unknown as PersonFormGroup (at the end of the form group instantiation form = new Form(…) as unknown …)— that’s what I was referring to as beating Typescript. In this case, I knew better than it what the actual shape of the run-time thing is! This is the only case so far usually, Typescript❤ ️knows better!
Taking this work to the next step is to type as much of the FormControl/FormGroup/FormArray types as possible. And the result is the package ngx-forms-typed. It provides the TypedFormControl (GitHub src)TypedFormGroupand TypedFormArray types.
For example, let’s take a look at TypedFormControl:
It adds a strong type to the value and valueChanges properties of FormControl so that you know what shape the value is when using it. It also strong types the method setValue so that you’d need to pass in a value of the expected type and maybe options — also strong typed. reset method is strongly typed too. See ResetValue source.
And helper functions for creating instances with those types typedFormControl(GitHub src), typedFormArray and typedFormGroup which make the creation of forms strongly typed too:
The function itself only instantiates a FormGroup which is to say that it is compatible with existing forms code and can be used next to it without breaking it:
I wanted to enable forms and sub-forms to communicate i.e. when sub-form get’s touched — touch the parent when it gets invalid — make the parent invalid too. These two are supported by the ControlValueAccessor abstraction. I also wanted to force the sub-form to get touched on cue from the parent, in order to show validation, which is not supported by ControlValueAccessor. I wanted to use existing ControlValueAccessor-AbstractControl channels of communication.
I came up with a builder-like pattern for interaction with controls of a form group/array. It comes in a function called forEachControlIn (see full code in GitHub). Its goal is to make interacting with controls in a form and between forms easy. It relies on having references to the forms.
Finally, to avoid boilerplate I tied it all in a ControlValueAccessorConnector (see full code on GitHub). It handles all the connection logic between a parent form and a sub-form.
See a working example here and code in Stackblitz:
Just like a user would expect, pressing the Submit button shows validation for the whole page. Blurring an input control shows validation only for that control.
There’s an example of a nested form using formGroup directive in the party-form.component and a stand-alone one, using ngModel in app.component.
You can see the library’s readme here.
This was a short introduction to the library. Would you like to see a deep dive? Vote here:
This approach (and package) is not the only one and there are several other approaches. Check out the next article in the series where I summarize my research in the @angular/forms space of packages, PRs, and articles.
I am working on a few Open Source Angular Dev Tooling projects. Check them out at:
SCuri — Unit test boilerplate automation (with Enterprise support option too)
ngx-forms-typed — Angular form, only strong typed!
ngx-show-form-control — Visualize/Edit any FormControl/Group