docs: delete forms- and router-deprecated, jade + examples (#2328)

This commit is contained in:
Ward Bell 2016-09-13 18:49:51 -07:00 committed by GitHub
parent f2b1bbaba9
commit 1fb488b4ec
58 changed files with 0 additions and 4784 deletions

View File

@ -1,64 +0,0 @@
/// <reference path='../_protractor/e2e.d.ts' />
'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);
});
});
});

View File

@ -1,12 +0,0 @@
// #docregion
(function(app) {
app.AppComponent = ng.core
.Component({
selector: 'my-app',
template: '<hero-form></hero-form>',
directives: [app.HeroFormComponent]
})
.Class({
constructor: function() {}
});
})(window.app || (window.app = {}));

View File

@ -1,195 +0,0 @@
<!-- #docplaster -->
<!-- #docregion final -->
<div class="container">
<!-- #docregion edit-div -->
<div [hidden]="submitted">
<h1>Hero Form</h1>
<!-- #docregion ngSubmit -->
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
<!-- #enddocregion ngSubmit -->
<!-- #enddocregion edit-div -->
<div class="form-group">
<label for="name">Name</label>
<!-- #docregion name-with-error-msg -->
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" #name="ngForm" >
<div [hidden]="name.valid" class="alert alert-danger">
Name is required
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control"
[(ngModel)]="model.alterEgo"
ngControl="alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" required
[(ngModel)]="model.power"
ngControl="power" #power="ngForm" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div [hidden]="power.valid" class="alert alert-danger">
Power is required
</div>
</div>
<!-- #docregion submit-button -->
<button type="submit" class="btn btn-default"
[disabled]="!heroForm.form.valid">Submit</button>
<!-- #enddocregion submit-button -->
</form>
</div>
<!-- #docregion submitted -->
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9 pull-left">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9 pull-left">{{ model.power }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">Edit</button>
</div>
<!-- #enddocregion submitted -->
</div>
<!-- #enddocregion final -->
<!-- ==================================================== -->
<div>
<form>
<!-- #docregion edit-div -->
<!-- ... all of the form ... -->
</form>
</div>
<!-- #enddocregion edit-div -->
<!-- ==================================================== -->
<hr>
<style>
.no-style .ng-valid {
border-left: 1px solid #CCC
}
.no-style .ng-invalid {
border-left: 1px solid #CCC
}
</style>
<div class="no-style" style="margin-left: 4px">
<!-- #docregion start -->
<div class="container">
<h1>Hero Form</h1>
<form>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required>
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control">
</div>
<!-- #enddocregion start -->
<!-- #docregion powers -->
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" required>
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
<!-- #enddocregion powers -->
<!-- #docregion start -->
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<!-- #enddocregion start -->
<!-- #enddocregion phase1-->
<!-- ==================================================== -->
<hr>
<!-- #docregion phase2-->
<div class="container">
<h1>Hero Form</h1>
<form>
<!-- #docregion ngModel-2-->
{{diagnostic()}}
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required
[(ngModel)]="model.name" >
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control"
[(ngModel)]="model.alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" required
[(ngModel)]="model.power" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
<!-- #enddocregion ngModel-2-->
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<!-- #enddocregion phase2-->
<!-- EXTRA MATERIAL FOR DOCUMENTATION -->
<hr>
<!-- #docregion ngModel-1-->
<input type="text" class="form-control" required
[(ngModel)]="model.name" >
TODO: remove this: {{model.name}}
<!-- #enddocregion ngModel-1-->
<hr>
<!-- #docregion ngModel-3-->
<input type="text" class="form-control" required
[ngModel]="model.name"
(ngModelChange)="model.name = $event" >
TODO: remove this: {{model.name}}
<!-- #enddocregion ngModel-3-->
<hr>
<form>
<!-- #docregion ngControl-1 -->
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" >
<!-- #enddocregion ngControl-1 -->
<hr>
<!-- #docregion ngControl-2 -->
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" #spy >
<br>TODO: remove this: {{spy.className}}
<!-- #enddocregion ngControl-2 -->
</form>
<div>
<hr>
Name via form.controls = {{showFormControls(heroForm)}}
</div>
</div>

View File

@ -1,52 +0,0 @@
// #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 = {}));

View File

@ -1,11 +0,0 @@
// #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 = {}));

View File

@ -1,6 +0,0 @@
// #docregion
(function(app) {
document.addEventListener('DOMContentLoaded', function() {
ng.platformBrowserDynamic.bootstrap(app.AppComponent);
});
})(window.app || (window.app = {}));

View File

@ -1,9 +0,0 @@
/* #docregion */
.ng-valid[required] {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid {
border-left: 5px solid #a94442; /* red */
}
/* #enddocregion */

View File

@ -1,45 +0,0 @@
<!DOCTYPE html>
<!-- #docplaster -->
<!-- #docregion -->
<html>
<head>
<title>Hero Form</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- #docregion bootstrap -->
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
<!-- #enddocregion bootstrap -->
<!-- #docregion styles -->
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="forms.css">
<!-- #enddocregion styles -->
<!-- IE required polyfill -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/@angular/core/bundles/core.umd.js"></script>
<script src="node_modules/@angular/common/bundles/common.umd.js"></script>
<script src="node_modules/@angular/compiler/bundles/compiler.umd.js"></script>
<script src="node_modules/@angular/platform-browser/bundles/platform-browser.umd.js"></script>
<script src="node_modules/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js"></script>
<!-- #docregion scripts-hero, scripts-hero-form -->
<script src='app/hero.js'></script>
<!-- #enddocregion scripts-hero -->
<script src='app/hero-form.component.js'></script>
<!-- #enddocregion scripts-hero-form -->
<!-- #docregion scripts, scripts-hero, scripts-hero-form -->
<script src='app/app.component.js'></script>
<script src='app/main.js'></script>
<!-- #enddocregion scripts, scripts-hero, scripts-hero-form -->
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

View File

@ -1,4 +0,0 @@
{
"description": "Forms",
"files":["app/**/*.js", "**/*.html", "**/*.css"]
}

View File

@ -1,10 +0,0 @@
// #docregion
import { Component } from '@angular/core';
import { HeroFormComponent } from './hero-form.component';
@Component({
selector: 'my-app',
template: '<hero-form></hero-form>',
directives: [HeroFormComponent]
})
export class AppComponent { }

View File

@ -1,208 +0,0 @@
<!-- #docplaster -->
<!-- #docregion final -->
<div class="container">
<!-- #docregion edit-div -->
<div [hidden]="submitted">
<h1>Hero Form</h1>
<!-- #docregion ngSubmit -->
<form *ngIf="active" (ngSubmit)="onSubmit()" #heroForm="ngForm">
<!-- #enddocregion ngSubmit -->
<!-- #enddocregion edit-div -->
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label>
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" #name="ngForm" >
<!-- #docregion hidden-error-msg -->
<div [hidden]="name.valid || name.pristine" class="alert alert-danger">
<!-- #enddocregion hidden-error-msg -->
Name is required
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control"
[(ngModel)]="model.alterEgo"
ngControl="alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" required
[(ngModel)]="model.power"
ngControl="power" #power="ngForm" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div [hidden]="power.valid || power.pristine" class="alert alert-danger">
Power is required
</div>
</div>
<!-- #docregion submit-button -->
<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>
<!-- #enddocregion submit-button -->
<!-- #docregion new-hero-button -->
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
<!-- #enddocregion new-hero-button -->
<!-- #enddocregion final -->
<!-- NOT SHOWN IN DOCS -->
<div>
<hr>
Name via form.controls = {{showFormControls(heroForm)}}
</div>
<!-- - -->
<!-- #docregion final -->
</form>
</div>
<!-- #docregion submitted -->
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9 pull-left">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9 pull-left">{{ model.power }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">Edit</button>
</div>
<!-- #enddocregion submitted -->
</div>
<!-- #enddocregion final -->
<!-- ==================================================== -->
<div>
<form>
<!-- #docregion edit-div -->
<!-- ... all of the form ... -->
</form>
</div>
<!-- #enddocregion edit-div -->
<!-- ==================================================== -->
<hr>
<style>
.no-style .ng-valid {
border-left: 1px solid #CCC
}
.no-style .ng-invalid {
border-left: 1px solid #CCC
}
</style>
<div class="no-style" style="margin-left: 4px">
<!-- #docregion start -->
<div class="container">
<h1>Hero Form</h1>
<form>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required>
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control">
</div>
<!-- #enddocregion start -->
<!-- #docregion powers -->
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" required>
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
<!-- #enddocregion powers -->
<!-- #docregion start -->
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<!-- #enddocregion start -->
<!-- #enddocregion phase1-->
<!-- ==================================================== -->
<hr>
<!-- #docregion phase2-->
<div class="container">
<h1>Hero Form</h1>
<form>
<!-- #docregion ngModel-2-->
{{diagnostic}}
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required
[(ngModel)]="model.name" >
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control"
[(ngModel)]="model.alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" required
[(ngModel)]="model.power" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
</div>
<!-- #enddocregion ngModel-2-->
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
<!-- #enddocregion phase2-->
<!-- EXTRA MATERIAL FOR DOCUMENTATION -->
<hr>
<!-- #docregion ngModel-1-->
<input type="text" class="form-control" required
[(ngModel)]="model.name" >
TODO: remove this: {{model.name}}
<!-- #enddocregion ngModel-1-->
<hr>
<!-- #docregion ngModel-3-->
<input type="text" class="form-control" required
[ngModel]="model.name"
(ngModelChange)="model.name = $event" >
TODO: remove this: {{model.name}}
<!-- #enddocregion ngModel-3-->
<hr>
<!-- #docregion form-active -->
<form *ngIf="active">
<!-- #enddocregion form-active -->
<!-- #docregion ngControl-1 -->
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" >
<!-- #enddocregion ngControl-1 -->
<hr>
<!-- #docregion ngControl-2 -->
<input type="text" class="form-control" required
[(ngModel)]="model.name"
ngControl="name" #spy >
<br>TODO: remove this: {{spy.className}}
<!-- #enddocregion ngControl-2 -->
</form>
</div>

View File

@ -1,66 +0,0 @@
// #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

View File

@ -1,11 +0,0 @@
// #docregion
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}

View File

@ -1,6 +0,0 @@
// #docregion
import { bootstrap } from '@angular/platform-browser-dynamic';
import { AppComponent } from './app.component';
bootstrap(AppComponent);

View File

@ -1,9 +0,0 @@
/* #docregion */
.ng-valid[required] {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid {
border-left: 5px solid #a94442; /* red */
}
/* #enddocregion */

View File

@ -1,34 +0,0 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<title>Hero Form</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- #docregion bootstrap -->
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
<!-- #enddocregion bootstrap -->
<!-- #docregion styles -->
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="forms.css">
<!-- #enddocregion styles -->
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

View File

@ -1,7 +0,0 @@
{
"description": "Forms-Deprecated",
"files":[
"!**/*.d.ts",
"!**/*.js"
]
}

View File

@ -1,127 +0,0 @@
/// <reference path='../_protractor/e2e.d.ts' />
'use strict';
describe('Router', function () {
beforeAll(function () {
browser.get('');
});
function getPageStruct() {
let hrefEles = element.all(by.css('my-app a'));
return {
hrefs: hrefEles,
routerParent: element(by.css('my-app > undefined')),
routerTitle: element(by.css('my-app > undefined > h2')),
crisisHref: hrefEles.get(0),
crisisList: element.all(by.css('my-app > undefined > undefined li')),
crisisDetail: element(by.css('my-app > undefined > undefined > div')),
crisisDetailTitle: element(by.css('my-app > undefined > undefined > div > h3')),
heroesHref: hrefEles.get(1),
heroesList: element.all(by.css('my-app > undefined li')),
heroDetail: element(by.css('my-app > undefined > div')),
heroDetailTitle: element(by.css('my-app > undefined > div > h3')),
};
}
it('should be able to see the start screen', function () {
let page = getPageStruct();
expect(page.hrefs.count()).toEqual(2, 'should be two dashboard choices');
expect(page.crisisHref.getText()).toEqual('Crisis Center');
expect(page.heroesHref.getText()).toEqual('Heroes');
});
it('should be able to see crises center items', function () {
let page = getPageStruct();
expect(page.crisisList.count()).toBe(4, 'should be 4 crisis center entries at start');
});
it('should be able to see hero items', function () {
let page = getPageStruct();
page.heroesHref.click().then(function() {
expect(page.routerTitle.getText()).toContain('HEROES');
expect(page.heroesList.count()).toBe(6, 'should be 6 heroes');
});
});
it('should be able to toggle the views', function () {
let page = getPageStruct();
page.crisisHref.click().then(function() {
expect(page.crisisList.count()).toBe(4, 'should be 4 crisis center entries');
return page.heroesHref.click();
}).then(function() {
expect(page.heroesList.count()).toBe(6, 'should be 6 heroes');
});
});
it('should be able to edit and save details from the crisis center view', function () {
crisisCenterEdit(2, true);
});
it('should be able to edit and cancel details from the crisis center view', function () {
crisisCenterEdit(3, false);
});
it('should be able to edit and save details from the heroes view', function () {
let page = getPageStruct();
let heroEle: protractor.ElementFinder;
let heroText: string;
page.heroesHref.click().then(function() {
heroEle = page.heroesList.get(4);
return heroEle.getText();
}).then(function(text) {
expect(text.length).toBeGreaterThan(0, 'should have some text');
// remove leading id from text
heroText = text.substr(text.indexOf(' ')).trim();
return heroEle.click();
}).then(function() {
expect(page.heroesList.count()).toBe(0, 'should no longer see crisis center entries');
expect(page.heroDetail.isPresent()).toBe(true, 'should be able to see crisis detail');
expect(page.heroDetailTitle.getText()).toContain(heroText);
let inputEle = page.heroDetail.element(by.css('input'));
return sendKeys(inputEle, '-foo');
}).then(function() {
expect(page.heroDetailTitle.getText()).toContain(heroText + '-foo');
let buttonEle = page.heroDetail.element(by.css('button'));
return buttonEle.click();
}).then(function() {
expect(heroEle.getText()).toContain(heroText + '-foo');
});
});
function crisisCenterEdit(index: number, shouldSave: boolean) {
let page = getPageStruct();
let crisisEle: protractor.ElementFinder;
let crisisText: string;
page.crisisHref.click()
.then(function () {
crisisEle = page.crisisList.get(index);
return crisisEle.getText();
}).then(function (text) {
expect(text.length).toBeGreaterThan(0, 'should have some text');
// remove leading id from text
crisisText = text.substr(text.indexOf(' ')).trim();
return crisisEle.click();
}).then(function () {
expect(page.crisisList.count()).toBe(0, 'should no longer see crisis center entries');
expect(page.crisisDetail.isPresent()).toBe(true, 'should be able to see crisis detail');
expect(page.crisisDetailTitle.getText()).toContain(crisisText);
let inputEle = page.crisisDetail.element(by.css('input'));
return sendKeys(inputEle, '-foo');
}).then(function () {
expect(page.crisisDetailTitle.getText()).toContain(crisisText + '-foo');
let buttonEle = page.crisisDetail.element(by.cssContainingText('button', shouldSave ? 'Save' : 'Cancel'));
return buttonEle.click();
}).then(function () {
if (shouldSave) {
expect(crisisEle.getText()).toContain(crisisText + '-foo');
} else {
expect(crisisEle.getText()).not.toContain(crisisText + '-foo');
}
});
}
});

View File

@ -1,43 +0,0 @@
/* First version */
// #docplaster
// #docregion
import { Component } from '@angular/core';
// #docregion import-router
import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated';
// #enddocregion import-router
import { CrisisListComponent } from './crisis-list.component';
import { HeroListComponent } from './hero-list.component';
@Component({
selector: 'my-app',
// #docregion template
template: `
<h1>Component Router (Deprecated)</h1>
<nav>
<a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,
// #enddocregion template
directives: [ROUTER_DIRECTIVES]
})
// #enddocregion
/*
// #docregion route-config
@Component({ ... })
// #enddocregion route-config
*/
// #docregion
// #docregion route-config
@RouteConfig([
// #docregion route-defs
{path: '/crisis-center', name: 'CrisisCenter', component: CrisisListComponent},
{path: '/heroes', name: 'Heroes', component: HeroListComponent}
// #enddocregion route-defs
])
export class AppComponent { }
// #enddocregion route-config
// #enddocregion

View File

@ -1,58 +0,0 @@
/* Second Heroes version */
// #docplaster
// #docregion
import { Component } from '@angular/core';
import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated';
import { CrisisListComponent } from './crisis-list.component';
// #enddocregion
/*
// Apparent Milestone 2 imports
// #docregion
// #docregion hero-import
import { HeroListComponent } from './heroes/hero-list.component';
import { HeroDetailComponent } from './heroes/hero-detail.component';
import { HeroService } from './heroes/hero.service';
// #enddocregion hero-import
// #enddocregion
*/
// Actual Milestone 2 imports
import { HeroListComponent } from './heroes/hero-list.component.1';
import { HeroDetailComponent } from './heroes/hero-detail.component.1';
import { HeroService } from './heroes/hero.service';
// #docregion
@Component({
selector: 'my-app',
template: `
<h1>Component Router (Deprecated)</h1>
<nav>
<a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,
providers: [HeroService],
directives: [ROUTER_DIRECTIVES]
})
// #enddocregion
/*
// #docregion route-config
@Component({ ... })
// #enddocregion route-config
*/
// #docregion
// #docregion route-config
@RouteConfig([
// #docregion route-defs
{path: '/crisis-center', name: 'CrisisCenter', component: CrisisListComponent},
{path: '/heroes', name: 'Heroes', component: HeroListComponent},
// #docregion hero-detail-route
{path: '/hero/:id', name: 'HeroDetail', component: HeroDetailComponent}
// #enddocregion hero-detail-route
// #enddocregion route-defs
])
export class AppComponent { }
// #enddocregion route-config
// #enddocregion

View File

@ -1,52 +0,0 @@
// #docplaster
import { Component } from '@angular/core';
import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component.1';
import { DialogService } from './dialog.service';
import { HeroService } from './heroes/hero.service';
@Component({
selector: 'my-app',
// #enddocregion
/* Typical link
// #docregion h-anchor
<a [routerLink]="['Heroes']">Heroes</a>
// #enddocregion h-anchor
*/
/* Incomplete Crisis Center link when CC lacks a default
// #docregion cc-anchor-fail
// The link now fails with a "non-terminal link" error
// #docregion cc-anchor-w-default
<a [routerLink]="['CrisisCenter']">Crisis Center</a>
// #enddocregion cc-anchor-w-default
// #enddocregion cc-anchor-fail
*/
/* Crisis Center link when CC lacks a default
// #docregion cc-anchor-no-default
<a [routerLink]="['CrisisCenter', 'CrisisList']">Crisis Center</a>
// #enddocregion cc-anchor-no-default
*/
/* Crisis Center Detail link
// #docregion Dragon-anchor
<a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:1}]">Dragon Crisis</a>
// #enddocregion Dragon-anchor
*/
// #docregion template
template: `
<h1 class="title">Component Router (Deprecated)</h1>
<nav>
<a [routerLink]="['CrisisCenter', 'CrisisList']">Crisis Center</a>
<a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:1}]">Dragon Crisis</a>
<a [routerLink]="['CrisisCenter', 'CrisisDetail', {id:2}]">Shark Crisis</a>
</nav>
<router-outlet></router-outlet>
`,
// #enddocregion template
providers: [DialogService, HeroService],
directives: [ROUTER_DIRECTIVES]
})
@RouteConfig([
{path: '/crisis-center/...', name: 'CrisisCenter', component: CrisisCenterComponent},
])
export class AppComponent { }

View File

@ -1,44 +0,0 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
import { RouteConfig, ROUTER_DIRECTIVES } from '@angular/router-deprecated';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { HeroListComponent } from './heroes/hero-list.component';
import { HeroDetailComponent } from './heroes/hero-detail.component';
import { DialogService } from './dialog.service';
import { HeroService } from './heroes/hero.service';
@Component({
selector: 'my-app',
// #docregion template
template: `
<h1 class="title">Component Router (Deprecated)</h1>
<nav>
<a [routerLink]="['CrisisCenter']">Crisis Center</a>
<a [routerLink]="['Heroes']">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,
// #enddocregion template
providers: [DialogService, HeroService],
directives: [ROUTER_DIRECTIVES]
})
// #docregion route-config
@RouteConfig([
// #docregion route-config-cc
{ // Crisis Center child route
path: '/crisis-center/...',
name: 'CrisisCenter',
component: CrisisCenterComponent,
useAsDefault: true
},
// #enddocregion route-config-cc
{path: '/heroes', name: 'Heroes', component: HeroListComponent},
{path: '/hero/:id', name: 'HeroDetail', component: HeroDetailComponent},
])
// #enddocregion route-config
export class AppComponent { }

View File

@ -1,28 +0,0 @@
import { Component } from '@angular/core';
import { RouteConfig, RouterOutlet } from '@angular/router-deprecated';
import { CrisisListComponent } from './crisis-list.component.1';
import { CrisisDetailComponent } from './crisis-detail.component.1';
import { CrisisService } from './crisis.service';
// #docregion minus-imports
@Component({
template: `
<h2>CRISIS CENTER</h2>
<router-outlet></router-outlet>
`,
directives: [RouterOutlet],
// #docregion providers
providers: [CrisisService]
// #enddocregion providers
})
// #docregion route-config
@RouteConfig([
// #docregion default-route
{path: '/', name: 'CrisisList', component: CrisisListComponent, useAsDefault: true},
// #enddocregion default-route
{path: '/:id', name: 'CrisisDetail', component: CrisisDetailComponent}
])
// #enddocregion route-config
export class CrisisCenterComponent { }
// #enddocregion minus-imports

View File

@ -1,22 +0,0 @@
// #docregion
import { Component } from '@angular/core';
import { RouteConfig, RouterOutlet } from '@angular/router-deprecated';
import { CrisisListComponent } from './crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail.component';
import { CrisisService } from './crisis.service';
@Component({
template: `
<h2>CRISIS CENTER</h2>
<router-outlet></router-outlet>
`,
directives: [RouterOutlet],
providers: [CrisisService]
})
@RouteConfig([
{path: '/', name: 'CrisisList', component: CrisisListComponent, useAsDefault: true},
{path: '/:id', name: 'CrisisDetail', component: CrisisDetailComponent}
])
export class CrisisCenterComponent { }
// #enddocregion

View File

@ -1,95 +0,0 @@
// #docplaster
// #docregion
import { Component, OnInit } from '@angular/core';
import { RouteParams, Router } from '@angular/router-deprecated';
// #docregion routerCanDeactivate
import { CanDeactivate, ComponentInstruction } from '@angular/router-deprecated';
import { DialogService } from '../dialog.service';
// #enddocregion routerCanDeactivate
import { Crisis, CrisisService } from './crisis.service';
@Component({
// #docregion template
template: `
<div *ngIf="crisis">
<h3>"{{editName}}"</h3>
<div>
<label>Id: </label>{{crisis.id}}</div>
<div>
<label>Name: </label>
<input [(ngModel)]="editName" placeholder="name"/>
</div>
<p>
<button (click)="save()">Save</button>
<button (click)="cancel()">Cancel</button>
</p>
</div>
`,
// #enddocregion template
styles: ['input {width: 20em}']
})
// #docregion routerCanDeactivate, cancel-save
export class CrisisDetailComponent implements OnInit, CanDeactivate {
crisis: Crisis;
editName: string;
// #enddocregion routerCanDeactivate, cancel-save
constructor(
private service: CrisisService,
private router: Router,
private routeParams: RouteParams,
private dialog: DialogService
) { }
// #docregion ngOnInit
ngOnInit() {
let id = +this.routeParams.get('id');
this.service.getCrisis(id).then(crisis => {
if (crisis) {
this.editName = crisis.name;
this.crisis = crisis;
} else { // id not found
this.gotoCrises();
}
});
}
// #enddocregion ngOnInit
// #docregion routerCanDeactivate
routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction): any {
// Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged.
if (!this.crisis || this.crisis.name === this.editName) {
return true;
}
// Otherwise ask the user with the dialog service and return its
// promise which resolves to true or false when the user decides
return this.dialog.confirm('Discard changes?');
}
// #enddocregion routerCanDeactivate
// #docregion cancel-save
cancel() {
this.editName = this.crisis.name;
this.gotoCrises();
}
save() {
this.crisis.name = this.editName;
this.gotoCrises();
}
// #enddocregion cancel-save
// #docregion gotoCrises
gotoCrises() {
// Like <a [routerLink]="['CrisisList']">Crisis Center</a
this.router.navigate(['CrisisList']);
}
// #enddocregion gotoCrises
// #docregion routerCanDeactivate, cancel-save
}
// #enddocregion routerCanDeactivate, cancel-save
// #enddocregion

View File

@ -1,85 +0,0 @@
// #docplaster
// #docregion
import { Component, OnInit } from '@angular/core';
import { RouteParams, Router } from '@angular/router-deprecated';
import { CanDeactivate, ComponentInstruction } from '@angular/router-deprecated';
import { Crisis, CrisisService } from './crisis.service';
import { DialogService } from '../dialog.service';
@Component({
template: `
<div *ngIf="crisis">
<h3>"{{editName}}"</h3>
<div>
<label>Id: </label>{{crisis.id}}</div>
<div>
<label>Name: </label>
<input [(ngModel)]="editName" placeholder="name"/>
</div>
<p>
<button (click)="save()">Save</button>
<button (click)="cancel()">Cancel</button>
</p>
</div>
`,
styles: ['input {width: 20em}']
})
export class CrisisDetailComponent implements OnInit, CanDeactivate {
crisis: Crisis;
editName: string;
constructor(
private service: CrisisService,
private router: Router,
private routeParams: RouteParams,
private _dialog: DialogService
) { }
ngOnInit() {
let id = +this.routeParams.get('id');
this.service.getCrisis(id).then(crisis => {
if (crisis) {
this.editName = crisis.name;
this.crisis = crisis;
} else { // id not found
this.gotoCrises();
}
});
}
routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction): any {
// Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged.
if (!this.crisis || this.crisis.name === this.editName) {
return true;
}
// Otherwise ask the user with the dialog service and return its
// promise which resolves to true or false when the user decides
return this._dialog.confirm('Discard changes?');
}
cancel() {
this.editName = this.crisis.name;
this.gotoCrises();
}
save() {
this.crisis.name = this.editName;
this.gotoCrises();
}
// #docregion gotoCrises
gotoCrises() {
let crisisId = this.crisis ? this.crisis.id : null;
// Pass along the hero id if available
// so that the CrisisListComponent can select that hero.
// Add a totally useless `foo` parameter for kicks.
// #docregion gotoCrises-navigate
this.router.navigate(['CrisisList', {id: crisisId, foo: 'foo'} ]);
// #enddocregion gotoCrises-navigate
}
// #enddocregion gotoCrises
}

View File

@ -1,37 +0,0 @@
// #docplaster
// #docregion
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router-deprecated';
import { Crisis, CrisisService } from './crisis.service';
@Component({
// #docregion template
template: `
<ul class="items">
<li *ngFor="let crisis of crises"
(click)="onSelect(crisis)">
<span class="badge">{{crisis.id}}</span> {{crisis.name}}
</li>
</ul>
`,
// #enddocregion template
})
export class CrisisListComponent implements OnInit {
crises: Crisis[];
constructor(
private service: CrisisService,
private router: Router) {}
ngOnInit() {
this.service.getCrises().then(crises => this.crises = crises);
}
// #docregion select
onSelect(crisis: Crisis) {
this.router.navigate(['CrisisDetail', { id: crisis.id }] );
}
// #enddocregion select
}

View File

@ -1,41 +0,0 @@
// #docplaster
// #docregion
import { Component, OnInit } from '@angular/core';
import { RouteParams, Router } from '@angular/router-deprecated';
import { Crisis, CrisisService } from './crisis.service';
@Component({
template: `
<ul class="items">
<li *ngFor="let crisis of crises"
[class.selected]="isSelected(crisis)"
(click)="onSelect(crisis)">
<span class="badge">{{crisis.id}}</span> {{crisis.name}}
</li>
</ul>
`,
})
export class CrisisListComponent implements OnInit {
crises: Crisis[];
private selectedId: number;
constructor(
private service: CrisisService,
private router: Router,
routeParams: RouteParams) {
this.selectedId = +routeParams.get('id');
}
isSelected(crisis: Crisis) { return crisis.id === this.selectedId; }
ngOnInit() {
this.service.getCrises().then(crises => this.crises = crises);
}
onSelect(crisis: Crisis) {
this.router.navigate( ['CrisisDetail', { id: crisis.id }] );
}
}

View File

@ -1,40 +0,0 @@
// #docplaster
// #docregion
import { Injectable } from '@angular/core';
export class Crisis {
constructor(public id: number, public name: string) { }
}
let crises = [
new Crisis(1, 'Dragon Burning Cities'),
new Crisis(2, 'Sky Rains Great White Sharks'),
new Crisis(3, 'Giant Asteroid Heading For Earth'),
new Crisis(4, 'Procrastinators Meeting Delayed Again'),
];
let crisesPromise = Promise.resolve(crises);
@Injectable()
export class CrisisService {
getCrises() { return crisesPromise; }
getCrisis(id: number | string) {
return crisesPromise
.then(crises => crises.find(c => c.id === +id));
}
// #enddocregion
static nextCrisisId = 100;
addCrisis(name: string) {
name = name.trim();
if (name) {
let crisis = new Crisis(CrisisService.nextCrisisId++, name);
crisesPromise.then(crises => crises.push(crisis));
}
}
// #docregion
}
// #enddocregion

View File

@ -1,10 +0,0 @@
// Initial empty version
// #docregion
import { Component } from '@angular/core';
@Component({
template: `
<h2>CRISIS CENTER</h2>
<p>Get your crisis here</p>`
})
export class CrisisListComponent { }

View File

@ -1,18 +0,0 @@
// #docregion
import { Injectable } from '@angular/core';
/**
* Async modal dialog service
* DialogService makes this app easier to test by faking this service.
* TODO: better modal implementation that doesn't use window.confirm
*/
@Injectable()
export class DialogService {
/**
* Ask user to confirm an action. `message` explains the action and choices.
* Returns promise resolving to `true`=confirm or `false`=cancel
*/
confirm(message?: string) {
return new Promise<boolean>((resolve, reject) =>
resolve(window.confirm(message || 'Is it OK?')));
};
}

View File

@ -1,10 +0,0 @@
/// Initial empty version
// #docregion
import { Component } from '@angular/core';
@Component({
template: `
<h2>HEROES</h2>
<p>Get your heroes here</p>`
})
export class HeroListComponent { }

View File

@ -1,47 +0,0 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { RouteParams, Router } from '@angular/router-deprecated';
import { Hero, HeroService } from './hero.service';
@Component({
template: `
<h2>HEROES</h2>
<div *ngIf="hero">
<h3>"{{hero.name}}"</h3>
<div>
<label>Id: </label>{{hero.id}}</div>
<div>
<label>Name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
<p>
<button (click)="gotoHeroes()">Back</button>
</p>
</div>
`,
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
// #docregion ctor
constructor(
private router: Router,
private routeParams: RouteParams,
private service: HeroService) {}
// #enddocregion ctor
// #docregion ngOnInit
ngOnInit() {
let id = this.routeParams.get('id');
this.service.getHero(id).then(hero => this.hero = hero);
}
// #enddocregion ngOnInit
// #docregion gotoHeroes
gotoHeroes() {
// Like <a [routerLink]="['Heroes']">Heroes</a>
this.router.navigate(['Heroes']);
}
// #enddocregion gotoHeroes
}

View File

@ -1,52 +0,0 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { RouteParams, Router } from '@angular/router-deprecated';
import { Hero, HeroService } from './hero.service';
@Component({
template: `
<h2>HEROES</h2>
<div *ngIf="hero">
<h3>"{{hero.name}}"</h3>
<div>
<label>Id: </label>{{hero.id}}</div>
<div>
<label>Name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
<p>
<button (click)="gotoHeroes()">Back</button>
</p>
</div>
`,
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
// #docregion ctor
constructor(
private router: Router,
private routeParams: RouteParams,
private service: HeroService) {}
// #enddocregion ctor
// #docregion ngOnInit
ngOnInit() {
let id = this.routeParams.get('id');
this.service.getHero(id).then(hero => this.hero = hero);
}
// #enddocregion ngOnInit
// #docregion gotoHeroes
gotoHeroes() {
let heroId = this.hero ? this.hero.id : null;
// Pass along the hero id if available
// so that the HeroList component can select that hero.
// Add a totally useless `foo` parameter for kicks.
// #docregion gotoHeroes-navigate
this.router.navigate(['Heroes', {id: heroId, foo: 'foo'} ]);
// #enddocregion gotoHeroes-navigate
}
// #enddocregion gotoHeroes
}

View File

@ -1,50 +0,0 @@
// #docplaster
// #docregion
// TODO SOMEDAY: Feature Componetized like HeroCenter
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router-deprecated';
import { Hero, HeroService } from './hero.service';
@Component({
// #docregion template
template: `
<h2>HEROES</h2>
<ul class="items">
<li *ngFor="let hero of heroes"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
`
// #enddocregion template
})
export class HeroListComponent implements OnInit {
heroes: Hero[];
// #docregion ctor
constructor(
private router: Router,
private service: HeroService) { }
// #enddocregion ctor
ngOnInit() {
this.service.getHeroes().then(heroes => this.heroes = heroes);
}
// #docregion select
onSelect(hero: Hero) {
// #docregion nav-to-detail
this.router.navigate( ['HeroDetail', { id: hero.id }] );
// #enddocregion nav-to-detail
}
// #enddocregion select
}
// #enddocregion
/* A link parameters array
// #docregion link-parameters-array
['HeroDetail', { id: hero.id }] // {id: 15}
// #enddocregion link-parameters-array
*/

View File

@ -1,56 +0,0 @@
// #docplaster
// TODO SOMEDAY: Feature Componetized like CrisisCenter
// #docregion
import { Component, OnInit } from '@angular/core';
// #docregion import-route-params
import { RouteParams, Router } from '@angular/router-deprecated';
// #enddocregion import-route-params
import { Hero, HeroService } from './hero.service';
@Component({
// #docregion template
template: `
<h2>HEROES</h2>
<ul class="items">
<li *ngFor="let hero of heroes"
[class.selected]="isSelected(hero)"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
`
// #enddocregion template
})
export class HeroListComponent implements OnInit {
heroes: Hero[];
// #docregion ctor
private selectedId: number;
constructor(
private service: HeroService,
private router: Router,
routeParams: RouteParams) {
this.selectedId = +routeParams.get('id');
}
// #enddocregion ctor
// #docregion isSelected
isSelected(hero: Hero) { return hero.id === this.selectedId; }
// #enddocregion isSelected
// #docregion select
onSelect(hero: Hero) {
this.router.navigate( ['HeroDetail', { id: hero.id }] );
}
// #enddocregion select
ngOnInit() {
this.service.getHeroes().then(heroes => this.heroes = heroes);
}
}
// #enddocregion

View File

@ -1,27 +0,0 @@
// #docregion
import { Injectable } from '@angular/core';
export class Hero {
constructor(public id: number, public name: string) { }
}
let HEROES = [
new Hero(11, 'Mr. Nice'),
new Hero(12, 'Narco'),
new Hero(13, 'Bombasto'),
new Hero(14, 'Celeritas'),
new Hero(15, 'Magneta'),
new Hero(16, 'RubberMan')
];
let heroesPromise = Promise.resolve(HEROES);
@Injectable()
export class HeroService {
getHeroes() { return heroesPromise; }
getHero(id: number | string) {
return heroesPromise
.then(heroes => heroes.find(h => h.id === +id));
}
}

View File

@ -1,24 +0,0 @@
/* First version */
// #docplaster
// #docregion all
import { bootstrap } from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router-deprecated';
import { AppComponent } from './app.component';
// #enddocregion all
/* Can't use AppComponent ... but display as if we can
// #docregion all
bootstrap(AppComponent, [
// #enddocregion all
*/
// Actually use the v.1 component
import { AppComponent as ac } from './app.component.1';
bootstrap(ac, [
// #docregion all
ROUTER_PROVIDERS
]);
// #enddocregion all

View File

@ -1,31 +0,0 @@
/* Second version */
// For Milestone #2
// Also includes digression on HashPathStrategy (not used in the final app)
// #docplaster
// #docregion
import { bootstrap } from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router-deprecated';
// Add these symbols to override the `LocationStrategy`
import { LocationStrategy,
HashLocationStrategy } from '@angular/common';
import { AppComponent } from './app.component';
// #enddocregion
/* Can't use AppComponent ... but display as if we can
// #docregion
bootstrap(AppComponent, [
// #enddocregion
*/
// Actually use the v.2 component
import { AppComponent as ac } from './app.component.2';
bootstrap(ac, [
// #docregion
ROUTER_PROVIDERS,
{ provide: LocationStrategy, useClass: HashLocationStrategy } // .../#/crisis-center/
]);
// #enddocregion

View File

@ -1,7 +0,0 @@
// #docregion
import { bootstrap } from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router-deprecated';
import { AppComponent } from './app.component.3';
bootstrap(AppComponent, [ROUTER_PROVIDERS]);

View File

@ -1,7 +0,0 @@
// #docregion
import { bootstrap } from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router-deprecated';
import { AppComponent } from './app.component';
bootstrap(AppComponent, [ROUTER_PROVIDERS]);

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<!-- #docregion base-href -->
<base href="/">
<!-- #enddocregion base-href -->
<title>Router (Deprecated) Sample v.1</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app/main.1') // <----- ONLY CHANGE
.then(null, console.error.bind(console));
</script>
</head>
<body>
<h1>Milestone 1</h1>
<my-app>loading...</my-app>
</body>
</html>
<!-- #enddocregion -->

View File

@ -1,31 +0,0 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<base href="/">
<title>Router (Deprecated) Sample v.2</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app/main.2') // <----- ONLY CHANGE
.then(null, console.error.bind(console));
</script>
</head>
<body>
<h1>Milestone 2</h1>
<my-app>loading...</my-app>
</body>
</html>
<!-- #enddocregion -->

View File

@ -1,31 +0,0 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<base href="/">
<title>Router (Deprecated) Sample v.3</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app/main.3') // <----- ONLY CHANGE
.then(null, console.error.bind(console));
</script>
</head>
<body>
<h1>Milestone 3</h1>
<my-app>loading...</my-app>
</body>
</html>
<!-- #enddocregion -->

View File

@ -1,30 +0,0 @@
<!DOCTYPE html>
<!-- #docregion -->
<html>
<head>
<!-- Set the base href -->
<base href="/">
<title>Router (Deprecated) Sample</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>loading...</my-app>
</body>
</html>
<!-- #enddocregion -->

View File

@ -1,12 +0,0 @@
{
"description": "Router (Deprecated Beta)",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2,3].*",
"!app/crisis-list.component.ts",
"!app/hero-list.component.ts",
"!app/crisis-center/add-crisis.component.ts"
],
"tags": ["router", "deprecated"]
}

View File

@ -36,13 +36,6 @@
"basics": true "basics": true
}, },
"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.",
"basics": true,
"hide": true
},
"dependency-injection": { "dependency-injection": {
"title": "Dependency Injection", "title": "Dependency Injection",
"intro": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\".", "intro": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\".",
@ -130,12 +123,6 @@
"intro": "Pipes transform displayed values within a template." "intro": "Pipes transform displayed values within a template."
}, },
"router-deprecated": {
"title": "Routing & Navigation",
"intro": "Discover the basics of screen navigation with the Angular 2 Component Router.",
"hide": true
},
"router": { "router": {
"title": "Routing & Navigation", "title": "Routing & Navigation",
"intro": "Discover the basics of screen navigation with the Angular 2 Component Router." "intro": "Discover the basics of screen navigation with the Angular 2 Component Router."

View File

@ -1,4 +0,0 @@
include ../_util-fns
:marked
This page has no Dart equivalent. Instead, see the [forms guide](forms.html).

View File

@ -1,4 +0,0 @@
include ../_util-fns
:marked
This page has no Dart equivalent. Instead, see the [router guide](router.html).

View File

@ -116,12 +116,6 @@
"intro": "Pipes transform displayed values within a template." "intro": "Pipes transform displayed values within a template."
}, },
"router-deprecated": {
"title": "Router (Deprecated Beta)",
"intro": "The deprecated Beta Router.",
"hide": true
},
"router": { "router": {
"title": "Routing & Navigation", "title": "Routing & Navigation",
"intro": "Discover the basics of screen navigation with the Angular 2 router." "intro": "Discover the basics of screen navigation with the Angular 2 router."

View File

@ -1,649 +0,0 @@
include ../_util-fns
.alert.is-important
:marked
This guide is using the deprecated forms API, which is disabled as of RC5, thus this sample only works up to RC4.
We have created a new version using the new API <a href='/docs/js/latest/guide/forms.html'>here</a>.
:marked
Weve 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>Live Example</live-example>
.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 &mdash; 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 forms 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 `<head>` 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
Theres 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 weve 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 `<hero-form>` 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 `<head>` 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* `<input>` control has the HTML5 `required` attribute;
the *Alter Ego* `<input>` 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 <code>index.html</code> and add the following link to the <code>&lt;head></code>.
+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 `<options>` 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 `<input>` 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, <span style="font-family:courier"><b>[()]</b></span>, 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, <span style="font-family:courier"><b>[]</b></span>.
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, <span style="font-family:courier"><b>()</b></span>.
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 <span style="font-family:courier"><b>[()]</b></span>
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" `<input>` binding:
+makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".")
:marked
<br>The Property Binding should feel familiar. The Event Binding might seem strange.
The `ngModelChange` is not an `<input>` element event.
It is actually an event property of the `NgModel` directive.
When Angular sees a binding target in the form <span style="font-family:courier">[(x)]</span>,
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 &mdash; 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 `<form`> 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 <code>ng-touched</code>
td <code>ng-untouched</code>
tr
td Control's value has changed
td <code>ng-dirty</code>
td <code>ng-pristine</code>
tr
td Control's value is valid
td <code>ng-valid</code>
td <code>ng-invalid</code>
:marked
Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy**
to the "Name" `<input>` 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 `<head>` 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 `<input>` tag with
1. a [local template variable](./template-syntax.html#local-vars)
1. the "*is required*" message in a nearby `<div>` 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=&quot;form&quot;)|(.*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 `<div>` 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.
<a id="ngForm"></a>
.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/index/NgForm-directive.html) directive** explicitly.
Angular added it surreptitiously, wrapping it around the `<form>` 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 `<select>` if we want
but it's not imperative because the selection box already constrains the
power to valid value.
.l-main-section
:marked
## Submit the form with **ngSubmit**
The user should be able to submit this form after filling it in.
The Submit button at the bottom of the form
does nothing on its own but it will
trigger a form submit because of its type (`type="submit"`).
A "form submit" is useless at the moment.
To make it useful, we'll update the `<form>` tag with another Angular directive, `NgSubmit`,
and bind it to the `HeroFormComponent.submit()` method with an event binding
+makeExample('forms-deprecated/js/app/hero-form.component.html', 'ngSubmit')(format=".")
:marked
We slipped in something extra there at the end! We defined a
template local variable, **`#heroForm`**, and initialized it with the value, "ngForm".
The variable `heroForm` is now a handle to the `NgForm` directive that we [discussed earlier](#ngForm)
This time `heroForm` remains a reference to the form as a whole.
Later in the template we bind the button's `disabled` property to the form's over-all validity via
the `heroForm` variable. Here's that bit of markup:
+makeExample('forms-deprecated/js/app/hero-form.component.html', 'submit-button')
:marked
Re-run the application. The form opens in a valid state and the button is enabled.
Now delete the *Name*. We violate the "name required" rule which
is duly noted in our error message as before. And now the Submit button is also disabled.
Not impressed? Think about it for a moment. What would we have to do to
wire the button's enable/disabled state to the form's validity without Angular's help?
For us, it was as simple as
1. Define a template local variable on the (enhanced) form element
2. Reference that variable in a button some 50 lines away.
.l-main-section
:marked
## Toggle two form regions (extra credit)
Submitting the form isn't terribly dramatic at the moment.
.l-sub-section
:marked
An unsurprising observation for a demo. To be honest,
jazzing it up won't teach us anything new about forms.
But this is an opportunity to exercise some of our newly won
binding skills.
If you're not interested, you can skip to the chapter's conclusion
and not miss a thing.
:marked
Let's do something more strikingly visual.
Let's hide the data entry area and display something else.
Start by wrapping the form in a `<div>` and bind
its `hidden` property to the `HeroFormComponent.submitted` property.
+makeExample('forms-deprecated/js/app/hero-form.component.html', 'edit-div', 'app/hero-form.component.html (excerpt)')(format=".")
:marked
The main form is visible from the start because the
the `submitted` property is false until we submit the form,
as this fragment from the `HeroFormComponent` reminds us:
+makeExample('forms-deprecated/js/app/hero-form.component.js', 'submitted')(format=".")
:marked
When we click the Submit button, the `submitted` flag becomes true and the form disappears
as planned.
Now we need to show something else while the form is in the submitted state.
Add the following block of HTML below the `<div>` wrapper we just wrote:
+makeExample('forms-deprecated/js/app/hero-form.component.html', 'submitted', 'app/hero-form.component.html (excerpt)')
:marked
There's our hero again, displayed read-only with interpolation bindings.
This slug of HTML only appears while the component is in the submitted state.
We added an Edit button whose click event is bound to an expression
that clears the `submitted` flag.
When we click it, this block disappears and the editable form reappears.
That's as much drama as we can muster for now.
.l-main-section
:marked
## Conclusion
The Angular 2 form discussed in this chapter takes advantage of the following framework features to provide support for data modification, validation and more:
- An Angular HTML form template.
- 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 local variables `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.
Our final project folder structure should look like this:
.filetree
.file angular2-forms
.children
.file app
.children
.file app.component.js
.file hero.js
.file hero-form.component.html
.file hero-form.component.js
.file main.ts
.file node_modules ...
.file typings ...
.file index.html
.file package.json
.file tsconfig.json
.file typings.json
:marked
Heres the final version of the source:
+makeTabs(
`forms-deprecated/js/app/hero-form.component.js,
forms-deprecated/js/app/hero-form.component.html,
forms-deprecated/js/app/hero.js,
forms-deprecated/js/app/app.component.js,
forms-deprecated/js/app/main.js,
forms-deprecated/js/index.html,
forms-deprecated/js/forms.css`,
'final, final,,,,,',
`hero-form.component.js,
hero-form.component.html,
hero.js,
app.component.js,
main.js,
index.html,
forms.css`)
:marked

View File

@ -1 +0,0 @@
!= partial("../../../_includes/_ts-temp")

View File

@ -36,14 +36,6 @@
"basics": true "basics": true
}, },
"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,
"basics": true,
"hide": true
},
"dependency-injection": { "dependency-injection": {
"title": "Dependency Injection", "title": "Dependency Injection",
"intro": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\".", "intro": "Angular's dependency injection system creates and delivers dependent services \"just-in-time\".",
@ -128,12 +120,6 @@
"intro": "Pipes transform displayed values within a template." "intro": "Pipes transform displayed values within a template."
}, },
"router-deprecated": {
"title": "Router (Deprecated Beta)",
"intro": "The deprecated Beta Router.",
"hide": true
},
"router": { "router": {
"title": "Routing & Navigation", "title": "Routing & Navigation",
"intro": "Discover the basics of screen navigation with the Angular 2 Component Router." "intro": "Discover the basics of screen navigation with the Angular 2 Component Router."

View File

@ -1,718 +0,0 @@
include ../_util-fns
.alert.is-important
:marked
This guide is using the deprecated forms API, which is disabled as of RC5, thus this sample only works up to RC4.
We have created a new version using the new API <a href='/docs/ts/latest/guide/forms.html'>here</a>.
:marked
Weve 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>Live Example</live-example>
.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 &mdash; 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 forms 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 parameters 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
Theres 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 weve 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 `<hero-form>` 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* `<input>` control has the HTML5 `required` attribute;
the *Alter Ego* `<input>` 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 <code>index.html</code> and add the following link to the <code>&lt;head></code>.
+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 `<options>` 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.
<a id="ngModel"></a>
.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 `<input>` 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, <span style="font-family:courier"><b>[()]</b></span>, 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, <span style="font-family:courier"><b>[]</b></span>.
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, <span style="font-family:courier"><b>()</b></span>.
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 <span style="font-family:courier"><b>[()]</b></span>
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" `<input>` binding:
+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".")
:marked
<br>The Property Binding should feel familiar. The Event Binding might seem strange.
The `ngModelChange` is not an `<input>` element event.
It is actually an event property of the `NgModel` directive.
When Angular sees a binding target in the form <span style="font-family:courier">[(x)]</span>,
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 &mdash; 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 `<form>` tag.
We'll talk about `NgForm` [later in the chapter](#ngForm).
The `ngControl` *attribute* in our template actually maps to the
[NgControlName](../api/common/index/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 <code>ng-touched</code>
td <code>ng-untouched</code>
tr
td Control's value has changed
td <code>ng-dirty</code>
td <code>ng-pristine</code>
tr
td Control's value is valid
td <code>ng-valid</code>
td <code>ng-invalid</code>
:marked
Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy**
to the "Name" `<input>` 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 `<head>` 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 `<input>` tag with
1. a [template reference variable](./template-syntax.html#ref-vars)
1. the "*is required*" message in a nearby `<div>` 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/index/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 &mdash; including `NgForm`, `NgModel`, `NgControlName` and `NgControlGroup` &mdash; *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 `<div>` 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 `<select>` if we want
but it's not imperative because the selection box already constrains the
power to valid value.
<a id="new-hero"></a>
<a id="reset"></a>
.l-main-section
:marked
## Add a hero and reset the form
We'd like to add a new hero in this form.
We place a "New Hero" button at the bottom of the form and bind its click event to a component method.
+makeExample('forms-deprecated/ts/app/hero-form.component.html',
'new-hero-button',
'app/hero-form.component.html (New Hero button)')
:marked
+makeExample('forms-deprecated/ts/app/hero-form.component.ts',
'new-hero-v1',
'app/hero-form.component.ts (New Hero method - v1)')(format=".")
:marked
Run the application again, click the *New Hero* button, and the form clears.
The *required* bars to the left of the input box are red, indicating invalid `name` and `power` properties.
That's understandable as these are required fields.
The error messages are hidden because the form is pristine; we haven't changed anything yet.
Enter a name and click *New Hero* again.
This time we see an error message! Why? We don't want that when we display a new (empty) hero.
Inspecting the element in the browser tools reveals that the *name* input box is no longer pristine.
Replacing the hero *did not restore the pristine state* of the control.
.l-sub-section
:marked
Upon reflection, we realize that Angular cannot distinguish between
replacing the entire hero and clearing the `name` property programmatically.
Angular makes no assumptions and leaves the control in its current, dirty state.
:marked
We'll have to reset the form controls manually with a small trick.
We add an `active` flag to the component, initialized to `true`. When we add a new hero,
we toggle `active` false and then immediately back to true with a quick `setTimeout`.
+makeExample('forms-deprecated/ts/app/hero-form.component.ts',
'new-hero',
'app/hero-form.component.ts (New Hero method - final)')(format=".")
:marked
Then we bind the form element to this `active` flag.
+makeExample('forms-deprecated/ts/app/hero-form.component.html',
'form-active',
'app/hero-form.component.html (Form tag)')
:marked
With `NgIf` bound to the `active` flag,
clicking "New Hero" removes the form from the DOM and recreates it in a blink of an eye.
The re-created form is in a pristine state. The error message is hidden.
.l-sub-section
:marked
This is a temporary workaround while we await a proper form reset feature.
:marked
.l-main-section
:marked
## Submit the form with **ngSubmit**
The user should be able to submit this form after filling it in.
The Submit button at the bottom of the form
does nothing on its own but it will
trigger a form submit because of its type (`type="submit"`).
A "form submit" is useless at the moment.
To make it useful, we'll update the `<form>` tag with another Angular directive, `NgSubmit`,
and bind it to the `HeroFormComponent.submit()` method with an event binding
+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'ngSubmit')(format=".")
:marked
We slipped in something extra there at the end! We defined a
template reference variable, **`#heroForm`**, and initialized it with the value, "ngForm".
The variable `heroForm` is now a reference to the `NgForm` directive that governs the form as a whole.
<a id="ngForm"></a>
.l-sub-section
:marked
### The NgForm directive
What `NgForm` directive? We didn't add an [NgForm](../api/common/index/NgForm-directive.html) directive!
Angular did. Angular creates and attaches an `NgForm` directive to the `<form>` tag automatically.
The `NgForm` directive supplements the `form` element with additional features.
It holds the controls we created for the elements with `ngControl` attributes
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
Later in the template we bind the button's `disabled` property to the form's over-all validity via
the `heroForm` variable. Here's that bit of markup:
+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'submit-button')
:marked
Re-run the application. The form opens in a valid state and the button is enabled.
Now delete the *Name*. We violate the "name required" rule which
is duly noted in our error message as before. And now the Submit button is also disabled.
Not impressed? Think about it for a moment. What would we have to do to
wire the button's enable/disabled state to the form's validity without Angular's help?
For us, it was as simple as
1. Define a template reference variable on the (enhanced) form element
2. Reference that variable in a button some 50 lines away.
.l-main-section
:marked
## Toggle two form regions (extra credit)
Submitting the form isn't terribly dramatic at the moment.
.l-sub-section
:marked
An unsurprising observation for a demo. To be honest,
jazzing it up won't teach us anything new about forms.
But this is an opportunity to exercise some of our newly won
binding skills.
If you're not interested, you can skip to the chapter's conclusion
and not miss a thing.
:marked
Let's do something more strikingly visual.
Let's hide the data entry area and display something else.
Start by wrapping the form in a `<div>` and bind
its `hidden` property to the `HeroFormComponent.submitted` property.
+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'edit-div', 'app/hero-form.component.html (excerpt)')(format=".")
:marked
The main form is visible from the start because the
the `submitted` property is false until we submit the form,
as this fragment from the `HeroFormComponent` reminds us:
+makeExample('forms-deprecated/ts/app/hero-form.component.ts', 'submitted')(format=".")
:marked
When we click the Submit button, the `submitted` flag becomes true and the form disappears
as planned.
Now we need to show something else while the form is in the submitted state.
Add the following block of HTML below the `<div>` wrapper we just wrote:
+makeExample('forms-deprecated/ts/app/hero-form.component.html', 'submitted', 'app/hero-form.component.html (excerpt)')
:marked
There's our hero again, displayed read-only with interpolation bindings.
This slug of HTML only appears while the component is in the submitted state.
We added an Edit button whose click event is bound to an expression
that clears the `submitted` flag.
When we click it, this block disappears and the editable form reappears.
That's as much drama as we can muster for now.
.l-main-section
:marked
## Conclusion
The Angular 2 form discussed in this chapter takes advantage of the following framework features to provide support for data modification, validation and more:
- An Angular HTML form template.
- 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 reference variables `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.
Our final project folder structure should look like this:
.filetree
.file angular2-forms
.children
.file app
.children
.file app.component.ts
.file hero.ts
.file hero-form.component.html
.file hero-form.component.ts
.file main.ts
.file node_modules ...
.file typings ...
.file index.html
.file package.json
.file tsconfig.json
.file typings.json
:marked
Heres the final version of the source:
+makeTabs(
`forms-deprecated/ts/app/hero-form.component.ts,
forms-deprecated/ts/app/hero-form.component.html,
forms-deprecated/ts/app/hero.ts,
forms-deprecated/ts/app/app.component.ts,
forms-deprecated/ts/app/main.ts,
forms-deprecated/ts/index.html,
forms-deprecated/ts/forms.css`,
'final, final,,,,,',
`hero-form.component.ts,
hero-form.component.html,
hero.ts,
app.component.ts,
main.ts,
index.html,
forms.css`)
:marked

File diff suppressed because it is too large Load Diff