In the past, the AngularInDepth blog has included some very helpful articles showing how the ReactiveFormsModule
in @angular/forms
can make your life easier.
- Unleash the power 💪of Forms with Angular’s Reactive Forms
- Dive into Reactive Forms
- Angular: Nested Reactive Forms Using ControlValueAccessors(CVAs)
Today, we're going to talk about some of the problems with the ReactiveFormsModule
and discuss a proposal to fix many of these problems. The formal proposal can be found as an issue in the Angular repo #31963 (it appears to be the fastest growing issue at the moment 1). The goal of this post is to encourage feedback from the community on improving the ReactiveFormsModule
and fixing some of it's longstanding issues.
So you may be wondering, what issues are there with the ReactiveFormsModule
? Some of the biggest issues are:
- The module is not strongly typed
- It’s relatively complicated to display error messages, given how fundamental this task is. See #25824 #24981 #22319 #21011 #2240 #9121 #18114
- The methods for adding errors are inflexible. It is difficult to interface with async services to display errors (hence the need for different update strategies like on
blur
/ onsubmit
). In general, working with errors is more difficult than it should be. - Numerous annoyances with unfortunate API decisions.
- You can't bind a single form control to multiple inputs without ControlValueAccessor #14451
- Can't store arbitrary metadata on a control #19686
- Calling
reset()
doesn't actually reset the control to its initial value #20214 #19747 #15741 #19251 - Must call
markAsTouched()
/markAsUntouched()
instead of simplymarkTouched(boolean)
, which is more programmatically friendly #23414 #23336 - Creating custom form components is relatively complex #12248
- etc. #11447 #12715 #10468 #10195 #31133
- In addition to all the issues dealing with errors (
#3
above), the API does not offer low level programmatic control and can be frustratingly not extensible.- See issues #3009 #20230 related to parsing/formatting user input
- See issues #31046 #24444 #10887 #30610 relating to touched/dirty/etc flag changes
- See issues #30486 #31070 #21823 relating to the lack of ng-submitted change tracking
- Ability to remove FormGroup control without emitting event #29662
- Ability to subscribe to FormGroup form control additions / removals #16756
- Ability to mark ControlValueAccessor as untouched #27315
- Provide ControlValueAccessors for libraries other than
@angular/forms
#27672
Fundamentally, the existing AbstractControl class does not offer the extensibility / ease of use that such an important object should have. It's unlikely that any one API could solve everyone's problems all of the time, but a well designed API solves most peoples problems the majority of the time and can be extended to solve problems of arbitrary complexity when needed.
What follows is a proposal for a new AbstractControl
API powered by a ControlEvent
interface. In general, this proposal addresses issues 1, 3, 4, and 5, above. Importantly, this proposal is a completely community driven effort. The Angular team has not provided any feedback in regards to this proposal.
- The Angular issue associated with this proposal can be seen here: angular/angular#31963
- The github repo for this proposal can be seen here: https://github.com/thefliik/reactive-forms-2-proposal
- The github repo also contains stackblitz examples of the proposed API in action.
- The stackblitz demo also contains an example compatibility directive, letting the new AbstractControl be used with existing angular forms components (such as Angular Material components).
- A prototype module for the proposal has been published on npm at
reactive-forms-module2-proposal
this is just suitable for experimentation!
The proposed AbstractControl class has a source: ControlSource<PartialControlEvent>
property which is the source of truth for all operations on the AbstractControl
. The ControlSource
is just a modified rxjs Subject. Internally, output from source
is piped to the events
observable, which performs any necessary actions to determine the new AbstractControl
state before emitting a new ControlEvent
object describing any mutations which occurred. This means that subscribing to the events
observable will get you all changes to the AbstractControl
.
With this relatively modest change, we can accomplish a whole host of API improvements. Lets walk through some of them by example.
Alternatively, you can skip to an overview of the ControlEvent API.
It's important that the new API be very familiar to users of the existing ReactiveFormsModule
, and be 100% usable by folks who don't want to use observables.
New API:
https://gist.github.com/7f5e47b57ba99cf014d77e27c5463a1e
The new API allows us to subscribe to the changes of any property. When applied to ControlContainers such as FormGroup
and FormArray
, we can subscribe to nested child properties. Importantly, in the example below if the address
FormGroup is removed, then our subscription will emit undefined
. If a new address
FormGroup is added, then our subscription will emit the new value of the street
FormControl.
This also allows us to subscribe to controls
changes of a FormGroup
/FormArray
.
https://gist.github.com/82763147f4aff6d63145b3241b5294fa
This example can be seen on stackblitz
Here, by subscribing the source
of controlB
to the events
of controlA
, controlB
will reflect all changes to controlA
.
https://gist.github.com/3985baa0e5dcf5ab045e948550a76981
Multiple form controls can also be linked to each other, meaning that all events to one will be applied to the others. Because events are keyed to source ids, this does not cause an infinite loop.
https://gist.github.com/f170288fdf42c7babd34fee8c29f4079
This example can be seen on stackblitz
Here, a user is providing string
date values and we want a control with javascript Date
objects. We create two controls, one for holding the string
values and the other for holding the Date
values and we sync all changes between them. However, value changes from one to the other are transformed to be in the appropriate format.
https://gist.github.com/09b8580ab33bfce266221839f399fdd5
This example can be seen on stackblitz
Manually syncing changes between controls, as shown in Example 4
, above, can be somewhat of a hassle. In most cases, we just want to parse the user input coming from an input
element and sync the parsed values.
To simplify this process, FormControlDirective
/FormControlNameDirective
/etc accept optional "toControl", "toAccessor", and "accessorValidator" functions. In this example, we provide a stringToDate
function which receives an input string and transforms it into a javascript Date
, or null
if the string isn't in the proper format. Similarly, we provide a dateToString
function to sync our control's Date | null
values back to the input element. We also provide an optional accessorValidator
function to validate the input element's strings and provide helpful error messages to the user.
https://gist.github.com/818300ebe98fc4eae2a9a7c936bdb68a
This example can be seen on stackblitz
Here, a usernameControl
is receiving text value from a user and we want to validate that input with an external service (e.g. "does the username already exist?"). As with the other examples show, there is no obvious way of accomplishing this example with the current ReactiveFormsModule
API.
https://gist.github.com/3c25a1521abed3d7535945bbcf121c5b
Some things to note in this example:
- The API allows users to associate a call to
markPending()
with a specific key (in this case"usernameValidator"
). This way, callingmarkPending(false)
elsewhere (e.g. a different service validation call) will not prematurely mark this service call as "no longer pending". The AbstractControl is pending so long as any key istrue
. - Similarly, errors are stored associated with a source. In this case, the source is
'usernameValidator'
. If this service adds an error, but another service later says there are no errors, that service will not accidentally overwrite this service's error.- Importantly, the
errors
property combines all errors into one object.
- Importantly, the
Note: it's important to emphasize that, for standard usage, developers don't need to know about the existance of the ControlEvent
API. If you don't like observables, you can continue to simply use setValue()
, patchValue()
, etc without fear. For the purposes of this post however, lets look under the hood at what is going on!
At the core of this AbstractControl proposal is a new ControlEvent API which controls all mutations (state changes) to the AbstractControl. It is powered by two properties on the AbstractControl: source
and events
.
https://gist.github.com/946e56f57cc166e3efe3b1d7e45f3552
To change the state of an AbstractControl, you emit a new PartialControlEvent object from the source property. This object has the interface
https://gist.github.com/8c7ad9ae7c9036221e71c094350f1c03
When you call a method like AbstractControl#markTouched()
, that method simply constructs the appropriate ControlEvent
object for you and emits that object from control's ControlSource
(which itself is just a modified rxjs Subject
).
https://gist.github.com/0273b209c801e740878cca73dab24427
Internally, the AbstractControl subscribes to output from the source
property and pipes that output to a protected processEvent()
method. After being processed, a new ControlEvent
object containing any changes is emitted from the control's events
property (so when a subscriber receives a ControlEvent from the events
property, any changes have already been applied to the AbstractControl).
https://gist.github.com/ec43f8c702354b925a0e8a04b9be7364
You'll notice that only events which haven't yet been processed by this AbstractControl are processed (i.e. !event.processed.includes(this.id)
). This allows two AbstractControls to subscribe to each other's events without entering into an infinite loop (more on this later).
- You can check out the github repo to see the full AbstractControl interface proposal, as well as working implementations of
FormGroup
/FormArray
.
Now that we know a bit more about the ControlEvent
API, lets look at some examples it allows:
Say we have two FormControl's and we want them to have the same state. The new API provides a handy AbstractControl#replayState()
method which returns an observable of the ControlEvent
state changes which describe the current AbstractControl's state.
If you subscribe one FormControl's source to the replayState()
of another form control, their values will be made equal.
https://gist.github.com/5f4b152670758435172f5542f7938f0d
The replayState()
method also provides a flexible way of "saving" a control state and reapplying all, or parts of it, later.
https://gist.github.com/00e05aebaf1f0039828e89fee1b95aeb
Say you are changing a control's value programmatically via a "service A". Separately, you have another component, "component B", watching the control's value changes and reacting to them. For whatever reason, you want component B to ignore value changes which have been triggered programmatically by service A.
In the current ReactiveFormsModule
, you can change a control's value and squelch the related observable emission by passing a "noEmit" option. Unfortunately, this will affect everything watching the control's value changes. If we only want componentB to ignore a values emission, we're out of luck.
With this new API proposal, things are much easier. Every method which mutates an AbstractControl's state accepts a meta
option to which you can pass an arbitrary object. If you subscribe directly to a control's events
, then we can view any passed metadata.
Here, the subscription in the ngOnInit()
hook ignores changes with the myService: true
meta property.
https://gist.github.com/e64654d0eea2d00932ecfdf3c7818646
Lets use this proposal's FormControlDirective
implementation as an example (full code can be seen in the github repo). Say you're creating a custom directive which exposes a public FormControl, and you wish to provide "lifecycle hooks" for subscribers of that FormControl.
In the specific case of the
FormControlDirective
, I wanted the ability for aControlValueAccessor
connected to aFormControlDirective
to be notified when the "input" control of theFormControlDirective
changed.
Admittedly, this is an advanced use case. But these are precisely the kinds of corner cases which the current ReactiveFormsModule
handles poorly. In the case of our new API, we can simply emit a custom event from the control's source
. The control won't actually do anything with the event itself, but will simply reemit it from the events
observable. This allows anything subscribed to the events
observable to see these custom events.
In this example, a custom ControlAccessor
might want to perform special setup when a new input control is connected to MyFormControlDirective
.
https://gist.github.com/846dadf0814a15de677da3029f4a1a13
This far, we've focused on changes to the AbstractControl API. But some of the problems with the ReactiveFormsModule
stem from the ControlValueAccessor
API. While the ControlEvent
API presented thus far doesn't rely on any assumptions about the ControlValueAccessor
API, and it will work just fine with the existing ControlValueAccessor
interface, it also allows for a big improvement to the ControlValueAccessor
API.
At the risk of introducing too many new ideas at one time, lets look at how we can improve ControlValueAccessor
using the new ControlEvent
API...
As a reminder, the existing ControlValueAccessor
interface looks like
https://gist.github.com/438ecddfb89d5338f803cb961f154682
The proposed ControlEvent
API allows for a new ControlAccessor
API which looks like:
https://gist.github.com/0b736e3b3232de7c228fc1241a476a8f
With this update, the control
property of a directive implementing ControlAccessor
contains an AbstractControl
representing the form state of the directive (as a reminder, components are directives).
This would have several advantages over the current ControlValueAccessor API:
- Easier to implement
- When the form is touched, mark the control as touched.
- When the form value is updated, setValue on the control.
- etc
- Easier to conceptualize (admittedly subjective)
- Allows a ControlAccessor to represent a FormGroup / FormArray / etc, rather than just a FormControl.
- A ControlAccessor can represent an address using a FormGroup.
- A ControlAccessor can represent people using a FormArray.
- etc
- Very flexible
- You can pass metadata tied to changes to the ControlAccessor via the meta option found on the new AbstractControl.
- You can create custom ControlEvents for a ControlAccessor.
- If appropriate, you can access the current form state of a ControlAccessor via a standard interface (and you can use the
replayState()
method to apply that state to another AbstractControl) - If appropriate, a ControlAccessor could make use of a custom control object extending AbstractControl.
As a refresher, here is a simple FirstNameInputComponent
implemented using the existing (ReactiveFormsModule
) ControlValueAccessor
interface:
https://gist.github.com/de1149b72b42ba8dcab892379456c8cd
Here is the same component implemented using the proposed ControlAccessor
interface:
https://gist.github.com/4fd2d16d7796771de1a27930b80e011b
If we want to programmatically mark this ControlAccessor as touched, we can simple call this.control.markTouched(true)
. If we want to programmatically update the value, we can simply setValue()
, etc.
Lets look at a few more advanced examples of the benefits of the new ControlAccessor
API:
Here, we create a custom form control component for an email address. Our custom component performs async validation of input email addresses using a userService
. Similarly to Example 6, we mark the component as pending and debounce user input so that we don't make too many requests to our external service.
https://gist.github.com/888651f031b7736bfbac3a41afda1f4f
Here, we create a "user form" component which encapsulates the input fields for our user form. We also make use of our custom email address input component from the previous example. This control accessor represents its value using a FormGroup
, something which is not possible using the current ControlValueAccessor
API.
I'll also note that, because this component is also a
ControlContainerAccessor
, the use offormControlName
will pull directly from theapp-user-form
component'scontrol
property. I.e. in this case, we don't need to use a[formGroup]='control'
directive inside the component's template.
https://gist.github.com/ddbf8e9546c65d36d0940cfb90f33f1a
Here, we utilize our custom "user form" component (created in the previous example) as part of a signup form. If the user attempts to submit the form when it is invalid, we grab the first invalid control and focus it.
https://gist.github.com/d5a7477d8463d28f15467b67c95a21b0
While fixing the existing ReactiveFormsModule
is a possibility, it would involve many breaking changes. As Renderer -> Renderer2
has shown, a more user friendly solution is to create a new ReactiveFormsModule2
module, deprecate the old module, and provide a compatibility layer to allow usage of the two side-by-side (including using a new FormControl
with a component expecting an old ControlValueAccessor
).
There is also a lot more to this proposal than what was covered here. To take a look at the code and the current state of the proposal, as well as view stackblitz examples, head on over to the repo: https://github.com/thefliik/reactive-forms-2-proposal.
To provide your support or disapproval for the proposal, head on over to its Angular issue: angular/angular#31963.
To provide feedback, make Pull Requests / contributions, etc, head on over to the github repo: https://github.com/thefliik/reactive-forms-2-proposal.
A lot of the issues with the current FormControl API are ultimately issues with the current ValidatorFn
/ ValidationErrors
API.
Examples include:
- If a control is required, a
[required]
attribute is not automatically added to the appropriate element in the DOM.- Similarly, other validators should also include DOM changes (e.g. a
maxLength
validator should add a[maxlength]
attribute for accessibility, there are ARIA attributes which should be added for accessibility, etc). - If you validate to make sure an input is a number, it's appropriate to add a
type="number"
attribute on the underlying<input>
.
- Similarly, other validators should also include DOM changes (e.g. a
- Generating and displaying error messages is much harder than it should be, for such a fundamental part a Forms API.
Ultimately, I see these as failings of the current ValidatorFn
/ ValidationErrors
API, and should be addressed in a fix to that API. Any such fix should be included in any ReactiveFormsModule2
and incorporated into this AbstractControl API, but is currently out of scope for this particular proposal.
- The "fastest growing issue" statement is based off the fact that, in 3 months, it has risen to the second page of the Angular repo's issues when sorted by "thumbsup" reactions. It is the only issue on the first 4 pages to have been created in 2019.