Skip to content

Instantly share code, notes, and snippets.

@sheff3rd
Last active February 9, 2017 10:28
Show Gist options
  • Save sheff3rd/dc80d1afa4cc6d0b8b20eafeb7415067 to your computer and use it in GitHub Desktop.
Save sheff3rd/dc80d1afa4cc6d0b8b20eafeb7415067 to your computer and use it in GitHub Desktop.

Existing forms API:

Directives:

  • ngControl
  • ngFormControl
  • ngModel
  • ngFormModel
  • ngControlGroup

Injectables:

  • Control()
  • ControlGroup()
  • ControlArray()
  • FormBuilder
  • Validators
  • Problem space

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: Provide an intuitive set of export names that devs can easily guess Disambiguate confusingly-similar directive names: ngControl & ngFormControl 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: Make migration to Angular 2 forms intuitive for Angular 1 devs 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: Simplify the "template-driven" form setup to avoid two competing directives "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: Clarify that "model-driven" forms bind existing controls Conceptually separate ngControl/ngControlGroup in template-driven vs. model-driven contexts, as they behave differently Communicate that "model-driven" forms are intended as an advanced use case, and simpler forms can be created using the other strategy Disambiguate confusingly-similar directive names: ngModel/ngFormModel 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: Facilitate passing multiple validators into a control 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: Create a more streamlined experience for ControlArrays

Proposed API Changes

Template-driven / "Angular 1-style" Reminder - goals: Provide an intuitive set of export names that devs can easily guess Make migration to Angular 2 forms intuitive for Angular 1 devs Simplify the "template-driven" form setup to avoid two competing directives 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: Clarify that "model-driven" forms bind existing controls Conceptually separate ngControl/ngControlGroup in template-driven vs. model-driven contexts, as they behave differently Disambiguate confusingly-similar directive names: ngModel/ngFormModel Disambiguate confusingly-similar directive names: ngControl/ngFormControl Communicate that "model-driven" forms are intended as an advanced use case, and simpler forms can be created using the other strategy

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: Template-driven/"angular 1 style" directives are provided as platform directives This means ngModel and ngModelGroup will be available by default As they are platform directives, they use the core "ng" prefix Model-driven/"reactive form" directives are part of @angular/common, but must be manually imported. As manually-imported directives are opt-in, they drop the ng prefix. Instead, they use the "form" prefix so it is clear that they comprise one cohesive unit that all relate to forms. 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. The names are more immediately recognizable and descriptive of what these classes actually represent. A "Control" on its own could describe a number of diverse things. The names are more parallel with the "form" prefix pattern created by their directive counterparts (formControlName goes with FormControl() ). This makes them easier to remember. 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:

With a FormArray, you use formArrayName rather than ngControlGroup Non-breaking: There is a way for you to easily iterate over the controls other than saving the original array of controls. Importing example import {REACTIVE_FORM_DIRECTIVES, FormControl, FormGroup} from '@angular/forms'; … directives: [REACTIVE_FORM_DIRECTIVES] TLDR: Problems

It's confusing that all form directives export as "ngForm" The distinction between "ngControl"/"ngFormControl" and "ngModel"/"ngFormModel" isn't clear from the naming Angular 1 devs are surprised by ngModel having only 75% of its original functionality It's not obvious how to populate values to the form in the template-driven approach It's not clear when you need to use "ngControl" vs "ngModel" and what each does It's confusing that ngControl/ngControlGroup behave differently in template-driven and vs. model-driven contexts The naming of model-driven directives does not communicate that existing controls are being synced, rather than being created (twice) Validator names are not intuitive It's not obvious how to pass multiple validators to a Control The name of ControlArray directives TLDR: Goals

Provide an intuitive set of export names that devs can easily guess Disambiguate confusingly-similar directive names like ngControl/ngFormControl and ngModel/ngFormModel Make migration to Angular 2 forms intuitive for Angular 1 devs Simplify the "template-driven" form setup to avoid two competing directives Clarify that "model-driven" forms bind existing controls Conceptually separate ngControl/ngControlGroup in template-driven vs. model-driven contexts, as they behave differently Communicate that "model-driven" forms are intended as an advanced use case, and simpler forms can be created using the other strategy Ensure validator names are intuitive based on their DOM equivalents Facilitate passing multiple validators into a control Create a more streamlined experience for ControlArrays Define a deprecation strategy that will help prevent breakages and developer frustration TLDR: API names

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)

Deprecation Strategy

New forms code goes in its own module, @angular/forms. RC-2: To use deprecated forms: no changes To switch to new forms: Use "disableDeprecatedForms()" to disable old form platform directives Use "provideForms()" function to provide new form platform directives Import any needed symbols from @angular/forms, not @angular/common 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: Provide FormsModule (or call provideForms() - function going away in next RC) Import any needed symbols from @angular/forms To use deprecated forms: Provide DeprecatedFormsModule Import any needed symbols from @angular/common 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 {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment