Forms Upcoming Change Proposal
Author: Kara Erickson, June 3, 2016
This doc proposes changes to the Angular 2 forms API to help make it friendlier and more intuitive.
Please note this doc describes *breaking changes only*. There are many form improvements, feature requests, and bug fixes to come that are additive, and thus not included in this doc (e.g. radio buttons, resetting forms, etc). These changes will be described in an upcoming doc.
Directives:
ngControl
ngFormControl
ngModel
ngFormModel
ngControlGroup
Injectables:
Control()
ControlGroup()
ControlArray()
FormBuilder
Validators
Template-driven forms:
Example 1: ngControl
<form #f="ngForm"> <div ngControlGroup="name" #name="ngForm"> <input ngControl="first" #first="ngForm"> <input ngControl="last"> </div> </form> {{ f.value | json }} // {name: {first: null, last: null}} {{ first.value }} // null |
This creates a basic form with no initial values. You can export each control or controlGroup to check its value and its validity state.
Problem: All exports are named "ngForm". Prima facie, it seems like name, first, and f should all refer to the same item - the root form. In actuality, each ngForm is a different value based on its parent element. It would be more intuitive for each export to match the name of the directive.
Problem: The distinction between "ngControl" and "ngFormControl" is not clear. They are both form controls, so why use ngControl over the other? In fact, ngControl creates a new form control and expects a string name as an input, while ngFormControl expects an existing Control instance to be passed in. In addition, only ngFormControl can be used outside the context of a form tag. This is not apparent from the naming.
Problem: It's not obvious how to populate initial values to the form. You could query for the form as a ViewChild and set the values of the control manually. However, this is not immediately obvious and devs familiar with Angular 1 are more likely to try to convert to ngModel (example 2).
Goals:
Example 2: ngModel
It is fair that developers coming from Angular 1 would expect the following to work. In Angular 1, adding ngModel and a name attribute is all that's necessary to register a form control with the root form.
<form #f="ngForm"> <div> <input name="first" [(ngModel)]="person.name.first" #first="ngForm"> <input name="last" [(ngModel)]="person.name.last"> </div> </form> {{ f.value | json }} // {} {{ first.value }} // 'Nancy' |
class MyComp { person = { name: { first: 'Nancy', last: 'Drew' } } } |
Problem: However, this setup only partially works. Unlike in Angular 1, Angular 2 ngModel creates an isolated control that doesn't know about parent groups or forms. While you can export the value of the individual form controls and their initial values are set, the aggregate form value is empty. It's also not possible to check the validity state of the group or the form. You must use ngControl to achieve that result.
Goals:
Example 3: ngControl + ngModel
A reasonable next step would be for a developer to re-add "ngControl" and "ngControlGroup".
<form #f="ngForm"> <div ngControlGroup="name"> <input ngControl="first" [(ngModel)]="person.name.first" #first="ngForm"> <input ngControl="last" [(ngModel)]="person.name.last"> </div> </form> {{ f.value | json }} // {name: {first: 'Nancy', last: 'Drew'}} {{ first.value }} // 'Nancy' |
class MyComp { person = { name: { first: 'Nancy', last: 'Drew' } } } |
With both, this form works as expected, populating initial values and setting value/validation on the parent form properly.
Problem: For developers looking at this setup for the first time, it's not clear where ngControl stops and ngModel starts. When do you need ngControl and when do you need ngModel? This is especially confusing because ngModel has *most* but not all of the functionality it had in Angular 1, and the two directives both provide validation state and classes for individual controls.
The result is that we have two confusingly-similar directives in Angular 2 where one directive used to suffice, and the lines between them aren't clear.
Problem: When ngControl and ngModel are on the same element, it's not obvious which is exported as ngForm. In reality, there is only one directive activated when they are used together -- ngControl -- and it simply takes ngModel information as an input. However, this is not apparent unless you look at the source code.
Goals:
"Model-driven forms"
Example 4: ngFormModel
<form [ngFormModel]="form"> <div ngControlGroup="name"> <input ngControl="first" #first="ngForm"> <input ngControl="last"> </div> </form> {{ form.value | json }} // {name: {first: 'Nancy', last: 'Drew'}} {{ first.value }} // 'Nancy' |
class MyComp { form = new ControlGroup({ name: new ControlGroup({ first: new Control('Nancy'), last: new Control('Drew') }) }); } |
Problem: If you're coming from template-driven forms, this seems like a whole lot of boilerplate. You'll notice that the template looks very similar to the template in Example #1. In that example, the template already had the ngControl and ngControlGroup directives and they would create controls. It's reasonable to assume that the same directives would create controls in this circumstance. A common question is: why is it still necessary to create controls in the template if you already created them in the class?
In reality, you are simply syncing the existing controls to the correct DOM elements. If you think about it, it does make sense that you'd need to associate each control you create in the class with an element in the DOM. But because the directives have the same names in template-driven and model-driven approaches, it's not clear that *in this context only* the directives simply search for existing controls rather than creating them.
We essentially have two distinct behaviors that we are forcing into one set of directives. It would be less confusing if we had two sets of directives that behave predictably in every case.
Problem: If this is the first forms example you see, it seems like quite a bit of work to create a simple form. It's not clear that this is an advanced use case, and that for simpler forms you can simply remove the ngFormModel and the class code.
Problem: The name "ngFormModel" doesn't self-describe very well. It sounds like an extension of "ngModel", where you might pass in a domain object for the entire form (e.g. "person" in our example). It actually expects a ControlGroup but this is not apparent.
Goals:
Example 5: Validators
class MyComp { form = new ControlGroup({ name: new ControlGroup({ first: new Control('Nancy', Validators.required), last: new Control('Drew', Validators.compose( Validators.required, Validators.minLength(3))) }) }); } |
This form adds validation that the each name is present, and that the last name has at least three characters.
Problem: Controls only accept one validator function, so it looks at first like you can only run one type of validation per control. It's unclear that you can pass many validators into compose() to create an aggregate validator. It would be a better developer experience if we made this optional, allowing arrays of validators that will be composed on the Control's end.
Goals:
Example 6: Control Arrays
<form [ngFormModel]="form"> <div ngControlGroup="cities"> <div *ngFor="let city of cityArray.controls; let i=index" > <input ngControl="{{i}}"> </div> </div> </form> <button (click)="push()">push</button> |
class myComp { cityArray = new ControlArray([ new Control('SF'), new Control('NY') ]); form = new ControlGroup({ cities: this.cityArray });
push() { this.cityArray.push(new Control('')); } } |
ControlArrays allow users to dynamically add form controls by pushing to the ControlArray.
Problem: To sync a ControlArray to a DOM element, you have to use the ngControlGroup directive. You'd expect that ControlArray have its own equivalent in the DOM, like ngControlArray.
Goals:
Template-driven / "Angular 1-style"
Reminder - goals:
In the current forms module, we have two very similar directives (ngControl and ngModel), both providing about 75% of common behavior. To get 100% of the behavior, you are currently forced to use them together. It doesn't make sense to support two separate directives that provide almost the same functionality, so this is an opportunity to simplify and consolidate.
Because most Angular developers are already familiar with ngModel, it makes sense to empower ngModel with the last 25% of the functionality and remove ngControl from the API as duplicative. This makes ngModel functionally equivalent to its Angular 1 counterpart, which will help meet expectations of developers transitioning from Angular 1. More importantly, developers won't have to wonder how to combine two similar directives; they always know to use ngModel.
This also simplifies the matter of export names, as there is only one concept: ngModel.
It's important to note that this change doesn't require developers to use two-way binding. It will simply be an available option. ngModel can also be used for the one-way binding and value change subscription paradigms common with ngControl.
To follow the pattern of "ngModel" and ensure it's easy to remember, ngControlGroup will be renamed to ngModelGroup. It's obvious from the naming that these two form a pair and can be used together. If you want validate an individual input, use ngModel, if you want to validate multiple inputs as a group, use ngModelGroup (examples follow).
API:
ngModel (same)
ngModelGroup (deprecated: ngControlGroup)
-- (deprecated: ngControl)
Exports:
<form> ngForm (same)
ngModel ngModel (deprecated: ngForm)
ngModelGroup ngModelGroup (deprecated: ngForm)
Export names always map to the name of the directive in question to take the guesswork out of accessing directive instances.
Simple ngModel example:
<input [(ngModel)]="person.name.first" #first="ngModel"> <input [(ngModel)]="person.name.last"> {{ first.valid }} // true |
class MyComp { person = { name: { first: 'Nancy', last: 'Drew' } } } |
Here is a simple example. ngModel can be used with or without a containing form tag. If you want standalone form controls, they can be validated individually.
Simple form example:
<form #f="ngForm"> <input name="first" [(ngModel)]="person.name.first"> <input name="last" [(ngModel)]="person.name.last"> </form> {{ f.value }} // {first: 'Nancy', last: 'Drew'} |
class MyComp { person = { name: { first: 'Nancy', last: 'Drew' } } } |
Like Angular 1, if you want ngModel to register with the parent form, the name attribute is required. This name will be used as the key for the form control value in the parent form.
Example with no two-way binding (populate initial values)
Sometimes you might want to create a simple form that populates initial values, but doesn't use two-way binding. If this is the case, simply omit banana box syntax and use only square brackets.
<form #f="ngForm"> <input name="first" [ngModel]="person.name.first"> <input name="last" [ngModel]="person.name.last"> </form> {{ f.value }} // {first: 'Nancy', last: 'Drew'} |
class MyComp { person = { name: { first: 'Nancy', last: 'Drew' } } } |
Example with no initial binding
Traditionally, you would do this with ngControl, but you can achieve the same effect with a name attribute and an unbound ngModel directive.
<form #f="ngForm"> <input name="first" ngModel> <input name="last" ngModel> </form> {{ f.value }} // {first: '', last: ''} |
It's clear from this setup that ngModel is the entity conferring validation state and registration with the form. Without it, you just have a normal HTML input.
If later you wish to add two-way binding, you can simply wrap the ngModel in banana box notation. It doesn't require much effort or - more importantly - any new concepts to switch back and forth.
Example with sub-group validation
<form #f="ngForm"> <div ngModelGroup="name"> <input name="first" ngModel> <input name="last" ngModel> </div> </form> {{ f.value }} // {name: {first: '', last: ''}} |
Like ngControlGroup, ngModelGroup creates a FormGroup instance under the hood and connects it to the element in question.
Example with custom form control
We also want to handle the case where a user creates a custom form control and is already using the name attribute internally for something else. In this specific case, you can pass in the name of the form control through ngModelOptions, and that name will be used to register the custom control with the parent form (here the custom name is "first", so f.value.first is defined). Eventually, ngModelOptions will contain more config/functionality, as it did in Angular 1.
<form #f="ngForm"> <person-input name="Nan" [ngModelOptions]="{name: 'first'}" ngModel> </form> {{ f.value }} // {first: ''} |
Example with radio buttons:
<form #f="ngForm"> <input type="radio" name="food" [(ngModel)]="food" value="one"> <input type="radio" name="food" [(ngModel)]="food" value="two"> </form> {{ f.value | json }} // {food: 'one'} |
class MyComp { food = 'one'; } |
Model-driven/ "Reactive forms"
Reminder - goals:
API:
formGroup (deprecated: ngFormModel)
formControl (deprecated: ngFormControl)
formControlName (deprecated: ngControl)
formGroupName (deprecated: ngControlGroup)
formArrayName (deprecated: ngControlGroup)
FormControl() (deprecated: Control)
FormGroup() (deprecated: ControlGroup)
FormArray() (deprecated: ControlArray)
FormBuilder (same)
Validators (same)
"form" prefix
We are separating the form strategies into two sets of directives:
The separation helps clarify which directive belongs to each approach, and emphasizes that the reactive directives are not necessary to get started. The default directives comprise one, fully-functional bucket and the manually-imported directives comprise a separate fully-functional bucket, leveraging a different strategy.
FormControl, FormGroup, FormArray
Control, ControlGroup, and ControlArray are being renamed to FormControl, FormGroup, and FormArray for a few reasons.
formGroup, formControl
formGroup has been renamed from ngFormModel because it morely clearly communicates that it expects a FormGroup() instance as an input. This name is also more distinct from ngModel, preventing confusion with anything ngModel-related.
We considered "bindFormGroup" as an alternative name, but the kebab-case equivalent "bind-bind-form-group" would be too confusing.
formControl is formGroup's partner directive for binding individual controls that already exist.
formControlName / formGroupName
One problem with the old API is that it's not clear when controls are being created for you, and when existing controls are being matched to the DOM. We also wanted to distinguish between directives that expect FormControl or FormGroup instances and directives that simply accept the name of a control. With the "name" suffix, formControlName and formGroupName clearly expect string inputs. The names are also distinct from ngModelGroup (which does create its own control).
No exports
We won't provide exports, as the preferred method would be to use the value from the class side. Allowing exports here would obscure how we expect reactive forms to be used and muddy the existing template/model separation issues.
Bind existing form example:
<form [formGroup]="myForm"> <div formGroupName="name"> <input formControlName="first"> <input formControlName="last"> </div> </form> {{ myForm.value | json }} // {name: {first: 'Nancy', last: 'Drew'}} |
class MyComp { myForm = new FormGroup({ name: new FormGroup({ first: new FormControl('Nancy'), last: new FormControl('Drew') }) }); } |
Bind existing form with async one-way binding
Sometimes you'll want to get data from an async source, like over HTTP, so you won't be able to initialize the data along with the form. For this case, you can call this.form.setValue() or this.form.patchValue() instead of using ngModel.
<form [formGroup]="myForm"> <div formGroupName="name"> <input formControlName="first"> <input formControlName="last"> </div> </form> {{ myForm.value | json }} // {name: {first: 'Nancy', last: 'Drew'}} |
class MyComp implements OnInit { person = { name: {first: '', last: ''} }; myForm = new FormGroup({ name: new FormGroup({ first: new FormControl(), last: new FormControl() }) }); constructor(private _http: Http) ngOnInit() { this._http.get(someUrl) .map(this.extractData) .subscribe(person => this.myForm.patchValue(person)); } } |
Validators example
class MyComp { form = new FormGroup({ name: new FormGroup({ first: new FormControl('Nancy', Validators.required), last: new FormControl('Drew', [ Validators.required, Validators.minLength(3) ]) }) }); } |
Non-breaking: FormControl() accepts either a composed validator function *OR* an array of validators that it knows how to compose itself.
That way, if you do want to compose validators in a special way yourself, you can. But if you don't, it's easy to see how to pass in multiple validators.
FormArray example
<form [formGroup]="form"> <div formArrayName="cities"> <div *ngFor="let city of cityArray.controls; let i=index" > <input [formControlName]="i"> </div> </div> </form> <button (click)="push()">push</button> |
class myComp { cityArray = new FormArray([ new FormControl('SF'), new FormControl('NY') ]); form = new FormGroup({ cities: this.cityArray });
push() { this.cityArray.push(new FormControl('')); } } |
Two new things:
Importing example
import {REACTIVE_FORM_DIRECTIVES, FormControl, FormGroup} from '@angular/forms'; … directives: [REACTIVE_FORM_DIRECTIVES] |
Platform directives:
ngModel (same)
ngModelGroup (deprecated: ngControlGroup)
Import yourself from @angular/forms:
REACTIVE_FORM_DIRECTIVES:
formGroup (deprecated: ngFormModel)
formControl (deprecated: ngFormControl)
formControlName (deprecated: ngControl)
formGroupName (deprecated: ngControlGroup)
formArrayName (deprecated: ngControlGroup)
FormControl() (deprecated: Control)
FormGroup() (deprecated: ControlGroup)
FormArray() (deprecated: ControlArray)
FormBuilder (same)
Validators (same)
New forms code goes in its own module, @angular/forms.
RC-2:
To use deprecated forms: no changes
To switch to new forms:
Example of bootstrap file:
import {disableDeprecatedForms, provideForms} from '@angular/forms'; bootstrap(AppComponent, [ disableDeprecatedForms() provideForms() ]) |
RC-4:
RC-5:
Default state: no forms
To use new forms:
To use deprecated forms:
Example bootstrap file (use new forms):
import {FormsModule} from '@angular/forms'; bootstrap(AppComponent, {modules: [FormsModule] }) |
Example bootstrap file (use deprecated forms):
import {DeprecatedFormsModule} from '@angular/common'; bootstrap(AppComponent, {modules: [DeprecatedFormsModule] }) |
Or you can choose to provide neither in your bootstrap and add FORM_DIRECTIVES and FORM_PROVIDERS as needed:
import {FORM_DIRECTIVES, FORM_PROVIDERS} from '@angular/forms'; @Component({ selector: 'some-comp', template: '', directives: [FORM_DIRECTIVES], providers: [FORM_PROVIDERS] }) class SomeComp {} |