You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 / on submit). In general, working with errors is more difficult than it should be.
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.
Fundamentally, the existing AbstractControl class does not offer the extensibility / ease of use that such an important object should have. This is a proposal to re-think the design of AbstractControl for inclusion in an eventual ReactiveFormsModule2. In general, it addresses points 1, 3, 4, and 5, above.
The 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 of the proposal has been published at reactive-forms-module2-proposalthis is just suitable for experimentation!
The proposed interface is shown below.
The focus of this proposal is on the API of AbstractControl, not the specific implementation I've created.
The new 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 ControlEvent object. This means that subscribing to the events observable will get you all changes to the AbstractControl.
Below are a few somewhat advanced examples of the benefits / flexibility of this new API (there are additional examples on stackblitz). Because AbstractControl is abstract (and cannot be instantiated), these example use a simple FormControl object that looks like so:
To begin, the new API should be very familiar to users of the old API.
constvalidatorFn: ValidatorFn=control=>typeofcontrol.value==='string' ? null : {invalidValue: true};constcontrol=newFormControl('',{validators: validatorFn,});control.setValidators(null);control.value;// ""// get current value and also changescontrol.observe('value').subscribe(value=>{// do stuff ...});// just subscribe to changescontrol.observeChanges('errors').subscribe(errors=>{// do stuff ...});control.setValue('string');control.touched;// falsecontrol.markTouched(true);// etc...
Example 1: linking one FormControl to another FormControl
Here, by subscribing the source of controlB to the events of controlA, controlB will reflect all changes to controlA.
Multiple form controls can also be linked to each other, meaning that all changes to one will be applied to the others. Because changes are keyed to source ids, this does not cause an infinite loop (as can be seen in the stackblitz example).
Example 2: subscribe to a nested property of a FormGroup
Here, we subscribe to validity changes of the firstName control of a nested form group. Everything is properly typed:
constformGroup=newFormGroup({userId: newFormControl(1),people: newFormArray([newFormGroup({id: newFormControl(1),firstName: newFormControl('John'),}),]),});formGroup.observe('controls','people','controls',0,'controls','firstName','valid').subscribe(valid=>{// ... do stuff;})
Importantly, this subscription will also emit if the validity changes because a control is replaced or removed. For example, if the FormArray has the FormGroup at index 0 removed, then this subscription will emit undefined. If a new FormGroup is later added at index 0, the subscription will emit to reflect the new firstName control's valid property.
This also allows us to subscribe to changes to the controls property of a form group, to be made aware of control additions / removals (or any other property of an AbstractControl).
Example 3: dynamically parse a control's text input
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.
To make things easier, the FormControlDirective / FormControlNameDirective / etc directives allow users to inject a ValueMapper object. This value mapper has toControl and toAccessor transform functions which will transform the control and input values, respectively. Optionally, you can also provide a accessorValidator function which validates the input values before they are transformed.
This hypothetical example transforms input string values into javascript Date objects (this can also be seen on stackblitz in example-three).
Example 4: validating the value of an AbstractControl via a service
Here, a usernameControl is receiving text value from a user and we want to validate that with an external service (e.g. "does the username already exist?").
constusernameControl=newFormControl();// here we want to always receive value updates, even if they were made with "noEmit"usernameControl.validationEvents.pipe(filter(event=>event.label==="End"),tap(()=>this.usernameControl.markPending(true,{source: 'userService'})),debounceTime(500),switchMap(event=>this.userService.doesNameExist(event.controlValue)),tap(()=>this.usernameControl.markPending(false,{source: 'userService'})),).subscribe(response=>{consterrors=response.payload ? {userNameExists: true} : null;this.usernameControl.setErrors(errors,{source: 'userService',});});
Some things to note in this example:
The API allows users to associate a call to markPending() with a specific key (in this case "userService"). This way, calling markPending(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 is true.
Internally, errors are stored associated with a source. In this case, the source is 'userService'. 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.
Example 5: using dependency injection to dynamically add new validator functions to a control
In the existingReactiveFormsModule, when you pass a control to a FormControlDirective via [formControl], that directive may dynamically add validator functions to the control. It does this by creating a new validator function which combines the control's existing validator function(s) with any additional validator functions the FormControlDirective has had injected. It then replaces the control's existing validator function with the new one. This process is complex and can lead to bugs. For example, after this process is complete there isn't any way to determine which validator functions were added by the user vs which ones were added dynamically.
Here, validators are internally stored keyed to a source id (similar to errors). If a FormControl is passed to a directive which dynamically injects additional validator functions, those functions will be stored separately from the FormControl's other functions (and are deleted separately). This leads to more consistent, predictable behavior that an unknowledgeable user cannot mess with.
@Directive({selector: 'myControlDirective',})classMyControlDirective{staticid=0;
@Input('myControlDirective')control: AbstractControl;privateid=Symbol(`myControlDirective ${MyControlDirective.id}`);constructor(
@Optional()
@Self()
@Inject(NG_VALIDATORS_2)privatevalidators: ValidatorFn[]|null,){MyControlDirective.id++;}ngOnChanges(changes: SimpleChanges){if(changes.control.previousValue){// clear injected validators from the old controlconstoldControl=changes.control.previousValue;oldControl.setValidators(null,{source: this.id,});}// add injected validators to the new controlthis.control.setValidators(this.validators,{source: this.id,});}}
The interface
interfaceAbstractControl<Value=any,Data=any>{/** * The ID is used to determine where StateChanges originated, * and to ensure that a given AbstractControl only processes * values one time. */readonlyid: ControlId;data: Data;/** * **Warning!** Do not use this property unless you know what you are doing. * * A control's `source` is the source of truth for the control. Events emitted * by the source are used to update the control's values. By passing events to * this control's source, you can programmatically control every aspect of * of this control. * * Never subscribe to the source directly. If you want to receive events for * this control, subscribe to the `events` observable. */source: ControlSource<PartialControlEvent>;/** An observable of all events for this AbstractControl */events: Observable<ControlEvent&{[key: string]: any}>;readonlyvalue: DeepReadonly<Value>;readonlyerrors: ValidationErrors|null;/** * A map of validation errors keyed to the source which added them. */readonlyerrorsStore: ReadonlyMap<ControlId,ValidationErrors>;readonlydisabled: boolean;readonlyenabled: boolean;readonlyvalid: boolean;readonlyinvalid: boolean;readonlypending: boolean;/** * A map of pending states keyed to the source which added them. * So long as there are any `true` boolean values, this control's * `pending` property will be `true`. */readonlypendingStore: ReadonlyMap<ControlId,true>;readonlystatus: 'DISABLED'|'PENDING'|'VALID'|'INVALID';/** * focusChanges allows consumers to be notified when this * form control should be focused or blurred. */focusChanges: Observable<boolean>;/** * These are special, internal events which signal when this control is * starting or finishing validation. * * These events are not emitted from the `events` observable. */validationEvents: Observable<ValidationEvent>;readonlyreadonly: boolean;readonlysubmitted: boolean;readonlytouched: boolean;readonlychanged: boolean;readonlydirty: boolean;/** * A map of ValidatorFn keyed to the source which added them. * * In general, users won't need to access this. But it is exposed for * advanced usage. */readonlyvalidatorStore: ReadonlyMap<ControlId,ValidatorFn>;/** * ***Advanced API*** * * The "atomic" map is used by controls + parent ControlContainers to ensure * that parent/child state changes happen atomically before any events are * emitted. */readonlyatomic: Map<ControlId,(event: ControlEvent)=>(()=>void)|null>;[AbstractControl.ABSTRACT_CONTROL_INTERFACE](): this;observeChanges<T=any>(props: string[],options?: {ignoreNoEmit?: boolean},): Observable<T>;observeChanges<Aextendskeyof this>(a: A,options?: {ignoreNoEmit?: boolean},): Observable<this[A]>;observeChanges<Aextendskeyof this,Bextendskeyof this[A]>(a: A,b: B,options?: {ignoreNoEmit?: boolean},): Observable<this[A][B]|undefined>;observe<T=any>(props: string[],options?: {ignoreNoEmit?: boolean},): Observable<T>;observe<Aextendskeyof this>(a: A,options?: {ignoreNoEmit?: boolean},): Observable<this[A]>;observe<Aextendskeyof this,Bextendskeyof this[A]>(a: A,b: B,options?: {ignoreNoEmit?: boolean},): Observable<this[A][B]|undefined>;equalValue(value: Value): value is Value;setValue(value: Value,options?: ControlEventOptions): void;patchValue(value: any,options?: ControlEventOptions): void;/** * If provided a `ValidationErrors` object or `null`, replaces the errors * associated with the source ID. * * If provided a `Map` object containing `ValidationErrors` keyed to source IDs, * uses it to replace the `errorsStore` associated with this control. */setErrors(value: ValidationErrors|null|ReadonlyMap<ControlId,ValidationErrors>,options?: ControlEventOptions,): void;/** * If provided a `ValidationErrors` object, that object is merged with the * existing errors associated with the source ID. If the error object has * properties containing `null`, errors associated with those keys are deleted * from the `errorsStore`. * * If provided a `Map` object containing `ValidationErrors` keyed to source IDs, * that object is merged with the existing `errorsStore`. */patchErrors(value: ValidationErrors|ReadonlyMap<ControlId,ValidationErrors>,options?: ControlEventOptions,): void;markTouched(value: boolean,options?: ControlEventOptions): void;markChanged(value: boolean,options?: ControlEventOptions): void;markReadonly(value: boolean,options?: ControlEventOptions): void;markSubmitted(value: boolean,options?: ControlEventOptions): void;markPending(value: boolean|ReadonlyMap<ControlId,true>,options?: ControlEventOptions,): void;markDisabled(value: boolean,options?: ControlEventOptions): void;focus(value?: boolean,options?: ControlEventOptions): void;setValidators(value:
|ValidatorFn|ValidatorFn[]|null|ReadonlyMap<ControlId,ValidatorFn>,options?: ControlEventOptions,): void;/** * Returns an observable of this control's state in the form of * StateChange objects which can be used to make another control * identical to this one. This observable will complete upon * replaying the necessary state changes. */replayState(options?: ControlEventOptions): Observable<ControlEvent>;/** * A convenience method for emitting an arbitrary control event. */emitEvent<TextendsPartialControlEvent=PartialControlEvent&{[key: string]: any}>(event: Partial<Pick<T,'id'|'meta'|'source'|'processed'|'noEmit'|'meta'>>&Omit<T,'id'|'meta'|'source'|'processed'|'noEmit'|'meta'>&{type: string;},): void;}interfacePartialControlEvent{id?: string;source: ControlId;readonlyprocessed: ControlId[];type: string;meta?: {[key: string]: any};noEmit?: boolean;}interfaceControlEventextendsPartialControlEvent{id: string;meta: {[key: string]: any};}
Wrapping up
There's a lot packed in to this API update. For a full overview, you should check out the repo.
Two other details to note:
When you pass the noEmit option to a function, that squelches emissions from any observe and observeChanges observables, but it does not effect the events observable. This is a good thing. It means that library authors can hook into the pure stream of control events on an AbstractControl and choose to honor or ignore noEmit as appropriate (via an observable operator like filter()).
All methods that will emit offer a meta option that accepts an arbitrary metadata object that will be included in the control event object. This greatly increases customizability / extensibility, as you can attach custom information to any action and access that custom information on the ControlEvent objects.
Things not included in this proposal
Validation
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>.
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, but they should be discussed in a separate issue.
ControlValueAccessor
This proposal does not touch ControlValueAccessor and this proposal works with the existing ControlValueAccessor API. This decision was again made to focus discussion on AbstractControl.
This being said, this API allows for the ControlValueAccessor interface to be changed to simply:
I mention this possible change to ControlValueAccessor mainly as a way of highlighting how flexible/powerful this new AbstractControl API is. With this update to ControlValueAccessor, the control property of a directive contains an AbstractControl representing the form state of the directive (as a reminder, components are directives).
Broadly speaking, this ControlAccessor API has 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 ControlValueAccessor to represent a FormGroup / FormArray / etc, rather than just a FormControl.
A ControlValueAccessor can represent an address using a FormGroup.
A ControlValueAccessor can represent people using a FormArray.
etc
Very flexible
You can pass metadata tied to changes to the ControlValueAccessor via the meta option found on the new AbstractControl.
You can create custom ControlEvent events for a ControlValueAccessor.
If appropriate, you can access the current form state of a ControlValueAccessor via a standard interface (and you can use the replayState() method to apply that state to another AbstractControl)
If appropriate, a ControlValueAccessor could make use of a custom object extending AbstractControl.
In terms of specifics, this ControlValueAccessor change is made possible because this new API allows you to make two form controls identical (via replayState()) and to link two form controls so they maintain identical states. This means that, in cases where a directive/component is receiving an AbstractControl as some sort of input, you can easily create a readonly reference control which represents the current input control's state.
For example, where before in ngOnChanges you might have this (and consumers of MyComponent would need to deal with changes to control):
Notice that this version insulates consumers of your directive/component from changes to the providedControl, allowing them to interface with a static (readonly) reference control.
While this might seem like a small difference, the ability to have static references to controls ends up being a pretty big deal in terms of user-friendliness. For example, if you inject NgFormControlDirective into your component, you don't need to deal with changes to the formControl property of the NgFormControlDirective. You can simply subscribe to NgFormControlDirective#control and know that that control will always have the same state as NgFormControlDirective#formControl.
There is a blog post which goes into greater detail showing how this updated ControlAccessor would work.
Describe alternatives you've considered
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, depricate the old module, and provide a compatibility layer to allow usage of the two side-by-side.
🚀 feature request
Relevant Package
This Proposal is for @angular/forms
Description
The ReactiveFormsModule is pretty good, but it has a number of problems.
ControlValueAccessor<T>#19340 (forms) Add types for "OnChangeCallback" and "OnTouchedCallback" #19329 relating to ControlValueAccessorblur/ onsubmit). In general, working with errors is more difficult than it should be.reset()doesn't actually reset the control to its initial value feat(forms): introduce initialValue to AbstractControl #20214 [Proposal] Initial / Original value API for AbstractControl #19747 FormGroup reset() doesn't reset custom form control #15741 FormGroup.disable() and FormGroup.enable() do not allow resetting disabled state. #19251markAsTouched()/markAsUntouched()instead of simplymarkTouched(boolean), which is more programmatically friendly Add AbstractControl.setEnabled(enabled: boolean) #23414 Prevent FormArray push to emit event #23336#3above), the API does not offer low level programmatic control and can be frustratingly not extensible.@angular/formsProvide ControlValueAccessors for libraries other than @angular/forms #27672Describe the solution you'd like
Fundamentally, the existing
AbstractControlclass does not offer the extensibility / ease of use that such an important object should have. This is a proposal to re-think the design ofAbstractControlfor inclusion in an eventualReactiveFormsModule2. In general, it addresses points 1, 3, 4, and 5, above.AbstractControlbe used with existing angular forms components (such as Angular Material components).reactive-forms-module2-proposalthis is just suitable for experimentation!I wrote a blog post about this issue
Overview:
The new
AbstractControlclass has asource: ControlSource<PartialControlEvent>property which is the source of truth for all operations on the AbstractControl. TheControlSourceis just a modified rxjsSubject. Internally, output fromsourceis piped to theeventsobservable, which performs any necessary actions to determine the newAbstractControlstate before emitting aControlEventobject. This means that subscribing to theeventsobservable will get you all changes to theAbstractControl.Below are a few somewhat advanced examples of the benefits / flexibility of this new API (there are additional examples on stackblitz). Because
AbstractControlis abstract (and cannot be instantiated), these example use a simpleFormControlobject that looks like so:Example 0: the new API is similar to the old API
To begin, the new API should be very familiar to users of the old API.
Example 1: linking one FormControl to another FormControl
Here, by subscribing the source of
controlBto the events ofcontrolA,controlBwill reflect all changes tocontrolA.Multiple form controls can also be linked to each other, meaning that all changes to one will be applied to the others. Because changes are keyed to source ids, this does not cause an infinite loop (as can be seen in the stackblitz example).
Example 2: subscribe to a nested property of a FormGroup
Here, we subscribe to validity changes of the
firstNamecontrol of a nested form group. Everything is properly typed:Importantly, this subscription will also emit if the validity changes because a control is replaced or removed. For example, if the
FormArrayhas theFormGroupat index0removed, then this subscription will emitundefined. If a newFormGroupis later added at index0, the subscription will emit to reflect the newfirstNamecontrol'svalidproperty.This also allows us to subscribe to changes to the
controlsproperty of a form group, to be made aware of control additions / removals (or any other property of an AbstractControl).Example 3: dynamically parse a control's text input
Here, a user is providing
stringdate values and we want a control with javascriptDateobjects. We create two controls, one for holding thestringvalues and the other for holding theDatevalues and we sync all changes between them. However, value changes from one to the other are transformed to be in the appropriate format.As with the other examples, demoing this on stackblitz might be helpful.
To make things easier, the
FormControlDirective/FormControlNameDirective/ etc directives allow users to inject aValueMapperobject. This value mapper hastoControlandtoAccessortransform functions which will transform the control and input values, respectively. Optionally, you can also provide aaccessorValidatorfunction which validates the input values before they are transformed.Usage is like:
This hypothetical example transforms input string values into javascript
Dateobjects (this can also be seen on stackblitz inexample-three).Example 4: validating the value of an AbstractControl via a service
Here, a
usernameControlis receiving text value from a user and we want to validate that with an external service (e.g. "does the username already exist?").Some things to note in this example:
markPending()with a specific key (in this case "userService"). 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 anykeyis true.'userService'. 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.errorsproperty combines all errors into one object.Example 5: using dependency injection to dynamically add new validator functions to a control
In the existing
ReactiveFormsModule, when you pass a control to aFormControlDirectivevia[formControl], that directive may dynamically add validator functions to the control. It does this by creating a new validator function which combines the control's existing validator function(s) with any additional validator functions theFormControlDirectivehas had injected. It then replaces the control's existing validator function with the new one. This process is complex and can lead to bugs. For example, after this process is complete there isn't any way to determine which validator functions were added by the user vs which ones were added dynamically.Here, validators are internally stored keyed to a source id (similar to errors). If a FormControl is passed to a directive which dynamically injects additional validator functions, those functions will be stored separately from the FormControl's other functions (and are deleted separately). This leads to more consistent, predictable behavior that an unknowledgeable user cannot mess with.
The interface
Wrapping up
There's a lot packed in to this API update. For a full overview, you should check out the repo.
Two other details to note:
noEmitoption to a function, that squelches emissions from anyobserveandobserveChangesobservables, but it does not effect theeventsobservable. This is a good thing. It means that library authors can hook into the pure stream of control events on an AbstractControl and choose to honor or ignorenoEmitas appropriate (via an observable operator likefilter()).metaoption that accepts an arbitrary metadata object that will be included in the control event object. This greatly increases customizability / extensibility, as you can attach custom information to any action and access that custom information on the ControlEvent objects.Things not included in this proposal
Validation
A lot of the issues with the current FormControl API are ultimately issues with the current
ValidatorFn/ValidationErrorsAPI.Examples include:
[required]attribute is not automatically added to the appropriate element in the DOM.[maxlength]attribute for accessibility, there are ARIA attributes which should be added for accessibility, etc).number, it's appropriate to add atype="number"attribute on the underlying<input>.Ultimately, I see these as failings of the current
ValidatorFn/ValidationErrorsAPI, and should be addressed in a fix to that API. Any such fix should be included in anyReactiveFormsModule2, but they should be discussed in a separate issue.ControlValueAccessor
This proposal does not touch ControlValueAccessor and this proposal works with the existing ControlValueAccessor API. This decision was again made to focus discussion on AbstractControl.
This being said, this API allows for the
ControlValueAccessorinterface to be changed to simply:You can see an example of this in the repo.
I mention this possible change to ControlValueAccessor mainly as a way of highlighting how flexible/powerful this new AbstractControl API is. With this update to ControlValueAccessor, the
controlproperty of a directive contains anAbstractControlrepresenting the form state of the directive (as a reminder, components are directives).Broadly speaking, this ControlAccessor API has several advantages over the current ControlValueAccessor API:
setValueon the control.ControlValueAccessorto represent aFormGroup/FormArray/ etc, rather than just aFormControl.addressusing a FormGroup.peopleusing a FormArray.metaoption found on the newAbstractControl.ControlEventevents for aControlValueAccessor.ControlValueAccessorvia a standard interface (and you can use thereplayState()method to apply that state to another AbstractControl)ControlValueAccessorcould make use of a custom object extendingAbstractControl.In terms of specifics, this
ControlValueAccessorchange is made possible because this new API allows you to make two form controls identical (viareplayState()) and to link two form controls so they maintain identical states. This means that, in cases where a directive/component is receiving an AbstractControl as some sort of input, you can easily create a readonly reference control which represents the current input control's state.For example, where before in ngOnChanges you might have this (and consumers of
MyComponentwould need to deal with changes tocontrol):With the new API you can have:
Notice that this version insulates consumers of your directive/component from changes to the
providedControl, allowing them to interface with a static (readonly) reference control.While this might seem like a small difference, the ability to have static references to controls ends up being a pretty big deal in terms of user-friendliness. For example, if you inject
NgFormControlDirectiveinto your component, you don't need to deal with changes to theformControlproperty of theNgFormControlDirective. You can simply subscribe toNgFormControlDirective#controland know that that control will always have the same state asNgFormControlDirective#formControl.There is a blog post which goes into greater detail showing how this updated
ControlAccessorwould work.Describe alternatives you've considered
While fixing the existing
ReactiveFormsModuleis a possibility, it would involve many breaking changes. AsRenderer->Renderer2has shown, a more user friendly solution is to create a newReactiveFormsModule2module, depricate the old module, and provide a compatibility layer to allow usage of the two side-by-side.