docs: delete forms- and router-deprecated, jade + examples (#2328)
This commit is contained in:
parent
f2b1bbaba9
commit
1fb488b4ec
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -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 = {}));
|
|
|
@ -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>
|
|
|
@ -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 = {}));
|
|
|
@ -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 = {}));
|
|
|
@ -1,6 +0,0 @@
|
||||||
// #docregion
|
|
||||||
(function(app) {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
ng.platformBrowserDynamic.bootstrap(app.AppComponent);
|
|
||||||
});
|
|
||||||
})(window.app || (window.app = {}));
|
|
|
@ -1,9 +0,0 @@
|
||||||
/* #docregion */
|
|
||||||
.ng-valid[required] {
|
|
||||||
border-left: 5px solid #42A948; /* green */
|
|
||||||
}
|
|
||||||
|
|
||||||
.ng-invalid {
|
|
||||||
border-left: 5px solid #a94442; /* red */
|
|
||||||
}
|
|
||||||
/* #enddocregion */
|
|
|
@ -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>
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"description": "Forms",
|
|
||||||
"files":["app/**/*.js", "**/*.html", "**/*.css"]
|
|
||||||
}
|
|
|
@ -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 { }
|
|
|
@ -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>
|
|
|
@ -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
|
|
|
@ -1,11 +0,0 @@
|
||||||
// #docregion
|
|
||||||
export class Hero {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public id: number,
|
|
||||||
public name: string,
|
|
||||||
public power: string,
|
|
||||||
public alterEgo?: string
|
|
||||||
) { }
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
// #docregion
|
|
||||||
import { bootstrap } from '@angular/platform-browser-dynamic';
|
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
bootstrap(AppComponent);
|
|
|
@ -1,9 +0,0 @@
|
||||||
/* #docregion */
|
|
||||||
.ng-valid[required] {
|
|
||||||
border-left: 5px solid #42A948; /* green */
|
|
||||||
}
|
|
||||||
|
|
||||||
.ng-invalid {
|
|
||||||
border-left: 5px solid #a94442; /* red */
|
|
||||||
}
|
|
||||||
/* #enddocregion */
|
|
|
@ -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>
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"description": "Forms-Deprecated",
|
|
||||||
"files":[
|
|
||||||
"!**/*.d.ts",
|
|
||||||
"!**/*.js"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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 { }
|
|
|
@ -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 { }
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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 }] );
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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 { }
|
|
|
@ -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?')));
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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 { }
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
*/
|
|
|
@ -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
|
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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]);
|
|
|
@ -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]);
|
|
|
@ -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 -->
|
|
|
@ -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 -->
|
|
|
@ -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 -->
|
|
|
@ -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 -->
|
|
|
@ -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"]
|
|
||||||
}
|
|
|
@ -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."
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
This page has no Dart equivalent. Instead, see the [forms guide](forms.html).
|
|
|
@ -1,4 +0,0 @@
|
||||||
include ../_util-fns
|
|
||||||
|
|
||||||
:marked
|
|
||||||
This page has no Dart equivalent. Instead, see the [router guide](router.html).
|
|
|
@ -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."
|
||||||
|
|
|
@ -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
|
|
||||||
We’ve all used a form to login, submit a help request, place an order, book a flight,
|
|
||||||
schedule a meeting and perform countless other data entry tasks.
|
|
||||||
Forms are the mainstay of business applications.
|
|
||||||
|
|
||||||
Any seasoned web developer can slap together an HTML form with all the right tags.
|
|
||||||
It's more challenging to create a cohesive data entry experience that guides the
|
|
||||||
user efficiently and effectively through the workflow behind the form.
|
|
||||||
|
|
||||||
*That* takes design skills that are, to be frank, well out of scope for this chapter.
|
|
||||||
|
|
||||||
It also takes framework support for
|
|
||||||
**two-way data binding, change tracking, validation, and error handling**
|
|
||||||
... which we shall cover in this chapter on Angular forms.
|
|
||||||
|
|
||||||
We will build a simple form from scratch, one step at a time. Along the way we'll learn
|
|
||||||
|
|
||||||
- How to build an Angular form with a component and template
|
|
||||||
|
|
||||||
- The `ngModel` two-way data binding syntax for reading and writing values to input controls
|
|
||||||
|
|
||||||
- The `ngControl` directive to track the change state and validity of form controls
|
|
||||||
|
|
||||||
- The special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback
|
|
||||||
|
|
||||||
- How to display validation errors to users and enable/disable form controls
|
|
||||||
|
|
||||||
- How to share information across controls with template local variables
|
|
||||||
|
|
||||||
<live-example>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 — login forms, contact forms ... pretty much any business forms.
|
|
||||||
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
|
||||||
conditionally enable or disable specific controls, trigger built-in visual feedback, and much more.
|
|
||||||
|
|
||||||
It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd
|
|
||||||
otherwise wrestle with ourselves.
|
|
||||||
|
|
||||||
We'll discuss and learn to build the following template-driven form:
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
|
||||||
heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis!
|
|
||||||
|
|
||||||
Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot.
|
|
||||||
|
|
||||||
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
p We'll' customize the colors and location of the "required" bar with standard CSS.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We will build this form in the following sequence of small steps
|
|
||||||
|
|
||||||
1. Create the `Hero` model class
|
|
||||||
1. Create the component that controls the form
|
|
||||||
1. Create a template with the initial form layout
|
|
||||||
1. Add the **ngModel** directive to each form input control
|
|
||||||
1. Add the **ngControl** directive to each form input control
|
|
||||||
1. Add custom CSS to provide visual feedback
|
|
||||||
1. Show and hide validation error messages
|
|
||||||
1. Handle form submission with **ngSubmit**
|
|
||||||
1. Disable the form’s submit button until the form is valid
|
|
||||||
|
|
||||||
:marked
|
|
||||||
## Setup
|
|
||||||
Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html).
|
|
||||||
|
|
||||||
## Create the Hero Model Class
|
|
||||||
|
|
||||||
As users enter form data, we capture their changes and update an instance of a model.
|
|
||||||
We can't layout the form until we know what the model looks like.
|
|
||||||
|
|
||||||
A model can be as simple as a "property bag" that holds facts about a thing of application importance.
|
|
||||||
That describes well our `Hero` class with its three required fields (`id`, `name`, `power`)
|
|
||||||
and one optional field (`alterEgo`).
|
|
||||||
|
|
||||||
Create a new file in the app folder called `hero.js` and give it the following constructor:
|
|
||||||
|
|
||||||
+makeExample('forms-deprecated/js/app/hero.js', null, 'app/hero.js')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
|
||||||
|
|
||||||
The `alterEgo` is optional and the constructor lets us omit it by being the last argument.
|
|
||||||
|
|
||||||
We can create a new hero like this:
|
|
||||||
code-example(format="").
|
|
||||||
var myHero = new Hero(42, 'SkyDog',
|
|
||||||
'Fetch any object at any distance', 'Leslie Rollover');
|
|
||||||
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
|
|
||||||
:marked
|
|
||||||
We update the `<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
|
|
||||||
There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before.
|
|
||||||
|
|
||||||
Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters
|
|
||||||
|
|
||||||
1. We use the `ng.core` object from the Angular library as we usually do.
|
|
||||||
|
|
||||||
1. The `Component()` selector value of "hero-form" means we can drop this form in a parent template with a `<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><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 — 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="form")|(.*div.*$)|(Name is required)/gm };
|
|
||||||
+makeExample('forms-deprecated/js/app/hero-form.component.html',
|
|
||||||
'name-with-error-msg',
|
|
||||||
'app/hero-form.component.html (excerpt)',
|
|
||||||
stylePattern)
|
|
||||||
:marked
|
|
||||||
When we added the `ngControl` directive, we bound it to the model's `name` property.
|
|
||||||
Here we initialize a template local variable (`name`) with the value "ngForm" (`#name="ngForm"`).
|
|
||||||
Angular recognizes that syntax and re-sets the `name` local template variable to the
|
|
||||||
`ngControl` directive instance.
|
|
||||||
In other words, the `name` local template variable becomes a handle on the `ngControl` object
|
|
||||||
for this input box.
|
|
||||||
|
|
||||||
Now we can control visibility of the "name" error message by binding the message `<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 variable’s `valid` property on input controls to check if a control is valid and show/hide error messages.
|
|
||||||
- Controlling the submit button's enabled state by binding to `NgForm` validity.
|
|
||||||
- Custom CSS classes that provide visual feedback to users about invalid controls.
|
|
||||||
|
|
||||||
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
|
|
||||||
Here’s 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
|
|
|
@ -1 +0,0 @@
|
||||||
!= partial("../../../_includes/_ts-temp")
|
|
|
@ -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."
|
||||||
|
|
|
@ -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
|
|
||||||
We’ve all used a form to login, submit a help request, place an order, book a flight,
|
|
||||||
schedule a meeting and perform countless other data entry tasks.
|
|
||||||
Forms are the mainstay of business applications.
|
|
||||||
|
|
||||||
Any seasoned web developer can slap together an HTML form with all the right tags.
|
|
||||||
It's more challenging to create a cohesive data entry experience that guides the
|
|
||||||
user efficiently and effectively through the workflow behind the form.
|
|
||||||
|
|
||||||
*That* takes design skills that are, to be frank, well out of scope for this chapter.
|
|
||||||
|
|
||||||
It also takes framework support for
|
|
||||||
**two-way data binding, change tracking, validation, and error handling**
|
|
||||||
... which we shall cover in this chapter on Angular forms.
|
|
||||||
|
|
||||||
We will build a simple form from scratch, one step at a time. Along the way we'll learn
|
|
||||||
|
|
||||||
- to build an Angular form with a component and template
|
|
||||||
|
|
||||||
- two-way data binding with `[(ngModel)]` syntax for reading and writing values to input controls
|
|
||||||
|
|
||||||
- using `ngControl` to track the change state and validity of form controls
|
|
||||||
|
|
||||||
- the special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback
|
|
||||||
|
|
||||||
- displaying validation errors to users and enable/disable form controls
|
|
||||||
|
|
||||||
- sharing information among controls with template reference variables
|
|
||||||
|
|
||||||
<live-example>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 — login forms, contact forms ... pretty much any business forms.
|
|
||||||
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
|
||||||
conditionally enable or disable specific controls, trigger built-in visual feedback, and much more.
|
|
||||||
|
|
||||||
It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd
|
|
||||||
otherwise wrestle with ourselves.
|
|
||||||
|
|
||||||
We'll discuss and learn to build the following template-driven form:
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
|
||||||
heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis!
|
|
||||||
|
|
||||||
Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot.
|
|
||||||
|
|
||||||
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
|
||||||
|
|
||||||
figure.image-display
|
|
||||||
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red.
|
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
p We'll customize the colors and location of the "required" bar with standard CSS.
|
|
||||||
|
|
||||||
:marked
|
|
||||||
We will build this form in the following sequence of small steps
|
|
||||||
|
|
||||||
1. Create the `Hero` model class
|
|
||||||
1. Create the component that controls the form
|
|
||||||
1. Create a template with the initial form layout
|
|
||||||
1. Bind data properties to each form input control with the `ngModel` two-way data binding syntax
|
|
||||||
1. Add the **ngControl** directive to each form input control
|
|
||||||
1. Add custom CSS to provide visual feedback
|
|
||||||
1. Show and hide validation error messages
|
|
||||||
1. Handle form submission with **ngSubmit**
|
|
||||||
1. Disable the form’s submit button until the form is valid
|
|
||||||
|
|
||||||
:marked
|
|
||||||
## Setup
|
|
||||||
Create a new project folder (`angular2-forms`) and follow the steps in the [QuickStart](../quickstart.html).
|
|
||||||
|
|
||||||
include ../_quickstart_repo
|
|
||||||
:marked
|
|
||||||
## Create the Hero Model Class
|
|
||||||
|
|
||||||
As users enter form data, we capture their changes and update an instance of a model.
|
|
||||||
We can't layout the form until we know what the model looks like.
|
|
||||||
|
|
||||||
A model can be as simple as a "property bag" that holds facts about a thing of application importance.
|
|
||||||
That describes well our `Hero` class with its three required fields (`id`, `name`, `power`)
|
|
||||||
and one optional field (`alterEgo`).
|
|
||||||
|
|
||||||
Create a new file in the app folder called `hero.ts` and give it the following class definition:
|
|
||||||
|
|
||||||
+makeExample('forms-deprecated/ts/app/hero.ts', null, 'app/hero.ts')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
|
||||||
|
|
||||||
The TypeScript compiler generates a public field for each `public` constructor parameter and
|
|
||||||
assigns the parameter’s value to that field automatically when we create new heroes.
|
|
||||||
|
|
||||||
The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`.
|
|
||||||
|
|
||||||
We can create a new hero like this:
|
|
||||||
code-example(format="").
|
|
||||||
let myHero = new Hero(42, 'SkyDog',
|
|
||||||
'Fetch any object at any distance', 'Leslie Rollover');
|
|
||||||
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
|
|
||||||
:marked
|
|
||||||
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Create a Form component
|
|
||||||
|
|
||||||
An Angular form has two parts: an HTML-based template and a code-based Component to handle data and user interactions.
|
|
||||||
|
|
||||||
We begin with the Component because it states, in brief, what the Hero editor can do.
|
|
||||||
|
|
||||||
Create a new file called `hero-form.component.ts` and give it the following definition:
|
|
||||||
|
|
||||||
+makeExample('forms-deprecated/ts/app/hero-form.component.ts', 'first', 'app/hero-form.component.ts')
|
|
||||||
|
|
||||||
:marked
|
|
||||||
There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before.
|
|
||||||
|
|
||||||
Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters
|
|
||||||
|
|
||||||
1. We import the `Component` decorator from the Angular library as we usually do.
|
|
||||||
|
|
||||||
1. The `@Component` selector value of "hero-form" means we can drop this form in a parent template with a `<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><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 — 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 — including `NgForm`, `NgModel`, `NgControlName` and `NgControlGroup` — *exportAs* "ngForm"
|
|
||||||
and we only ever apply *one* of these directives to an element tag.
|
|
||||||
Consistency rules!
|
|
||||||
|
|
||||||
Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `<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 variable’s `valid` property on input controls to check if a control is valid and show/hide error messages.
|
|
||||||
- Controlling the submit button's enabled state by binding to `NgForm` validity.
|
|
||||||
- Custom CSS classes that provide visual feedback to users about invalid controls.
|
|
||||||
|
|
||||||
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
|
|
||||||
Here’s 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
Loading…
Reference in New Issue