From e41da1144403f3cdf9712ce85e8ddd70dbe40f54 Mon Sep 17 00:00:00 2001 From: Torgeir Helgevold Date: Sun, 19 Jun 2016 11:50:27 -0400 Subject: [PATCH] docs(forms): upgrading forms guide to new api This PR upgrades the existing forms to the new API, while leaving a copy for existing users. The current forms will be the default until RC4, at which point we will switch the default to the new API but still retain a link to the old forms API. After RC5 the old API docs will be completely removed. --- .../_examples/forms-deprecated/e2e-spec.ts | 64 ++ .../forms-deprecated/js/app/app.component.js | 12 + .../js/app/hero-form.component.html | 195 +++++ .../js/app/hero-form.component.js | 52 ++ .../_examples/forms-deprecated/js/app/hero.js | 11 + .../_examples/forms-deprecated/js/app/main.js | 6 + .../forms-deprecated/js/example-config.json | 0 .../_examples/forms-deprecated/js/forms.css | 9 + .../_examples/forms-deprecated/js/index.html | 45 ++ .../_examples/forms-deprecated/js/plnkr.json | 4 + .../forms-deprecated/ts/app/app.component.ts | 10 + .../ts/app/hero-form.component.html | 208 +++++ .../ts/app/hero-form.component.ts | 66 ++ .../_examples/forms-deprecated/ts/app/hero.ts | 11 + .../_examples/forms-deprecated/ts/app/main.ts | 6 + .../forms-deprecated/ts/example-config.json | 0 .../_examples/forms-deprecated/ts/forms.css | 9 + .../_examples/forms-deprecated/ts/index.html | 34 + .../_examples/forms-deprecated/ts/plnkr.json | 7 + public/docs/_examples/forms/e2e-spec.ts | 4 +- .../forms/js/app/hero-form.component.html | 24 +- public/docs/_examples/forms/js/app/main.js | 5 +- public/docs/_examples/forms/js/index.html | 1 + .../forms/ts/app/hero-form.component.html | 26 +- public/docs/_examples/forms/ts/app/main.ts | 7 +- public/docs/js/latest/guide/_data.json | 2 +- .../js/latest/guide/forms-deprecated.jade | 648 ++++++++++++++++ public/docs/js/latest/guide/forms.jade | 91 ++- public/docs/ts/latest/guide/_data.json | 2 +- .../ts/latest/guide/forms-deprecated.jade | 718 ++++++++++++++++++ public/docs/ts/latest/guide/forms.jade | 81 +- 31 files changed, 2252 insertions(+), 106 deletions(-) create mode 100644 public/docs/_examples/forms-deprecated/e2e-spec.ts create mode 100644 public/docs/_examples/forms-deprecated/js/app/app.component.js create mode 100644 public/docs/_examples/forms-deprecated/js/app/hero-form.component.html create mode 100644 public/docs/_examples/forms-deprecated/js/app/hero-form.component.js create mode 100644 public/docs/_examples/forms-deprecated/js/app/hero.js create mode 100644 public/docs/_examples/forms-deprecated/js/app/main.js create mode 100644 public/docs/_examples/forms-deprecated/js/example-config.json create mode 100644 public/docs/_examples/forms-deprecated/js/forms.css create mode 100644 public/docs/_examples/forms-deprecated/js/index.html create mode 100644 public/docs/_examples/forms-deprecated/js/plnkr.json create mode 100644 public/docs/_examples/forms-deprecated/ts/app/app.component.ts create mode 100644 public/docs/_examples/forms-deprecated/ts/app/hero-form.component.html create mode 100644 public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts create mode 100644 public/docs/_examples/forms-deprecated/ts/app/hero.ts create mode 100644 public/docs/_examples/forms-deprecated/ts/app/main.ts create mode 100644 public/docs/_examples/forms-deprecated/ts/example-config.json create mode 100644 public/docs/_examples/forms-deprecated/ts/forms.css create mode 100644 public/docs/_examples/forms-deprecated/ts/index.html create mode 100644 public/docs/_examples/forms-deprecated/ts/plnkr.json create mode 100644 public/docs/js/latest/guide/forms-deprecated.jade create mode 100644 public/docs/ts/latest/guide/forms-deprecated.jade diff --git a/public/docs/_examples/forms-deprecated/e2e-spec.ts b/public/docs/_examples/forms-deprecated/e2e-spec.ts new file mode 100644 index 0000000000..ab5c8321be --- /dev/null +++ b/public/docs/_examples/forms-deprecated/e2e-spec.ts @@ -0,0 +1,64 @@ +/// +'use strict'; +describeIf(browser.appIsTs || browser.appIsJs, 'Forms (Deprecated) Tests', function () { + + beforeEach(function () { + browser.get(''); + }); + + it('should display correct title', function () { + expect(element.all(by.css('h1')).get(0).getText()).toEqual('Hero Form'); + }); + + + it('should not display message before submit', function () { + let ele = element(by.css('h2')); + expect(ele.isDisplayed()).toBe(false); + }); + + it('should hide form after submit', function () { + let ele = element.all(by.css('h1')).get(0); + expect(ele.isDisplayed()).toBe(true); + let b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(ele.isDisplayed()).toBe(false); + }); + }); + + it('should display message after submit', function () { + let b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(element(by.css('h2')).getText()).toContain('You submitted the following'); + }); + }); + + it('should hide form after submit', function () { + let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + expect(alterEgoEle.isDisplayed()).toBe(true); + let submitButtonEle = element.all(by.css('button[type=submit]')).get(0); + submitButtonEle.click().then(function() { + expect(alterEgoEle.isDisplayed()).toBe(false); + }); + }); + + it('should reflect submitted data after submit', function () { + let test = 'testing 1 2 3'; + let newValue: string; + let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + alterEgoEle.getAttribute('value').then(function(value) { + // alterEgoEle.sendKeys(test); + sendKeys(alterEgoEle, test); + newValue = value + test; + expect(alterEgoEle.getAttribute('value')).toEqual(newValue); + }).then(function() { + let b = element.all(by.css('button[type=submit]')).get(0); + return b.click(); + }).then(function() { + let alterEgoTextEle = element(by.cssContainingText('div', 'Alter Ego')); + expect(alterEgoTextEle.isPresent()).toBe(true, 'cannot locate "Alter Ego" label'); + let divEle = element(by.cssContainingText('div', newValue)); + expect(divEle.isPresent()).toBe(true, 'cannot locate div with this text: ' + newValue); + }); + }); +}); + diff --git a/public/docs/_examples/forms-deprecated/js/app/app.component.js b/public/docs/_examples/forms-deprecated/js/app/app.component.js new file mode 100644 index 0000000000..bb6b789938 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/app.component.js @@ -0,0 +1,12 @@ +// #docregion +(function(app) { + app.AppComponent = ng.core + .Component({ + selector: 'my-app', + template: '', + directives: [app.HeroFormComponent] + }) + .Class({ + constructor: function() {} + }); +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/app/hero-form.component.html b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.html new file mode 100644 index 0000000000..fdf8e08396 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.html @@ -0,0 +1,195 @@ + + +
+ +
+

Hero Form

+ +
+ + +
+ + + +
+ Name is required +
+ +
+ +
+ + +
+ +
+ + +
+ Power is required +
+
+ + + + +
+
+ + +
+

You submitted the following:

+
+
Name
+
{{ model.name }}
+
+
+
Alter Ego
+
{{ model.alterEgo }}
+
+
+
Power
+
{{ model.power }}
+
+
+ +
+ +
+ + + +
+
+ + + + +
+
+ + + +
+ +
+ +
+

Hero Form

+
+
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + + +
+
+ + + + +
+ +
+

Hero Form

+
+ + {{diagnostic()}} +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ + + +
+ + + TODO: remove this: {{model.name}} + +
+ + + TODO: remove this: {{model.name}} + +
+
+ + + +
+ + +
TODO: remove this: {{spy.className}} + +
+ +
+
+ Name via form.controls = {{showFormControls(heroForm)}} +
+ +
diff --git a/public/docs/_examples/forms-deprecated/js/app/hero-form.component.js b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.js new file mode 100644 index 0000000000..8988231189 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/hero-form.component.js @@ -0,0 +1,52 @@ +// #docplaster +// #docregion +// #docregion first, final +(function(app) { + app.HeroFormComponent = ng.core + .Component({ + selector: 'hero-form', + templateUrl: 'app/hero-form.component.html' + }) + .Class({ + // #docregion submitted + constructor: function() { + // #enddocregion submitted + this.powers = ['Really Smart', 'Super Flexible', + 'Super Hot', 'Weather Changer' + ]; + + this.model = new app.Hero(18, 'Dr IQ', this.powers[0], + 'Chuck Overstreet'); + + // #docregion submitted + this.submitted = false; + }, + onSubmit: function() { + this.submitted = true; + }, + // #enddocregion submitted + + // #enddocregion final + // TODO: Remove this when we're done + diagnostic: function() { + return JSON.stringify(this.model); + }, + // #enddocregion first + + + //////// DO NOT SHOW IN DOCS //////// + + // Reveal in html: + // AlterEgo via form.controls = {{showFormControls(hf)}} + showFormControls: function(form) { + return form.controls['alterEgo'] && + // #docregion form-controls + form.controls['name'].value; // Dr. IQ + // #enddocregion form-controls + }, + ///////////////////////////// + + // #docregion first, final + }); + // #enddocregion first, final +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/app/hero.js b/public/docs/_examples/forms-deprecated/js/app/hero.js new file mode 100644 index 0000000000..9c2449c922 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/hero.js @@ -0,0 +1,11 @@ +// #docregion +(function(app) { + app.Hero = Hero; + + function Hero(id, name, power, alterEgo) { + this.id = id; + this.name = name; + this.power = power; + this.alterEgo = alterEgo; + } +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/app/main.js b/public/docs/_examples/forms-deprecated/js/app/main.js new file mode 100644 index 0000000000..5930bdd061 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/app/main.js @@ -0,0 +1,6 @@ +// #docregion +(function(app) { + document.addEventListener('DOMContentLoaded', function() { + ng.platformBrowserDynamic.bootstrap(app.AppComponent); + }); +})(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms-deprecated/js/example-config.json b/public/docs/_examples/forms-deprecated/js/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/forms-deprecated/js/forms.css b/public/docs/_examples/forms-deprecated/js/forms.css new file mode 100644 index 0000000000..d7e11405b1 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/forms.css @@ -0,0 +1,9 @@ +/* #docregion */ +.ng-valid[required] { + border-left: 5px solid #42A948; /* green */ +} + +.ng-invalid { + border-left: 5px solid #a94442; /* red */ +} +/* #enddocregion */ \ No newline at end of file diff --git a/public/docs/_examples/forms-deprecated/js/index.html b/public/docs/_examples/forms-deprecated/js/index.html new file mode 100644 index 0000000000..e15f41e4fe --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/index.html @@ -0,0 +1,45 @@ + + + + + + Hero Form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/forms-deprecated/js/plnkr.json b/public/docs/_examples/forms-deprecated/js/plnkr.json new file mode 100644 index 0000000000..0105283bd3 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/js/plnkr.json @@ -0,0 +1,4 @@ +{ + "description": "Forms", + "files":["app/**/*.js", "**/*.html", "**/*.css"] +} diff --git a/public/docs/_examples/forms-deprecated/ts/app/app.component.ts b/public/docs/_examples/forms-deprecated/ts/app/app.component.ts new file mode 100644 index 0000000000..2dcbc8037a --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/app.component.ts @@ -0,0 +1,10 @@ +// #docregion +import { Component } from '@angular/core'; +import { HeroFormComponent } from './hero-form.component'; + +@Component({ + selector: 'my-app', + template: '', + directives: [HeroFormComponent] +}) +export class AppComponent { } diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.html b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.html new file mode 100644 index 0000000000..6fcfc2a7cb --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.html @@ -0,0 +1,208 @@ + + +
+ +
+

Hero Form

+ +
+ + +
+ + + + +
+ + Name is required +
+ +
+ +
+ + +
+ +
+ + +
+ Power is required +
+
+ + + + + + + + + + + +
+
+ Name via form.controls = {{showFormControls(heroForm)}} +
+ + +
+
+ + +
+

You submitted the following:

+
+
Name
+
{{ model.name }}
+
+
+
Alter Ego
+
{{ model.alterEgo }}
+
+
+
Power
+
{{ model.power }}
+
+
+ +
+ +
+ + + +
+
+ + + + +
+
+ + + +
+ +
+ +
+

Hero Form

+
+
+ + +
+ +
+ + +
+ + + +
+ + +
+ + + + + +
+
+ + + + +
+ +
+

Hero Form

+
+ + {{diagnostic}} +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ + + +
+ + + TODO: remove this: {{model.name}} + +
+ + + TODO: remove this: {{model.name}} + +
+ +
+ + + + + +
+ + +
TODO: remove this: {{spy.className}} + +
+ +
diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts new file mode 100644 index 0000000000..7ea7c44738 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/hero-form.component.ts @@ -0,0 +1,66 @@ +// #docplaster +// #docregion +// #docregion first, final +import { Component } from '@angular/core'; +import { NgForm } from '@angular/common'; + +import { Hero } from './hero'; + +@Component({ + selector: 'hero-form', + templateUrl: 'app/hero-form.component.html' +}) +export class HeroFormComponent { + + powers = ['Really Smart', 'Super Flexible', + 'Super Hot', 'Weather Changer']; + + model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet'); + + // #docregion submitted + submitted = false; + + onSubmit() { this.submitted = true; } + // #enddocregion submitted + + // #enddocregion final + // TODO: Remove this when we're done + get diagnostic() { return JSON.stringify(this.model); } + // #enddocregion first + + // #docregion final + // Reset the form with a new hero AND restore 'pristine' class state + // by toggling 'active' flag which causes the form + // to be removed/re-added in a tick via NgIf + // TODO: Workaround until NgForm has a reset method (#6822) + // #docregion new-hero + active = true; + + // #docregion new-hero-v1 + newHero() { + this.model = new Hero(42, '', ''); + // #enddocregion new-hero-v1 + this.active = false; + setTimeout(() => this.active = true, 0); + // #docregion new-hero-v1 + } + // #enddocregion new-hero-v1 + // #enddocregion new-hero + // #enddocregion final + //////// NOT SHOWN IN DOCS //////// + + // Reveal in html: + // Name via form.controls = {{showFormControls(heroForm)}} + showFormControls(form: NgForm) { + + return form && form.controls['name'] && + // #docregion form-controls + form.controls['name'].value; // Dr. IQ + // #enddocregion form-controls + } + + ///////////////////////////// + + // #docregion first, final +} +// #enddocregion first, final diff --git a/public/docs/_examples/forms-deprecated/ts/app/hero.ts b/public/docs/_examples/forms-deprecated/ts/app/hero.ts new file mode 100644 index 0000000000..c128626452 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/hero.ts @@ -0,0 +1,11 @@ +// #docregion +export class Hero { + + constructor( + public id: number, + public name: string, + public power: string, + public alterEgo?: string + ) { } + +} diff --git a/public/docs/_examples/forms-deprecated/ts/app/main.ts b/public/docs/_examples/forms-deprecated/ts/app/main.ts new file mode 100644 index 0000000000..5338161d66 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/app/main.ts @@ -0,0 +1,6 @@ +// #docregion +import { bootstrap } from '@angular/platform-browser-dynamic'; + +import { AppComponent } from './app.component'; + +bootstrap(AppComponent); diff --git a/public/docs/_examples/forms-deprecated/ts/example-config.json b/public/docs/_examples/forms-deprecated/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/forms-deprecated/ts/forms.css b/public/docs/_examples/forms-deprecated/ts/forms.css new file mode 100644 index 0000000000..d7e11405b1 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/forms.css @@ -0,0 +1,9 @@ +/* #docregion */ +.ng-valid[required] { + border-left: 5px solid #42A948; /* green */ +} + +.ng-invalid { + border-left: 5px solid #a94442; /* red */ +} +/* #enddocregion */ \ No newline at end of file diff --git a/public/docs/_examples/forms-deprecated/ts/index.html b/public/docs/_examples/forms-deprecated/ts/index.html new file mode 100644 index 0000000000..4df2d32d46 --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/index.html @@ -0,0 +1,34 @@ + + + + + Hero Form + + + + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/forms-deprecated/ts/plnkr.json b/public/docs/_examples/forms-deprecated/ts/plnkr.json new file mode 100644 index 0000000000..c813933f8e --- /dev/null +++ b/public/docs/_examples/forms-deprecated/ts/plnkr.json @@ -0,0 +1,7 @@ +{ + "description": "Forms-Deprecated", + "files":[ + "!**/*.d.ts", + "!**/*.js" + ] +} \ No newline at end of file diff --git a/public/docs/_examples/forms/e2e-spec.ts b/public/docs/_examples/forms/e2e-spec.ts index 502a2cbdce..87ab6ef904 100644 --- a/public/docs/_examples/forms/e2e-spec.ts +++ b/public/docs/_examples/forms/e2e-spec.ts @@ -33,7 +33,7 @@ describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () { }); it('should hide form after submit', function () { - let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + let alterEgoEle = element.all(by.css('input[name=alterEgo]')).get(0); expect(alterEgoEle.isDisplayed()).toBe(true); let submitButtonEle = element.all(by.css('button[type=submit]')).get(0); submitButtonEle.click().then(function() { @@ -44,7 +44,7 @@ describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () { it('should reflect submitted data after submit', function () { let test = 'testing 1 2 3'; let newValue: string; - let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + let alterEgoEle = element.all(by.css('input[name=alterEgo]')).get(0); alterEgoEle.getAttribute('value').then(function(value) { // alterEgoEle.sendKeys(test); sendKeys(alterEgoEle, test); diff --git a/public/docs/_examples/forms/js/app/hero-form.component.html b/public/docs/_examples/forms/js/app/hero-form.component.html index fdf8e08396..fbe6ee917f 100644 --- a/public/docs/_examples/forms/js/app/hero-form.component.html +++ b/public/docs/_examples/forms/js/app/hero-form.component.html @@ -13,7 +13,7 @@ + name="name" #name="ngModel" >
Name is required
@@ -24,14 +24,14 @@ + name="alterEgo" >
@@ -133,19 +133,19 @@
+ [(ngModel)]="model.name" name="name" >
+ [(ngModel)]="model.alterEgo" name="alterEgo">
@@ -173,18 +173,18 @@
- + - + name="name" > +
- + + name="name" #spy >
TODO: remove this: {{spy.className}} - +
diff --git a/public/docs/_examples/forms/js/app/main.js b/public/docs/_examples/forms/js/app/main.js index 5930bdd061..2b866b3685 100644 --- a/public/docs/_examples/forms/js/app/main.js +++ b/public/docs/_examples/forms/js/app/main.js @@ -1,6 +1,9 @@ // #docregion (function(app) { document.addEventListener('DOMContentLoaded', function() { - ng.platformBrowserDynamic.bootstrap(app.AppComponent); + ng.platformBrowserDynamic.bootstrap(app.AppComponent,[ + ng.forms.disableDeprecatedForms(), + ng.forms.provideForms() + ]); }); })(window.app || (window.app = {})); diff --git a/public/docs/_examples/forms/js/index.html b/public/docs/_examples/forms/js/index.html index e15f41e4fe..8353dfb180 100644 --- a/public/docs/_examples/forms/js/index.html +++ b/public/docs/_examples/forms/js/index.html @@ -24,6 +24,7 @@ + diff --git a/public/docs/_examples/forms/ts/app/hero-form.component.html b/public/docs/_examples/forms/ts/app/hero-form.component.html index 6fcfc2a7cb..80401a1313 100644 --- a/public/docs/_examples/forms/ts/app/hero-form.component.html +++ b/public/docs/_examples/forms/ts/app/hero-form.component.html @@ -13,7 +13,7 @@ + name="name" #name="ngModel" >
@@ -26,14 +26,14 @@ + name="alterEgo" >
@@ -148,19 +148,19 @@
+ [(ngModel)]="model.name" name="name">
+ [(ngModel)]="model.alterEgo" name="alterEgo">
@@ -176,7 +176,7 @@
+ [(ngModel)]="model.name" name="name"> TODO: remove this: {{model.name}}
@@ -191,18 +191,18 @@
- + - + name="name" > +
- + + name="name" #spy >
TODO: remove this: {{spy.className}} - +
diff --git a/public/docs/_examples/forms/ts/app/main.ts b/public/docs/_examples/forms/ts/app/main.ts index 5338161d66..9e0244c26a 100644 --- a/public/docs/_examples/forms/ts/app/main.ts +++ b/public/docs/_examples/forms/ts/app/main.ts @@ -1,6 +1,11 @@ // #docregion import { bootstrap } from '@angular/platform-browser-dynamic'; +import { disableDeprecatedForms, provideForms } from '@angular/forms'; import { AppComponent } from './app.component'; -bootstrap(AppComponent); +bootstrap(AppComponent, [ + disableDeprecatedForms(), + provideForms() + ]) + .catch((err: any) => console.error(err)); diff --git a/public/docs/js/latest/guide/_data.json b/public/docs/js/latest/guide/_data.json index 437e7df624..a0f8270eec 100644 --- a/public/docs/js/latest/guide/_data.json +++ b/public/docs/js/latest/guide/_data.json @@ -29,7 +29,7 @@ "basics": true }, - "forms": { + "forms-deprecated": { "title": "Forms", "intro": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors.", "nextable": true, diff --git a/public/docs/js/latest/guide/forms-deprecated.jade b/public/docs/js/latest/guide/forms-deprecated.jade new file mode 100644 index 0000000000..185b25dee5 --- /dev/null +++ b/public/docs/js/latest/guide/forms-deprecated.jade @@ -0,0 +1,648 @@ +include ../_util-fns + +.alert.is-important + :marked + This guide is using the deprecated forms API. + + We have created a new version using the new API here. + +:marked + We’ve all used a form to login, submit a help request, place an order, book a flight, + schedule a meeting and perform countless other data entry tasks. + Forms are the mainstay of business applications. + + Any seasoned web developer can slap together an HTML form with all the right tags. + It's more challenging to create a cohesive data entry experience that guides the + user efficiently and effectively through the workflow behind the form. + + *That* takes design skills that are, to be frank, well out of scope for this chapter. + + It also takes framework support for + **two-way data binding, change tracking, validation, and error handling** + ... which we shall cover in this chapter on Angular forms. + + We will build a simple form from scratch, one step at a time. Along the way we'll learn + + - How to build an Angular form with a component and template + + - The `ngModel` two-way data binding syntax for reading and writing values to input controls + + - The `ngControl` directive to track the change state and validity of form controls + + - The special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback + + - How to display validation errors to users and enable/disable form controls + + - How to share information across controls with template local variables + + [Live Example](/resources/live-examples/forms-deprecated/js/plnkr.html) +.l-main-section +:marked + ## Template-Driven Forms + + Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with + the form-specific directives and techniques described in this chapter. +.l-sub-section + :marked + That's not the only way to create a form but it's the way we'll cover in this chapter. +:marked + We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms. + We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors, + conditionally enable or disable specific controls, trigger built-in visual feedback, and much more. + + It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd + otherwise wrestle with ourselves. + + We'll discuss and learn to build the following template-driven form: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form") + +:marked + Here at the *Hero Employment Agency* we use this form to maintain personal information about the + heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis! + + Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot. + + If we delete the hero name, the form displays a validation error in an attention grabbing style: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required") + +:marked + Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red. + +.l-sub-section + p We'll' customize the colors and location of the "required" bar with standard CSS. + +:marked + We will build this form in the following sequence of small steps + + 1. Create the `Hero` model class + 1. Create the component that controls the form + 1. Create a template with the initial form layout + 1. Add the **ngModel** directive to each form input control + 1. Add the **ngControl** directive to each form input control + 1. Add custom CSS to provide visual feedback + 1. Show and hide validation error messages + 1. Handle form submission with **ngSubmit** + 1. Disable the form’s submit button until the form is valid + +:marked + ## Setup + Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html). + + ## Create the Hero Model Class + + As users enter form data, we capture their changes and update an instance of a model. + We can't layout the form until we know what the model looks like. + + A model can be as simple as a "property bag" that holds facts about a thing of application importance. + That describes well our `Hero` class with its three required fields (`id`, `name`, `power`) + and one optional field (`alterEgo`). + + Create a new file in the app folder called `hero.js` and give it the following constructor: + ++makeExample('forms-deprecated/js/app/hero.js', null, 'app/hero.js') + +:marked + It's an anemic model with few requirements and no behavior. Perfect for our demo. + + The `alterEgo` is optional and the constructor lets us omit it by being the last argument. + + We can create a new hero like this: +code-example(format=""). + var myHero = new Hero(42, 'SkyDog', + 'Fetch any object at any distance', 'Leslie Rollover'); + console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog" +:marked + We update the `` of the `index.html` to include this javascript file. + ++makeExample('forms-deprecated/js/index.html', 'scripts-hero', 'index.html (excerpt)')(format=".") + +.l-main-section +:marked + ## Create a Form component + + An Angular form has two parts: an HTML-based template and a code-based Component to handle data and user interactions. + + We begin with the Component because it states, in brief, what the Hero editor can do. + + Create a new file called `hero-form.component.js` and give it the following definition: + ++makeExample('forms-deprecated/js/app/hero-form.component.js', 'first', 'app/hero-form.component.js') + +:marked + There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before. + + Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters + + 1. We use the `ng.core` object from the Angular library as we usually do. + + 1. The `Component()` selector value of "hero-form" means we can drop this form in a parent template with a `` tag. + + 1. The `templateUrl` property points to a separate file for template HTML called `hero-form.component.html`. + + 1. We defined dummy data for `model` and `powers` as befits a demo. + Down the road, we can inject a data service to get and save real data + or perhaps expose these properties as [inputs and outputs](./template-syntax.html#inputs-outputs) for binding to a + parent component. None of this concerns us now and these future changes won't affect our form. + + 1. We threw in a `diagnostic` method at the end to return a JSON representation of our model. + It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later. + + Why don't we write the template inline in the component file as we often do + elsewhere in the Developer Guide? + + There is no “right” answer for all occasions. We like inline templates when they are short. + Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to + write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code. + We also like short files with a clear and obvious purpose like this one. + + We made a good choice to put the HTML template elsewhere. + We'll write that template in a moment. Before we do, we'll take a step back + and revise the `app.component.js` to make use of our new `HeroFormComponent`. + +:marked + Again we update the `` of the `index.html` to include the new javascript file. + ++makeExample('forms-deprecated/js/index.html', 'scripts-hero-form', 'index.html (excerpt)')(format=".") + +.l-main-section +:marked + ## Revise the *app.component.js* + + `app.component.js` is the application's root component. It will host our new `HeroFormComponent`. + + Replace the contents of the "QuickStart" version with the following: ++makeExample('forms-deprecated/js/app/app.component.js', null, 'app/app.component.js') + +:marked +.l-sub-section + :marked + There are only two changes: + + 1. The `template` is simply the new element tag identified by the component's `select` property. + + 1. The `directives` array tells Angular that our template depends upon the `HeroFormComponent` + which is itself a Directive (as are all Components). + +.l-main-section +:marked + ## Create an initial HTML Form Template + + Create a new template file called `hero-form.component.html` and give it the following definition: + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'start', 'app/hero-form.component.html') + +:marked + That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and + opening them up for user input in input boxes. + + The *Name* `` control has the HTML5 `required` attribute; + the *Alter Ego* `` control does not because `alterEgo` is optional. + + We've got a *Submit* button at the bottom with some classes on it. + + **We are not using Angular yet**. There are no bindings. No extra directives. Just layout. + + The `container`,`form-group`, `form-control`, and `btn` classes + come from [Twitter Boostrap](http://getbootstrap.com/css/). Purely cosmetic. + We're using Bootstrap to gussy up our form. + Hey, what's a form without a little style! + +.callout.is-important + header Angular Forms Do Not Require A Style Library + :marked + Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or + the styles of any external library. Angular apps can use any CSS library + ... or none at all. + +:marked + Let's add the stylesheet. + +ol + li Open a terminal window in the application root folder and enter the command: + code-example(language="html" escape="html"). + npm install bootstrap --save + li Open index.html and add the following link to the <head>. + +makeExample('forms-deprecated/js/index.html', 'bootstrap')(format=".") +:marked +.l-main-section +:marked + ## Add Powers with ***ngFor** + Our hero may choose one super power from a fixed list of Agency-approved powers. + We maintain that list internally (in `HeroFormComponent`). + + We'll add a `select` to our + form and bind the options to the `powers` list using `NgFor`, + a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter. + + Add the following HTML *immediately below* the *Alter Ego* group. ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".") + +:marked + We are repeating the `` tag for each power in the list of Powers. + The `#p` local template variable is a different power in each iteration; + we display its name using the interpolation syntax with the double-curly-braces. + +.l-main-section +:marked + ## Two-way data binding with ***ngModel** + Running the app right now would be disappointing. + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding") +:marked + We don't see hero data because we are not binding to the `Hero` yet. + We know how to do that from earlier chapters. + [Displaying Data](./displaying-data.html) taught us Property Binding. + [User Input](./user-input.html) showed us how to listen for DOM events with an + Event Binding and how to update a component property with the displayed value. + + Now we need to display, listen, and extract at the same time. + + We could use those techniques again in our form. + Instead we'll introduce something new, the `NgModel` directive, that + makes binding our form to the model super-easy. + + Find the `` tag for the "Name" and update it like this + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".") + +.l-sub-section + :marked + We appended a diagnostic interpolation after the input tag + so we can see what we're doing. + We left ourselves a note to throw it way when we're done. + +:marked + Focus on the binding syntax: `[(ngModel)]="..."`. + + If we ran the app right now and started typing in the *Name* input box, + adding and deleting characters, we'd see them appearing and disappearing + from the interpolated text. + At some point it might look like this. +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action") +:marked + The diagnostic is evidence that we really are flowing values from the input box to the model and + back again. **That's two-way data binding!** + + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. + We'll ditch the input box binding message + and add a new binding at the top to the component's `diagnostic` method. + Then we can confirm that two-way data binding works *for the entire Hero model*. + + After revision the core of our form should have three `[(ngModel)]` bindings that + look much like this: + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)') + +:marked + If we ran the app right now and changed every Hero model property, the form might display like this: +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action") +:marked + The diagnostic near the top of the form + confirms that all of our changes are reflected in the model. + + **Delete** the `{{diagnostic()}}` binding at the top as it has served its purpose. + +.l-sub-section + :marked + ### Inside [(ngModel)] + *This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!* + + The punctuation in the binding syntax, [()], is a good clue to what's going on. + + In a Property Binding, a value flows from the model to a target property on screen. + We identify that target property by surrounding its name in brackets, []. + This is a one-way data binding **from the model to the view**. + + In an Event Binding, we flow the value from the target property on screen to the model. + We identify that target property by surrounding its name in parentheses, (). + This is a one-way data binding in the opposite direction **from the view to the model**. + + No wonder Angular chose to combine the punctuation as [()] + to signify a two-way data binding and a **flow of data in both directions**. + + In fact, we can break the `NgModel` binding into its two separate modes + as we do in this re-write of the "Name" `` binding: + +makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".") + + :marked +
The Property Binding should feel familiar. The Event Binding might seem strange. + + The `ngModelChange` is not an `` element event. + It is actually an event property of the `NgModel` directive. + When Angular sees a binding target in the form [(x)], + it expects the `x` directive to have an `x` input property and an `xChange` output property. + + The other oddity is the template expression, `model.name = $event`. + We're used to seeing an `$event` object coming from a DOM event. + The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter` + property that returns the input box value when it fires — which is precisely what + we should assign to the model's `name' property. + + Nice to know but is it practical? We almost always prefer `[(ngModel)]`. + We might split the binding if we had to do something special in + the event handling such as debounce or throttle the key strokes. + + Learn more about `NgModel` and other template syntax in the + [Template Syntax](./template-syntax.html) chapter. + +.l-main-section +:marked + ## Track change-state and validity with **ngControl** + + A form isn't just about data binding. We'd also like to know the state of the controls on our form. + The `NgControl` directive keeps track of control state for us. + +.callout.is-helpful + header NgControl requires Form + :marked + The `NgControl` is one of a family of `NgForm` directives that can only be applied to + a control within a ` tag. +:marked + Our application can ask an `NgControl` if the user touched the control, + if the value changed, or if the value became invalid. + + `NgControl` doesn't just track state; it updates the control with special + Angular CSS classes from the set we listed above. + We can leverage those class names to change the appearance of the + control and make messages appear or disappear. + + We'll explore those effects soon. Right now + we should **add `ngControl`to all three form controls**, + starting with the *Name* input box ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") +:marked + Be sure to assign a unique name to each `ngControl` directive. + +.l-sub-section + :marked + Angular registers controls under their `ngControl` names + with the `NgForm`. + We didn't add the `NgForm` directive explicitly but it's here + and we'll talk about it [later in this chapter](#ngForm). + +.l-main-section +:marked + ## Add Custom CSS for Visual Feedback + + `NgControl` doesn't just track state. + It updates the control with three classes that reflect the state. + +table + tr + th State + th Class if true + th Class if false + tr + td Control has been visited + td ng-touched + td ng-untouched + tr + td Control's value has changed + td ng-dirty + td ng-pristine + tr + td Control's value is valid + td ng-valid + td ng-invalid +:marked + Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy** + to the "Name" `` tag and use the spy to display those classes. + ++makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") + +:marked + Now run the app and focus on the *Name* input box. + Follow the next four steps *precisely* + + 1. Look but don't touched + 1. Click in the input box, then click outside the text input box + 1. Add slashes to the end of the name + 1. Erase the name + + The actions and effects are as follows: +figure.image-display + img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition") +:marked + We should be able to see the following four sets of class names and their transitions: +figure.image-display + img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions") + +:marked + The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a + strong visual signal when the data are invalid and we want to mark required fields. + + We realize we can do both at the same time with a colored bar on the left of the input box: + +figure.image-display + img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form") + +:marked + We achieve this effect by adding two styles to a new `forms.css` file + that we add to our project as a sibling to `index.html`. + ++makeExample('forms-deprecated/js/forms.css',null,'forms.css')(format=".") +:marked + These styles select for the two Angular validity classes and the HTML 5 "required" attribute. + + We update the `` of the `index.html` to include this style sheet. ++makeExample('forms-deprecated/js/index.html', 'styles', 'index.html (excerpt)')(format=".") +:marked + ## Show and Hide Validation Error messages + + We can do better. + + The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we + don't know *what* is wrong or what to do about it. + We can leverage the `ng-invalid` class to reveal a helpful message. + + Here's the way it should look when the user deletes the name: +figure.image-display + img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required") + +:marked + To achieve this effect we extend the `` tag with + 1. a [local template variable](./template-syntax.html#local-vars) + 1. the "*is required*" message in a nearby `
` which we'll display only if the control is invalid. + + Here's how we do it for the *name* input box: +-var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm }; ++makeExample('forms-deprecated/js/app/hero-form.component.html', + 'name-with-error-msg', + 'app/hero-form.component.html (excerpt)', + stylePattern) +:marked + When we added the `ngControl` directive, we bound it to the model's `name` property. + Here we initialize a template local variable (`name`) with the value "ngForm" (`#name="ngForm"`). + Angular recognizes that syntax and re-sets the `name` local template variable to the + `ngControl` directive instance. + In other words, the `name` local template variable becomes a handle on the `ngControl` object + for this input box. + + Now we can control visibility of the "name" error message by binding the message `
` element's `hidden` property + to the `ngControl` object's `valid` property. The message is hidden while the control is valid; + the message is revealed when the control becomes invalid. + +.l-sub-section + :marked + ### The NgForm directive + We just set a template local variable with the value of an `NgForm` directive. + Why did that work? We didn't add the **[`NgForm`](../api/common/NgForm-directive.html) directive** explicitly. + + Angular added it surreptitiously, wrapping it around the `
` element + + The `NgForm` directive supplements the `form` element with additional features. + It collects `Controls` (elements identified by an `ngControl` directive) + and monitors their properties including their validity. + It also has its own `valid` property which is true only if every contained + control is valid. +:marked + The Hero *Alter Ego* is optional so we can leave that be. + + Hero *Power* selection is required. + We can add the same kind of error handling to the `` tag. This is a requirement when using `[(ngModel)]` in combination with a form, so that we can easily refer to it in the aggregate form value and validity state. + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. We'll ditch the input box binding message and add a new binding at the top to the component's `diagnostic` method. @@ -348,44 +373,33 @@ figure.image-display .l-main-section :marked - ## Track change-state and validity with **ngControl** + ## Track change-state and validity with **ngModel** A form isn't just about data binding. We'd also like to know the state of the controls on our form. - The `NgControl` directive keeps track of control state for us. -.callout.is-helpful - header NgControl requires Form - :marked - The `NgControl` is one of a family of `NgForm` directives that can only be applied to - a control within a ` tag. -:marked - Our application can ask an `NgControl` if the user touched the control, - if the value changed, or if the value became invalid. + Using `ngModel` in a form gives us more than just two way data binding. It also tells us if the user touched the control, if the value changed, or if the value became invalid. - `NgControl` doesn't just track state; it updates the control with special - Angular CSS classes from the set we listed above. + `ngModel` doesn't just track state; it updates the control with special Angular CSS classes from the set we listed above. We can leverage those class names to change the appearance of the control and make messages appear or disappear. We'll explore those effects soon. Right now - we should **add `ngControl`to all three form controls**, - starting with the *Name* input box -+makeExample('forms/js/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") + let's make sure we have `ngModel` and the corresponding name attribute on all three form controls, + starting with the *Name* input box. ++makeExample('forms/js/app/hero-form.component.html', 'ngModelName-1', 'app/hero-form.component.html (excerpt)')(format=".") :marked - Be sure to assign a unique name to each `ngControl` directive. + We set the `name` attribute to "name" which makes sense for our app. Any unique value will do. .l-sub-section :marked - Angular registers controls under their `ngControl` names - with the `NgForm`. - We didn't add the `NgForm` directive explicitly but it's here - and we'll talk about it [later in this chapter](#ngForm). + Internally Angular creates `FormControls` and registers them with an `NgForm` directive that Angular attached to the `` tag. Each `FormControl` is registered under the name we assigned to the `name` attribute. + We'll talk about `NgForm` [later in the chapter](#ngForm). .l-main-section :marked ## Add Custom CSS for Visual Feedback - `NgControl` doesn't just track state. + `NgModel` doesn't just track state. It updates the control with three classes that reflect the state. table @@ -409,7 +423,7 @@ table Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy** to the "Name" `` tag and use the spy to display those classes. -+makeExample('forms/js/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") ++makeExample('forms/js/app/hero-form.component.html', 'ngModelName-2','app/hero-form.component.html (excerpt)')(format=".") :marked Now run the app and focus on the *Name* input box. @@ -472,16 +486,18 @@ figure.image-display 'app/hero-form.component.html (excerpt)', stylePattern) :marked - When we added the `ngControl` directive, we bound it to the model's `name` property. - Here we initialize a template local variable (`name`) with the value "ngForm" (`#name="ngForm"`). - Angular recognizes that syntax and re-sets the `name` local template variable to the - `ngControl` directive instance. - In other words, the `name` local template variable becomes a handle on the `ngControl` object - for this input box. - - Now we can control visibility of the "name" error message by binding the message `
` element's `hidden` property - to the `ngControl` object's `valid` property. The message is hidden while the control is valid; - the message is revealed when the control becomes invalid. + We need a template reference variable to access the input box's Angular control from within the template. + Here we created a variable called `name` and gave it the value "ngModel". +.l-sub-section + :marked + Why "ngModel"? + A directive's [exportAs](../api/core/DirectiveMetadata-class.html#!#exportAs) property + tells Angular how to link the reference variable to the directive. + We set `name` to `ngModel` because the `ngModel` directive's `exportAs` property happens to be "ngModel". + + Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `
` element's `hidden` property. + + The message is hidden while the control is valid; the message is revealed when the control becomes invalid. .l-sub-section :marked @@ -492,7 +508,7 @@ figure.image-display Angular added it surreptitiously, wrapping it around the `` element The `NgForm` directive supplements the `form` element with additional features. - It collects `Controls` (elements identified by an `ngControl` directive) + It holds the controls we created for the elements with `ngModel` directive and `name` attribute and monitors their properties including their validity. It also has its own `valid` property which is true only if every contained control is valid. @@ -597,8 +613,7 @@ figure.image-display - A form component class with a `Component` decorator. - The `ngSubmit` directive for handling the form submission. - Template local variables such as `#heroForm`, `#name`, `#alter-ego` and `#power`. - - The `ngModel` directive for two-way data binding. - - The `ngControl` for validation and form element change tracking. + - The `[(ngModel)]` syntax for two-way data binding, validation and change tracking. - The local variable’s `valid` property on input controls to check if a control is valid and show/hide error messages. - Controlling the submit button's enabled state by binding to `NgForm` validity. - Custom CSS classes that provide visual feedback to users about invalid controls. diff --git a/public/docs/ts/latest/guide/_data.json b/public/docs/ts/latest/guide/_data.json index c24dd17ee4..aee44f8f1f 100644 --- a/public/docs/ts/latest/guide/_data.json +++ b/public/docs/ts/latest/guide/_data.json @@ -29,7 +29,7 @@ "basics": true }, - "forms": { + "forms-deprecated": { "title": "Forms", "intro": "A form creates a cohesive, effective, and compelling data entry experience. An Angular form coordinates a set of data-bound user controls, tracks changes, validates input, and presents errors.", "nextable": true, diff --git a/public/docs/ts/latest/guide/forms-deprecated.jade b/public/docs/ts/latest/guide/forms-deprecated.jade new file mode 100644 index 0000000000..ed3699f402 --- /dev/null +++ b/public/docs/ts/latest/guide/forms-deprecated.jade @@ -0,0 +1,718 @@ +include ../_util-fns + +.alert.is-important + :marked + This guide is using the deprecated forms API. + + We have created a new version using the new API here. + +:marked + We’ve all used a form to login, submit a help request, place an order, book a flight, + schedule a meeting and perform countless other data entry tasks. + Forms are the mainstay of business applications. + + Any seasoned web developer can slap together an HTML form with all the right tags. + It's more challenging to create a cohesive data entry experience that guides the + user efficiently and effectively through the workflow behind the form. + + *That* takes design skills that are, to be frank, well out of scope for this chapter. + + It also takes framework support for + **two-way data binding, change tracking, validation, and error handling** + ... which we shall cover in this chapter on Angular forms. + + We will build a simple form from scratch, one step at a time. Along the way we'll learn + + - to build an Angular form with a component and template + + - two-way data binding with `[(ngModel)]` syntax for reading and writing values to input controls + + - using `ngControl` to track the change state and validity of form controls + + - the special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback + + - displaying validation errors to users and enable/disable form controls + + - sharing information among controls with template reference variables + + [Live Example](/resources/live-examples/forms-deprecated/ts/plnkr.html) +.l-main-section +:marked + ## Template-Driven Forms + + Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with + the form-specific directives and techniques described in this chapter. +.l-sub-section + :marked + That's not the only way to create a form but it's the way we'll cover in this chapter. +:marked + We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms. + We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors, + conditionally enable or disable specific controls, trigger built-in visual feedback, and much more. + + It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd + otherwise wrestle with ourselves. + + We'll discuss and learn to build the following template-driven form: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form") + +:marked + Here at the *Hero Employment Agency* we use this form to maintain personal information about the + heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis! + + Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot. + + If we delete the hero name, the form displays a validation error in an attention grabbing style: + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required") + +:marked + Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red. + +.l-sub-section + p We'll customize the colors and location of the "required" bar with standard CSS. + +:marked + We will build this form in the following sequence of small steps + + 1. Create the `Hero` model class + 1. Create the component that controls the form + 1. Create a template with the initial form layout + 1. Bind data properties to each form input control with the `ngModel` two-way data binding syntax + 1. Add the **ngControl** directive to each form input control + 1. Add custom CSS to provide visual feedback + 1. Show and hide validation error messages + 1. Handle form submission with **ngSubmit** + 1. Disable the form’s submit button until the form is valid + +:marked + ## Setup + Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html). + +include ../_quickstart_repo +:marked + ## Create the Hero Model Class + + As users enter form data, we capture their changes and update an instance of a model. + We can't layout the form until we know what the model looks like. + + A model can be as simple as a "property bag" that holds facts about a thing of application importance. + That describes well our `Hero` class with its three required fields (`id`, `name`, `power`) + and one optional field (`alterEgo`). + + Create a new file in the app folder called `hero.ts` and give it the following class definition: + ++makeExample('forms-deprecated/ts/app/hero.ts', null, 'app/hero.ts') + +:marked + It's an anemic model with few requirements and no behavior. Perfect for our demo. + + The TypeScript compiler generates a public field for each `public` constructor parameter and + assigns the parameter’s value to that field automatically when we create new heroes. + + The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`. + + We can create a new hero like this: +code-example(format=""). + let myHero = new Hero(42, 'SkyDog', + 'Fetch any object at any distance', 'Leslie Rollover'); + console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog" +:marked + +.l-main-section +:marked + ## Create a Form component + + An Angular form has two parts: an HTML-based template and a code-based Component to handle data and user interactions. + + We begin with the Component because it states, in brief, what the Hero editor can do. + + Create a new file called `hero-form.component.ts` and give it the following definition: + ++makeExample('forms-deprecated/ts/app/hero-form.component.ts', 'first', 'app/hero-form.component.ts') + +:marked + There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before. + + Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters + + 1. We import the `Component` decorator from the Angular library as we usually do. + + 1. The `@Component` selector value of "hero-form" means we can drop this form in a parent template with a `` tag. + + 1. The `templateUrl` property points to a separate file for template HTML called `hero-form.component.html`. + + 1. We defined dummy data for `model` and `powers` as befits a demo. + Down the road, we can inject a data service to get and save real data + or perhaps expose these properties as [inputs and outputs](./template-syntax.html#inputs-outputs) for binding to a + parent component. None of this concerns us now and these future changes won't affect our form. + + 1. We threw in a `diagnostic` property at the end to return a JSON representation of our model. + It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later. + + Why don't we write the template inline in the component file as we often do + elsewhere in the Developer Guide? + + There is no “right” answer for all occasions. We like inline templates when they are short. + Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to + write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code. + We also like short files with a clear and obvious purpose like this one. + + We made a good choice to put the HTML template elsewhere. + We'll write that template in a moment. Before we do, we'll take a step back + and revise the `app.component.ts` to make use of our new `HeroFormComponent`. + +.l-main-section +:marked + ## Revise the *app.component.ts* + + `app.component.ts` is the application's root component. It will host our new `HeroFormComponent`. + + Replace the contents of the "QuickStart" version with the following: ++makeExample('forms-deprecated/ts/app/app.component.ts', null, 'app/app.component.ts') + +:marked +.l-sub-section + :marked + There are only three changes: + + 1. We import the new `HeroFormComponent`. + + 1. The `template` is simply the new element tag identified by the component's `selector` property. + + 1. The `directives` array tells Angular that our template depends upon the `HeroFormComponent` + which is itself a Directive (as are all Components). + +.l-main-section +:marked + ## Create an initial HTML Form Template + + Create a new template file called `hero-form.component.html` and give it the following definition: + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'start', 'app/hero-form.component.html') + +:marked + That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and + opening them up for user input in input boxes. + + The *Name* `` control has the HTML5 `required` attribute; + the *Alter Ego* `` control does not because `alterEgo` is optional. + + We've got a *Submit* button at the bottom with some classes on it. + + **We are not using Angular yet**. There are no bindings. No extra directives. Just layout. + + The `container`,`form-group`, `form-control`, and `btn` classes + come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic. + We're using Bootstrap to gussy up our form. + Hey, what's a form without a little style! + +.callout.is-important + header Angular Forms Do Not Require A Style Library + :marked + Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or + the styles of any external library. Angular apps can use any CSS library + ... or none at all. + +:marked + Let's add the stylesheet. + +ol + li Open a terminal window in the application root folder and enter the command: + code-example(language="html" escape="html"). + npm install bootstrap --save + li Open index.html and add the following link to the <head>. + +makeExample('forms-deprecated/ts/index.html', 'bootstrap')(format=".") +:marked +.l-main-section +:marked + ## Add Powers with ***ngFor** + Our hero may choose one super power from a fixed list of Agency-approved powers. + We maintain that list internally (in `HeroFormComponent`). + + We'll add a `select` to our + form and bind the options to the `powers` list using `ngFor`, + a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter. + + Add the following HTML *immediately below* the *Alter Ego* group. ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".") + +:marked + We are repeating the `` tag for each power in the list of Powers. + The `p` template input variable is a different power in each iteration; + we display its name using the interpolation syntax with the double-curly-braces. + + +.l-main-section +:marked + ## Two-way data binding with **ngModel** + Running the app right now would be disappointing. + +figure.image-display + img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding") +:marked + We don't see hero data because we are not binding to the `Hero` yet. + We know how to do that from earlier chapters. + [Displaying Data](./displaying-data.html) taught us Property Binding. + [User Input](./user-input.html) showed us how to listen for DOM events with an + Event Binding and how to update a component property with the displayed value. + + Now we need to display, listen, and extract at the same time. + + We could use those techniques again in our form. + Instead we'll introduce something new, the `[(ngModel)]` syntax, that + makes binding our form to the model super-easy. + + Find the `` tag for the "Name" and update it like this + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".") + +.l-sub-section + :marked + We appended a diagnostic interpolation after the input tag + so we can see what we're doing. + We left ourselves a note to throw it away when we're done. + +:marked + Focus on the binding syntax: `[(ngModel)]="..."`. + + If we ran the app right now and started typing in the *Name* input box, + adding and deleting characters, we'd see them appearing and disappearing + from the interpolated text. + At some point it might look like this. +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action") +:marked + The diagnostic is evidence that we really are flowing values from the input box to the model and + back again. **That's two-way data binding!** + + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. + We'll ditch the input box binding message + and add a new binding at the top to the component's `diagnostic` property. + Then we can confirm that two-way data binding works *for the entire Hero model*. + + After revision the core of our form should have three `[(ngModel)]` bindings that + look much like this: + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)') + +:marked + If we ran the app right now and changed every Hero model property, the form might display like this: +figure.image-display + img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action") +:marked + The diagnostic near the top of the form + confirms that all of our changes are reflected in the model. + + **Delete** the `{{diagnostic}}` binding at the top as it has served its purpose. + +.l-sub-section + :marked + ### Inside [(ngModel)] + *This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!* + + The punctuation in the binding syntax, [()], is a good clue to what's going on. + + In a Property Binding, a value flows from the model to a target property on screen. + We identify that target property by surrounding its name in brackets, []. + This is a one-way data binding **from the model to the view**. + + In an Event Binding, we flow the value from the target property on screen to the model. + We identify that target property by surrounding its name in parentheses, (). + This is a one-way data binding in the opposite direction **from the view to the model**. + + No wonder Angular chose to combine the punctuation as [()] + to signify a two-way data binding and a **flow of data in both directions**. + + In fact, we can break the `NgModel` binding into its two separate modes + as we do in this re-write of the "Name" `` binding: + +makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".") + + :marked +
The Property Binding should feel familiar. The Event Binding might seem strange. + + The `ngModelChange` is not an `` element event. + It is actually an event property of the `NgModel` directive. + When Angular sees a binding target in the form [(x)], + it expects the `x` directive to have an `x` input property and an `xChange` output property. + + The other oddity is the template expression, `model.name = $event`. + We're used to seeing an `$event` object coming from a DOM event. + The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter` + property that returns the input box value when it fires — which is precisely what + we should assign to the model's `name` property. + + Nice to know but is it practical? We almost always prefer `[(ngModel)]`. + We might split the binding if we had to do something special in + the event handling such as debounce or throttle the key strokes. + + Learn more about `NgModel` and other template syntax in the + [Template Syntax](./template-syntax.html) chapter. + +.l-main-section +:marked + ## Track change-state and validity with **ngControl** + + A form isn't just about data binding. We'd also like to know the state of the controls on our form. + + By setting `ngControl` we create a directive that can tell if the user touched the control, + if the value changed, or if the value became invalid. + + This directive doesn't just track state; it updates the control with special + Angular CSS classes from the set we listed above. + We can leverage those class names to change the appearance of the + control and make messages appear or disappear. + + We'll explore those effects soon. Right now + we should **add `ngControl` to all three form controls**, + starting with the *Name* input box ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") +:marked + We set this particular `ngControl` to "name" which makes sense for our app. Any unique value will do. + +.l-sub-section + :marked + Internally Angular creates `Controls` and registers them under their `ngControl` names + with an `NgForm` directive that Angular attached to the `` tag. + We'll talk about `NgForm` [later in the chapter](#ngForm). + + The `ngControl` *attribute* in our template actually maps to the + [NgControlName](../api/common/NgControlName-directive.html) directive. + There is also a `NgControl` *abstract* directive which is *not the same thing*. + We often ignore this technical distinction and refer to `NgControlName` more conveniently (albeit incorrectly) as the *NgControl* directive. + + While we're under the hood, we might as well note that the `ngModel` in the + two-way binding syntax is now a property of the `NgControlName` directive. + The `NgModel` directive is no longer involved. We only need one directive to manage the DOM element + and there is no practical difference in the way either directive handles data binding. + +.l-main-section +:marked + ## Add Custom CSS for Visual Feedback + + The *NgControl* directive doesn't just track state. + It updates the control with three classes that reflect the state. + +table + tr + th State + th Class if true + th Class if false + tr + td Control has been visited + td ng-touched + td ng-untouched + tr + td Control's value has changed + td ng-dirty + td ng-pristine + tr + td Control's value is valid + td ng-valid + td ng-invalid +:marked + Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy** + to the "Name" `` tag and use the spy to display those classes. + ++makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") + +:marked + Now run the app and focus on the *Name* input box. + Follow the next four steps *precisely* + + 1. Look but don't touch + 1. Click in the input box, then click outside the text input box + 1. Add slashes to the end of the name + 1. Erase the name + + The actions and effects are as follows: +figure.image-display + img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition") +:marked + We should be able to see the following four sets of class names and their transitions: +figure.image-display + img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions") + +:marked + The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a + strong visual signal when the data are invalid and we want to mark required fields. + + We realize we can do both at the same time with a colored bar on the left of the input box: + +figure.image-display + img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form") + +:marked + We achieve this effect by adding two styles to a new `forms.css` file + that we add to our project as a sibling to `index.html`. + ++makeExample('forms-deprecated/ts/forms.css',null,'forms.css')(format=".") +:marked + These styles select for the two Angular validity classes and the HTML 5 "required" attribute. + + We update the `` of the `index.html` to include this style sheet. ++makeExample('forms-deprecated/ts/index.html', 'styles', 'index.html (excerpt)')(format=".") +:marked + ## Show and Hide Validation Error messages + + We can do better. + + The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we + don't know *what* is wrong or what to do about it. + We can leverage the `ng-invalid` class to reveal a helpful message. + + Here's the way it should look when the user deletes the name: +figure.image-display + img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required") + +:marked + To achieve this effect we extend the `` tag with + 1. a [template reference variable](./template-syntax.html#ref-vars) + 1. the "*is required*" message in a nearby `
` which we'll display only if the control is invalid. + + Here's how we do it for the *name* input box: ++makeExample('forms-deprecated/ts/app/hero-form.component.html', + 'name-with-error-msg', + 'app/hero-form.component.html (excerpt)')(format=".") +:marked + We need a template reference variable to access the input box's Angular control from within the template. + Here we created a variable called `name` and gave it the value "ngForm". +.l-sub-section + :marked + Why "ngForm"? + A directive's [exportAs](../api/core/DirectiveMetadata-class.html#!#exportAs) property + tells Angular how to link the reference variable to the directive. + We set `name` to `ngForm` because the `NgControlName` directive's `exportAs` property happens to be "ngForm". + + This seems unintuitive at first until we realize that *all* control directives in the + Angular form family — including `NgForm`, `NgModel`, `NgControlName` and `NgControlGroup` — *exportAs* "ngForm" + and we only ever apply *one* of these directives to an element tag. + Consistency rules! + + Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `
` element's `hidden` property. ++makeExample('forms-deprecated/ts/app/hero-form.component.html', + 'hidden-error-msg', + 'app/hero-form.component.html (excerpt)') +:marked + In this example, we hide the message when the control is valid or pristine; + pristine means the user hasn't changed the value since it was displayed in this form. + + This user experience is the developer's choice. Some folks want to see the message at all times. + If we ignore the `pristine` state, we would hide the message only when the value is valid. + If we arrive in this component with a new (blank) hero or an invalid hero, + we'll see the error message immediately, before we've done anything. + + Some folks find that behavior disconcerting. They only want to see the message when the user makes an invalid change. + Hiding the message while the control is "pristine" achieves that goal. + We'll see the significance of this choice when we [add a new hero](#new-hero) to the form. + + The Hero *Alter Ego* is optional so we can leave that be. + + Hero *Power* selection is required. + We can add the same kind of error handling to the `` tag. This is a requirement when using `[(ngModel)]` in combination with a form, so that we can easily refer to it in the aggregate form value and validity state. + Let's add similar `[(ngModel)]` bindings to *Alter Ego* and *Hero Power*. We'll ditch the input box binding message and add a new binding at the top to the component's `diagnostic` property. @@ -348,46 +374,33 @@ figure.image-display .l-main-section :marked - ## Track change-state and validity with **ngControl** + ## Track change-state and validity with **ngModel** A form isn't just about data binding. We'd also like to know the state of the controls on our form. - By setting `ngControl` we create a directive that can tell if the user touched the control, - if the value changed, or if the value became invalid. + Using `ngModel` in a form gives us more than just two way data binding. It also tells us if the user touched the control, if the value changed, or if the value became invalid. - This directive doesn't just track state; it updates the control with special - Angular CSS classes from the set we listed above. + `ngModel` doesn't just track state; it updates the control with special Angular CSS classes from the set we listed above. We can leverage those class names to change the appearance of the control and make messages appear or disappear. We'll explore those effects soon. Right now - we should **add `ngControl` to all three form controls**, - starting with the *Name* input box -+makeExample('forms/ts/app/hero-form.component.html', 'ngControl-1', 'app/hero-form.component.html (excerpt)')(format=".") + let's make sure we have `ngModel` and the corresponding name attribute on all three form controls, + starting with the *Name* input box. ++makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-1', 'app/hero-form.component.html (excerpt)')(format=".") :marked - We set this particular `ngControl` to "name" which makes sense for our app. Any unique value will do. + We set the `name` attribute to "name" which makes sense for our app. Any unique value will do. .l-sub-section :marked - Internally Angular creates `Controls` and registers them under their `ngControl` names - with an `NgForm` directive that Angular attached to the `` tag. + Internally Angular creates `FormControls` and registers them with an `NgForm` directive that Angular attached to the `` tag. Each `FormControl` is registered under the name we assigned to the `name` attribute. We'll talk about `NgForm` [later in the chapter](#ngForm). - The `ngControl` *attribute* in our template actually maps to the - [NgControlName](../api/common/NgControlName-directive.html) directive. - There is also a `NgControl` *abstract* directive which is *not the same thing*. - We often ignore this technical distinction and refer to `NgControlName` more conveniently (albeit incorrectly) as the *NgControl* directive. - - While we're under the hood, we might as well note that the `ngModel` in the - two-way binding syntax is now a property of the `NgControlName` directive. - The `NgModel` directive is no longer involved. We only need one directive to manage the DOM element - and there is no practical difference in the way either directive handles data binding. - .l-main-section :marked ## Add Custom CSS for Visual Feedback - The *NgControl* directive doesn't just track state. + The *NgModel* directive doesn't just track state. It updates the control with three classes that reflect the state. table @@ -411,7 +424,7 @@ table Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy** to the "Name" `` tag and use the spy to display those classes. -+makeExample('forms/ts/app/hero-form.component.html', 'ngControl-2','app/hero-form.component.html (excerpt)')(format=".") ++makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-2','app/hero-form.component.html (excerpt)')(format=".") :marked Now run the app and focus on the *Name* input box. @@ -473,18 +486,13 @@ figure.image-display 'app/hero-form.component.html (excerpt)')(format=".") :marked We need a template reference variable to access the input box's Angular control from within the template. - Here we created a variable called `name` and gave it the value "ngForm". + Here we created a variable called `name` and gave it the value "ngModel". .l-sub-section :marked - Why "ngForm"? + Why "ngModel"? A directive's [exportAs](../api/core/DirectiveMetadata-class.html#!#exportAs) property tells Angular how to link the reference variable to the directive. - We set `name` to `ngForm` because the `NgControlName` directive's `exportAs` property happens to be "ngForm". - - This seems unintuitive at first until we realize that *all* control directives in the - Angular form family — including `NgForm`, `NgModel`, `NgControlName` and `NgControlGroup` — *exportAs* "ngForm" - and we only ever apply *one* of these directives to an element tag. - Consistency rules! + We set `name` to `ngModel` because the `ngModel` directive's `exportAs` property happens to be "ngModel". Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `
` element's `hidden` property. +makeExample('forms/ts/app/hero-form.component.html', @@ -588,7 +596,7 @@ figure.image-display Angular did. Angular creates and attaches an `NgForm` directive to the `` tag automatically. The `NgForm` directive supplements the `form` element with additional features. - It holds the controls we created for the elements with `ngControl` attributes + It holds the controls we created for the elements with `ngModel` directive and `name` attribute and monitors their properties including their validity. It also has its own `valid` property which is true only *if every contained control* is valid. @@ -667,8 +675,7 @@ figure.image-display - A form component class with a `Component` decorator. - The `ngSubmit` directive for handling the form submission. - Template reference variables such as `#heroForm`, `#name`, `#alter-ego` and `#power`. - - The `[(ngModel)]` syntax for two-way data binding. - - The `ngControlName` directive for validation and form element change tracking. + - The `[(ngModel)]` syntax for two-way data binding, validation and change tracking. - The reference variable’s `valid` property on input controls to check if a control is valid and show/hide error messages. - Controlling the submit button's enabled state by binding to `NgForm` validity. - Custom CSS classes that provide visual feedback to users about invalid controls.