parent
a6ca213866
commit
92574937f3
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// #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 = {}));
|
|
@ -0,0 +1,6 @@
|
||||||
|
// #docregion
|
||||||
|
(function(app) {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
ng.platform.browser.bootstrap(app.AppComponent);
|
||||||
|
});
|
||||||
|
})(window.app || (window.app = {}));
|
|
@ -0,0 +1,195 @@
|
||||||
|
<!-- #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="#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="#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="#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>
|
|
@ -0,0 +1,52 @@
|
||||||
|
// #docplaster
|
||||||
|
// #docregion
|
||||||
|
// #docregion first, final
|
||||||
|
(function(app) {
|
||||||
|
app.HeroFormComponent = ng.core
|
||||||
|
.Component({
|
||||||
|
selector: 'hero-form',
|
||||||
|
templateUrl: 'app/hero-form.component.html'
|
||||||
|
})
|
||||||
|
.Class({
|
||||||
|
// #docregion submitted
|
||||||
|
constructor: function() {
|
||||||
|
// #enddocregion submitted
|
||||||
|
this.powers = ['Really Smart', 'Super Flexible',
|
||||||
|
'Super Hot', 'Weather Changer'
|
||||||
|
];
|
||||||
|
|
||||||
|
this.model = new app.Hero(18, 'Dr IQ', this.powers[0],
|
||||||
|
'Chuck Overstreet');
|
||||||
|
|
||||||
|
// #docregion submitted
|
||||||
|
this.submitted = false;
|
||||||
|
},
|
||||||
|
onSubmit: function() {
|
||||||
|
this.submitted = true;
|
||||||
|
},
|
||||||
|
// #enddocregion submitted
|
||||||
|
|
||||||
|
// #enddocregion final
|
||||||
|
// TODO: Remove this when we're done
|
||||||
|
diagnostic: function() {
|
||||||
|
return JSON.stringify(this.model);
|
||||||
|
},
|
||||||
|
// #enddocregion first
|
||||||
|
|
||||||
|
|
||||||
|
//////// DO NOT SHOW IN DOCS ////////
|
||||||
|
|
||||||
|
// Reveal in html:
|
||||||
|
// AlterEgo via form.controls = {{showFormControls(hf)}}
|
||||||
|
showFormControls: function(form) {
|
||||||
|
return form.controls['alterEgo'] &&
|
||||||
|
// #docregion form-controls
|
||||||
|
form.controls['name'].value; // Dr. IQ
|
||||||
|
// #enddocregion form-controls
|
||||||
|
},
|
||||||
|
/////////////////////////////
|
||||||
|
|
||||||
|
// #docregion first, final
|
||||||
|
});
|
||||||
|
// #enddocregion first, final
|
||||||
|
})(window.app || (window.app = {}));
|
|
@ -0,0 +1,11 @@
|
||||||
|
// #docregion
|
||||||
|
(function(app) {
|
||||||
|
app.Hero = Hero;
|
||||||
|
|
||||||
|
function Hero(id, name, power, alterEgo) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.power = power;
|
||||||
|
this.alterEgo = alterEgo;
|
||||||
|
}
|
||||||
|
})(window.app || (window.app = {}));
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- #docplaster -->
|
||||||
|
<!-- #docregion -->
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Hero Form</title>
|
||||||
|
<!-- #docregion bootstrap -->
|
||||||
|
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
|
||||||
|
<!-- #enddocregion bootstrap -->
|
||||||
|
<!-- #docregion styles -->
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<!-- #enddocregion styles -->
|
||||||
|
|
||||||
|
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
|
||||||
|
<script src="node_modules/angular2/bundles/angular2.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.js'></script>
|
||||||
|
<script src='app/boot.js'></script>
|
||||||
|
<!-- #enddocregion scripts, scripts-hero, scripts-hero-form -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<my-app>Loading...</my-app>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "angular2-examples-master",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Master package.json, the superset of all dependencies for all of the _example package.json files.",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"tsc": "tsc",
|
||||||
|
"tsc:w": "tsc -w",
|
||||||
|
"lite": "lite-server",
|
||||||
|
"live": "live-server",
|
||||||
|
"start": "npm run lite",
|
||||||
|
"both": "concurrent \"npm run tsc:w\" \"npm run start\" ",
|
||||||
|
"test": "karma start karma.conf.js",
|
||||||
|
"build-and-test": "npm run tsc && npm run test"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"angular2": "2.0.0-alpha.52",
|
||||||
|
"systemjs": "0.19.6",
|
||||||
|
"es6-promise": "^3.0.2",
|
||||||
|
"es6-shim": "^0.33.3",
|
||||||
|
"reflect-metadata": "0.1.2",
|
||||||
|
"rxjs": "5.0.0-alpha.14",
|
||||||
|
"zone.js": "0.5.8",
|
||||||
|
"bootstrap": "^3.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^1.0.0",
|
||||||
|
"lite-server": "^1.3.1",
|
||||||
|
"live-server": "^0.8.2",
|
||||||
|
"typescript": "^1.7.3",
|
||||||
|
"jasmine-core":"~2.1.0",
|
||||||
|
"karma": "^0.12.23",
|
||||||
|
"karma-chrome-launcher": "^0.1.4",
|
||||||
|
"karma-cli": "^0.0.4",
|
||||||
|
"karma-jasmine": "^0.3.6",
|
||||||
|
"rimraf": "^2.4.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"description": "Forms",
|
||||||
|
"files": ["**/*.js"]
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
/* #docregion */
|
||||||
|
.ng-valid[required] {
|
||||||
|
border-left: 5px solid #42A948; /* green */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ng-invalid {
|
||||||
|
border-left: 5px solid #a94442; /* red */
|
||||||
|
}
|
||||||
|
/* #enddocregion */
|
|
@ -53,7 +53,7 @@ include ../../../../_includes/_util-fns
|
||||||
We'll discuss and learn to build a template-driven form that looks like this:
|
We'll discuss and learn to build a template-driven form that looks like this:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/hf-1.png" width="400px" alt="Clean Form")
|
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
||||||
|
@ -64,7 +64,7 @@ figure.image-display
|
||||||
If we delete the hero name, the form displays a validation error in an attention-grabbing style:
|
If we delete the hero name, the form displays a validation error in an attention-grabbing style:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/hf-2.png" width="400px" alt="Invalid, Name Required")
|
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Note that the submit button is disabled, and the "required" bar to the left of the input control changed from green to red.
|
Note that the submit button is disabled, and the "required" bar to the left of the input control changed from green to red.
|
||||||
|
@ -259,7 +259,7 @@ figure.image-display
|
||||||
Running the app right now would be disappointing.
|
Running the app right now would be disappointing.
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/hf-3.png" width="400px" alt="Early form with no binding")
|
img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding")
|
||||||
:marked
|
:marked
|
||||||
We don't see hero data because we are not binding to the `Hero` yet.
|
We don't see hero data because we are not binding to the `Hero` yet.
|
||||||
We know how to do that from earlier chapters.
|
We know how to do that from earlier chapters.
|
||||||
|
|
|
@ -17,5 +17,10 @@
|
||||||
"user-input": {
|
"user-input": {
|
||||||
"title": "User Input",
|
"title": "User Input",
|
||||||
"intro": "DOM events drive user input in Angular. You can use the native events like click, mouseover, and keyup. Angular uses a special syntax to register events to DOM elements. This section covers all the ins and outs of using the event syntax."
|
"intro": "DOM events drive user input in Angular. You can use the native events like click, mouseover, and keyup. Angular uses a special syntax to register events to DOM elements. This section covers all the ins and outs of using the event syntax."
|
||||||
|
},
|
||||||
|
|
||||||
|
"forms": {
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,640 @@
|
||||||
|
include ../../../../_includes/_util-fns
|
||||||
|
|
||||||
|
:marked
|
||||||
|
We’ve all used a form to login, submit a help request, place an order, book a flight,
|
||||||
|
schedule a meeting and perform countless other data entry tasks.
|
||||||
|
Forms are the mainstay of business applications.
|
||||||
|
|
||||||
|
Any seasoned web developer can slap together an HTML form with all the right tags.
|
||||||
|
It's more challenging to create a cohesive data entry experience that guides the
|
||||||
|
user efficiently and effectively through the workflow behind the form.
|
||||||
|
|
||||||
|
*That* takes design skills that are, to be frank, well out of scope for this chapter.
|
||||||
|
|
||||||
|
It also takes framework support for
|
||||||
|
**two-way data binding, change tracking, validation, and error handling**
|
||||||
|
... which we shall cover in this chapter on Angular forms.
|
||||||
|
|
||||||
|
We will build a simple form from scratch, one step at a time. Along the way we'll learn
|
||||||
|
|
||||||
|
- How to build an Angular form with a component and template
|
||||||
|
|
||||||
|
- The `ngModel` two-way data binding syntax for reading and writing values to input controls
|
||||||
|
|
||||||
|
- The `ngControl` directive to track the change state and validity of form controls
|
||||||
|
|
||||||
|
- The special CSS classes that `ngControl` adds to form controls and how we can use them to provide strong visual feedback
|
||||||
|
|
||||||
|
- How to display validation errors to users and enable/disable form controls
|
||||||
|
|
||||||
|
- How to share information across controls with template local variables
|
||||||
|
|
||||||
|
[Live Example](/resources/live-examples/forms/js/plnkr.html)
|
||||||
|
.l-main-section
|
||||||
|
:marked
|
||||||
|
## Template-Driven Forms
|
||||||
|
|
||||||
|
Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with
|
||||||
|
the form-specific directives and techniques described in this chapter.
|
||||||
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
That's not the only way to create a form but it's the way we'll cover in this chapter.
|
||||||
|
:marked
|
||||||
|
We can build almost any form we need with an Angular template $mdash; login forms, contact forms ... pretty much any business forms.
|
||||||
|
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
||||||
|
conditionally enable or disable specific controls, trigger built-in visual feedback, and much more.
|
||||||
|
|
||||||
|
It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd
|
||||||
|
otherwise wrestle with ourselves.
|
||||||
|
|
||||||
|
We'll discuss and learn to build the following template-driven form:
|
||||||
|
|
||||||
|
figure.image-display
|
||||||
|
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
||||||
|
heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis!
|
||||||
|
|
||||||
|
Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot.
|
||||||
|
|
||||||
|
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
||||||
|
|
||||||
|
figure.image-display
|
||||||
|
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required")
|
||||||
|
|
||||||
|
:marked
|
||||||
|
Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red.
|
||||||
|
|
||||||
|
.l-sub-section
|
||||||
|
p We'll' customize the colors and location of the "required" bar with standard CSS.
|
||||||
|
|
||||||
|
:marked
|
||||||
|
We will build this form in the following sequence of small steps
|
||||||
|
|
||||||
|
1. Create the `Hero` model class
|
||||||
|
1. Create the component that controls the form
|
||||||
|
1. Create a template with the initial form layout
|
||||||
|
1. Add the **ngModel** directive to each form input control
|
||||||
|
1. Add the **ngControl** directive to each form input control
|
||||||
|
1. Add custom CSS to provide visual feedback
|
||||||
|
1. Show and hide validation error messages
|
||||||
|
1. Handle form submission with **ngSubmit**
|
||||||
|
1. Disable the 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/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/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/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/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/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/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 Boostrap](http://getbootstrap.com/css/). Purely cosmetic.
|
||||||
|
We're using Bootstrap to gussy up our form.
|
||||||
|
Hey, what's a form without a little style!
|
||||||
|
|
||||||
|
.callout.is-important
|
||||||
|
header Angular Forms Do Not Require A Style Library
|
||||||
|
:marked
|
||||||
|
Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or
|
||||||
|
the styles of any external library. Angular apps can use any CSS library
|
||||||
|
... or none at all.
|
||||||
|
|
||||||
|
:marked
|
||||||
|
Let's add the stylesheet.
|
||||||
|
|
||||||
|
ol
|
||||||
|
li Open a terminal window in the application root folder and enter the command:
|
||||||
|
code-example(language="html" escape="html").
|
||||||
|
npm install bootstrap --save
|
||||||
|
li Open <code>index.html</code> and add the following link to the <code><head></code>.
|
||||||
|
+makeExample('forms/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/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/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/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/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">[(abc)]</span>,
|
||||||
|
it expects the `abc` directive to have an `abc` input property and an `abc-change` 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/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/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 `styles.css` file
|
||||||
|
that we add to our project as a sibling to `index.html`.
|
||||||
|
|
||||||
|
+makeExample('forms/js/styles.css',null,'styles.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/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/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 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/core/NgForm-class.html) directive** explicitly.
|
||||||
|
|
||||||
|
Angular added it surreptiously, 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/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/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 duely 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/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/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/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:
|
||||||
|
code-example(format="").
|
||||||
|
angular2-forms
|
||||||
|
├── node_modules
|
||||||
|
├── app
|
||||||
|
| ├── app.component.js
|
||||||
|
| ├── boot.js
|
||||||
|
| ├── hero.js
|
||||||
|
| ├── hero-form.component.html
|
||||||
|
| └── hero-form.component.js
|
||||||
|
├── index.html
|
||||||
|
├── styles.css
|
||||||
|
├── tsconfig.json
|
||||||
|
└── package.json
|
||||||
|
:marked
|
||||||
|
Here’s the final version of the source:
|
||||||
|
|
||||||
|
+makeTabs(
|
||||||
|
`forms/js/app/hero-form.component.js,
|
||||||
|
forms/js/app/hero-form.component.html,
|
||||||
|
forms/js/app/hero.js,
|
||||||
|
forms/js/app/app.component.js,
|
||||||
|
forms/js/app/boot.js,
|
||||||
|
forms/js/index.html,
|
||||||
|
forms/js/styles.css`,
|
||||||
|
'final, final,,,,,',
|
||||||
|
`hero-form.component.js,
|
||||||
|
hero-form.component.html,
|
||||||
|
hero.js,
|
||||||
|
app.component.js,
|
||||||
|
boot.js,
|
||||||
|
index.html,
|
||||||
|
styles.css`)
|
||||||
|
:marked
|
|
@ -44,13 +44,13 @@ include ../../../../_includes/_util-fns
|
||||||
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
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.
|
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 repeative, boiler plate tasks we'd
|
It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd
|
||||||
otherwise wrestle with ourselves.
|
otherwise wrestle with ourselves.
|
||||||
|
|
||||||
We'll discuss and learn to build the following template-driven form:
|
We'll discuss and learn to build the following template-driven form:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/heroForm-1.png" width="400px" alt="Clean Form")
|
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
||||||
|
@ -61,7 +61,7 @@ figure.image-display
|
||||||
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/heroForm-2.png" width="400px" alt="Invalid, Name Required")
|
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red.
|
Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red.
|
||||||
|
@ -243,7 +243,7 @@ ol
|
||||||
Running the app right now would be disappointing.
|
Running the app right now would be disappointing.
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/heroForm-3.png" width="400px" alt="Early form with no binding")
|
img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding")
|
||||||
:marked
|
:marked
|
||||||
We don't see hero data because we are not binding to the `Hero` yet.
|
We don't see hero data because we are not binding to the `Hero` yet.
|
||||||
We know how to do that from earlier chapters.
|
We know how to do that from earlier chapters.
|
||||||
|
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue