parent
ad5fc1ffc1
commit
f82b730cbd
|
@ -17,5 +17,7 @@ pubspec.lock
|
|||
public/docs/xref-*.*
|
||||
_zip-output
|
||||
www
|
||||
*plnkr.html
|
||||
*plnkr.no-link.html
|
||||
/npm-debug.log
|
||||
*.plnkr.html
|
||||
plnkr.html
|
||||
*plnkr.no-link.html
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// #docregion all
|
||||
library hero_form.hero;
|
||||
|
||||
class Hero {
|
||||
|
@ -6,8 +7,16 @@ class Hero {
|
|||
String power;
|
||||
String alterEgo;
|
||||
|
||||
Hero(this.number, this.name, this.power, {this.alterEgo});
|
||||
Hero(this.number, this.name, this.power, [this.alterEgo]);
|
||||
|
||||
Map toJson() =>
|
||||
{'number': number, 'name': name, 'power': power, 'alterEgo': alterEgo};
|
||||
String toString() => '$number: $name ($alterEgo). Super power: $power';
|
||||
}
|
||||
// #enddocregion all
|
||||
|
||||
main() {
|
||||
// #docregion newhero
|
||||
var myHero = new Hero(
|
||||
42, 'SkyDog', 'Fetch any object at any distance', 'Leslie Rollover');
|
||||
print('My hero is ${myHero.name}.'); // "My hero is SkyDog."
|
||||
// #enddocregion newhero
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
// #docregion no-todo
|
||||
library hero_form.hero_form_component;
|
||||
|
||||
import 'package:angular2/angular2.dart';
|
||||
|
@ -10,18 +13,25 @@ const List<String> _powers = const [
|
|||
'Weather Changer'
|
||||
];
|
||||
|
||||
@Component(selector: 'hero-form')
|
||||
@View(
|
||||
@Component(
|
||||
selector: 'hero-form',
|
||||
templateUrl: 'hero_form_component.html',
|
||||
// Soon specifying directives here will be unnecessary.
|
||||
directives: const [CORE_DIRECTIVES, FORM_DIRECTIVES])
|
||||
class HeroFormComponent {
|
||||
List<String> get powers => _powers;
|
||||
|
||||
// #docregion submitted
|
||||
bool submitted = false;
|
||||
|
||||
Hero model = new Hero(18, 'Dr IQ', _powers[0], alterEgo: 'Chuck Overstreet');
|
||||
// #enddocregion submitted
|
||||
Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet');
|
||||
// #docregion submitted
|
||||
// #enddocregion no-todo
|
||||
// TODO: Remove this when we're done
|
||||
String get diagnostic => 'DIAGNOSTIC: $model';
|
||||
// #docregion no-todo
|
||||
|
||||
onSubmit() {
|
||||
submitted = true;
|
||||
}
|
||||
// #enddocregion submitted
|
||||
}
|
||||
|
|
|
@ -1,42 +1,67 @@
|
|||
<div class="container" [hidden]="submitted">
|
||||
{{model.toJson()}}
|
||||
<!-- #docplaster ... all of the form ... -->
|
||||
<!-- #docregion -->
|
||||
<div class="container">
|
||||
<!-- #docregion edit-div -->
|
||||
<div [hidden]="submitted">
|
||||
<h1>Hero Form</h1>
|
||||
<!-- #docregion ng-submit -->
|
||||
<form (ng-submit)="onSubmit()" #hf="form">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" class="form-control" required [(ng-model)]="model.name" ng-control="name" #name="form" >
|
||||
<div [hidden]="name.valid" class="alert alert-danger">
|
||||
Name is required
|
||||
</div>
|
||||
<!-- #enddocregion ng-submit -->
|
||||
<!-- #enddocregion edit-div -->
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion name-with-error-msg -->
|
||||
<input type="text" class="form-control" required
|
||||
[(ng-model)]="model.name"
|
||||
ng-control="name" #name="form" >
|
||||
<div [hidden]="name.valid" class="alert alert-danger">
|
||||
Name is required
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input type="text" class="form-control" [(ng-model)]="model.alterEgo" ng-control="alterEgo" >
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
<select class="form-control" required [(ng-model)]="model.power" ng-control="power" >
|
||||
<option *ng-for="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default" [disabled]="!hf.form.valid">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- #enddocregion name-with-error-msg -->
|
||||
</div>
|
||||
|
||||
<div [hidden]="!submitted">
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input type="text" class="form-control"
|
||||
[(ng-model)]="model.alterEgo"
|
||||
ng-control="alterEgo" >
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
<select class="form-control" required
|
||||
[(ng-model)]="model.power"
|
||||
ng-control="power" >
|
||||
<option *ng-for="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- #docregion submit-button -->
|
||||
<button type="submit" class="btn btn-default"
|
||||
[disabled]="!hf.form.valid">Submit</button>
|
||||
<!-- #enddocregion submit-button -->
|
||||
<!-- #docregion edit-div -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- #enddocregion edit-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 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 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 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>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// #docregion
|
||||
library hero_form.hero_form_component;
|
||||
|
||||
import 'package:angular2/angular2.dart';
|
||||
|
||||
@Component(selector: 'hero-form', template: 'Hero form will go here')
|
||||
class HeroFormComponent {}
|
|
@ -0,0 +1,15 @@
|
|||
<!-- #docregion -->
|
||||
<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>
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,32 @@
|
|||
<!-- #docregion -->
|
||||
<div class="container">
|
||||
<h1>Hero Form</h1>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion ng-control-1 -->
|
||||
<input type="text" class="form-control" required
|
||||
[(ng-model)]="model.name"
|
||||
ng-control="name" >
|
||||
<!-- #enddocregion ng-control-1 -->
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input type="text" class="form-control"
|
||||
[(ng-model)]="model.alterEgo"
|
||||
ng-control="alterEgo" >
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
<select class="form-control" required
|
||||
[(ng-model)]="model.power"
|
||||
ng-control="power" >
|
||||
<option *ng-for="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<!-- #docregion -->
|
||||
<div class="container">
|
||||
<h1>Hero Form</h1>
|
||||
<form>
|
||||
<!-- #docregion ng-model-2 -->
|
||||
{{diagnostic}}
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" class="form-control" required
|
||||
[(ng-model)]="model.name" >
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input type="text" class="form-control"
|
||||
[(ng-model)]="model.alterEgo">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
<select class="form-control" required
|
||||
[(ng-model)]="model.power">
|
||||
<option *ng-for="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- #enddocregion ng-model-2 -->
|
||||
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<!-- #docregion -->
|
||||
<div class="container">
|
||||
<h1>Hero Form</h1>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion ng-model-1 -->
|
||||
<input type="text" class="form-control" required
|
||||
[(ng-model)]="model.name" >
|
||||
TODO: remove this: {{model.name}}
|
||||
<!-- #enddocregion ng-model-1 -->
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<!-- #docregion powers -->
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
<select class="form-control" required>
|
||||
<option *ng-for="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- #enddocregion powers -->
|
||||
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
<!-- #docregion -->
|
||||
<div class="container">
|
||||
<h1>Hero Form</h1>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion ng-model-3 -->
|
||||
<input type="text" class="form-control" required
|
||||
[ng-model]="model.name"
|
||||
(ng-model-change)="model.name = $event" >
|
||||
TODO: remove this: {{model.name}}
|
||||
<!-- #enddocregion ng-model-3 -->
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input type="text" class="form-control"
|
||||
[(ng-model)]="model.alterEgo">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
<select class="form-control" required
|
||||
[(ng-model)]="model.power">
|
||||
<option *ng-for="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,33 @@
|
|||
<!-- #docregion -->
|
||||
<div class="container">
|
||||
<h1>Hero Form</h1>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion ng-control-2 -->
|
||||
<input type="text" class="form-control" required
|
||||
[(ng-model)]="model.name"
|
||||
ng-control="name" #spy >
|
||||
TODO: remove this: {{spy.className}}
|
||||
<!-- #enddocregion ng-control-2 -->
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input type="text" class="form-control"
|
||||
[(ng-model)]="model.alterEgo"
|
||||
ng-control="alterEgo" >
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
<select class="form-control" required
|
||||
[(ng-model)]="model.power"
|
||||
ng-control="power" >
|
||||
<option *ng-for="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
</form>
|
||||
</div>
|
|
@ -1,9 +1,11 @@
|
|||
# #docregion
|
||||
name: hero_form
|
||||
description: Form example
|
||||
version: 0.0.1
|
||||
dependencies:
|
||||
angular2: 2.0.0-alpha.45
|
||||
angular2: 2.0.0-alpha.47
|
||||
browser: ^0.10.0
|
||||
transformers:
|
||||
- angular2:
|
||||
entry_points: web/main.dart
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,13 +1,24 @@
|
|||
<!-- #docplaster -->
|
||||
<!-- #docregion -->
|
||||
<!-- #docregion initial -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Displaying Data</title>
|
||||
<head>
|
||||
<title>Hero Form</title>
|
||||
<!-- #enddocregion initial -->
|
||||
<!-- #docregion styles -->
|
||||
<!-- #docregion bootstrap-and-script -->
|
||||
<link rel="stylesheet" href="bootstrap.min.css">
|
||||
<!-- #enddocregion bootstrap-and-script -->
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script async src="main.dart" type="application/dart"></script>
|
||||
<script async src="packages/browser/dart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<hero-form></hero-form>
|
||||
</body>
|
||||
<!-- #enddocregion styles -->
|
||||
<!-- #docregion initial -->
|
||||
<!-- #docregion bootstrap-and-script -->
|
||||
<script defer src="main.dart" type="application/dart"></script>
|
||||
<script defer src="packages/browser/dart.js"></script>
|
||||
<!-- #enddocregion bootstrap-and-script -->
|
||||
</head>
|
||||
<body>
|
||||
<hero-form>Loading...</hero-form>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// #docregion
|
||||
import 'package:angular2/bootstrap.dart';
|
||||
import 'package:hero_form/hero_form_component.dart';
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* #docregion */
|
||||
.ng-valid[required] {
|
||||
border-left: 5px solid #42A948; /* green */
|
||||
}
|
||||
.ng-invalid {
|
||||
.ng-invalid, .ng-invalid:focus {
|
||||
border-left: 5px solid #a94442; /* red */
|
||||
}
|
||||
|
|
|
@ -17,5 +17,10 @@
|
|||
"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."
|
||||
},
|
||||
|
||||
"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,659 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
<!-- http://plnkr.co/edit/wg154K -->
|
||||
:marked
|
||||
We’ve all used a form to log in, 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 `ng-model` two-way data binding syntax for reading and writing values to input controls
|
||||
|
||||
- The `ng-control` directive to track the change state and validity of form controls
|
||||
|
||||
- The special CSS classes that `ng-control` adds to form controls and how to 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
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Template-driven forms
|
||||
|
||||
Many of us will build forms by writing templates in the Angular
|
||||
template syntax
|
||||
<!-- link to ./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 form.
|
||||
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, boilerplate tasks we'd
|
||||
otherwise wrestle with ourselves.
|
||||
|
||||
We'll discuss and learn to build a template-driven form that looks like this:
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/forms/hf-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/hf-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
|
||||
:marked
|
||||
We'll customize the colors and location of the "required" bar with standard CSS.
|
||||
|
||||
:marked
|
||||
We'll build this form in 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 **ng-model** directive to each form input control.
|
||||
1. Add the **ng-control** 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 **ng-submit**.
|
||||
1. Change the form's display after submission.
|
||||
|
||||
:marked
|
||||
## Setup
|
||||
Create a new project folder (`angular2_forms`) and create 3 files:
|
||||
`pubspec.yaml`, `web/index.html`, and `web/main.dart`.
|
||||
(These files should be familiar from the
|
||||
[QuickStart](../quickstart.html).) Put these contents in the files:
|
||||
|
||||
+makeTabs('forms/dart/pubspec.yaml, forms/dart/web/index.html, forms/dart/web/main.dart', ',initial,', 'pubspec.yaml, web/index.html, web/main.dart')
|
||||
|
||||
:marked
|
||||
So that the code can run,
|
||||
let's create a stub for the `<hero-form>` component.
|
||||
|
||||
Create a new directory called `lib`.
|
||||
In it, put a file called `hero_form_component.dart`
|
||||
with the following code:
|
||||
|
||||
+makeExample('forms/dart/lib/hero_form_component_initial.dart', null, 'lib/hero_form_component.dart')
|
||||
|
||||
:marked
|
||||
The app should now run, but it won't do anything interesting.
|
||||
Let's add some data.
|
||||
|
||||
|
||||
## Create the Hero model class
|
||||
|
||||
As users enter form data, we'll capture their changes and update an instance of a model.
|
||||
We can't lay out 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`).
|
||||
|
||||
In the `lib` directory, add a file called `hero.dart`
|
||||
with the following code:
|
||||
|
||||
+makeExample('forms/dart/lib/hero.dart', 'all', 'lib/hero.dart')
|
||||
|
||||
:marked
|
||||
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
||||
|
||||
The `alterEgo` is optional, so the constructor lets us omit it: note the
|
||||
`[]` in `[this.alterEgo]`.
|
||||
|
||||
We can create a new hero like this:
|
||||
|
||||
+makeExample('forms/dart/lib/hero.dart', 'newhero')(format=".")
|
||||
|
||||
: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.
|
||||
|
||||
Edit `hero_form_component.dart`, replacing all of its contents
|
||||
with the following code:
|
||||
|
||||
+makeExample('forms/dart/lib/hero_form_component.dart', null, 'lib/hero_form_component.dart')
|
||||
|
||||
:marked
|
||||
There’s nothing special about this component, nothing to distinguish it from any component we've written before,
|
||||
nothing form-specific about it ... except, perhaps, the tell-tale `FORM_DIRECTIVES` import.
|
||||
[PENDING: Soon that import shouldn't be necessary.]
|
||||
|
||||
Understanding this component requires only the Angular 2 concepts covered in previous chapters.
|
||||
|
||||
1. The code imports a standard set of symbols from the Angular library.
|
||||
|
||||
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
|
||||
<!--TODO: link to (./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 to return a
|
||||
string describing 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 isn't the template inline in the component file?
|
||||
|
||||
Inline templates can be nice when they are short,
|
||||
but most form templates aren't short. Dart 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.
|
||||
It's also nice to have short files with a clear and obvious purpose.
|
||||
|
||||
We made a good choice to put the HTML template elsewhere. Let's write it.
|
||||
|
||||
<!-- NOTE: I deleted the Dart equivalent of "Revise the app.ts"
|
||||
because we started with example-specific index.html & main.dart
|
||||
files. -->
|
||||
|
||||
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Create an initial HTML form template
|
||||
|
||||
Create a new file under `lib` called `hero_form_component.html`,
|
||||
and put the following template code in it:
|
||||
|
||||
+makeExample('forms/dart/lib/hero_form_component_initial.html', null, 'lib/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 are [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.
|
||||
|
||||
1. Download the Bootstrap stylesheet from
|
||||
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css,
|
||||
and put it in the `web` directory.
|
||||
|
||||
2. Edit `web/index.html`, adding a link to `bootstrap.min.css`:
|
||||
|
||||
- var stylePattern = { otl: /(<link rel.*$)/gm };
|
||||
+makeExample('forms/dart/web/index.html', 'bootstrap-and-script', 'web/index.html (excerpt)', stylePattern)(format=".")
|
||||
|
||||
[PENDING: runnable now? Remind about pub get? remind them to look in the browser console]
|
||||
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Add powers with ***ng-for**
|
||||
Our hero must 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 used before in [Displaying Data](./displaying-data.html).
|
||||
|
||||
Add the following HTML *immediately below* the Alter Ego group.
|
||||
+makeExample('forms/dart/lib/hero_form_component_ngmodel_ngfor.html', 'powers', 'lib/hero_form_component.html (excerpt)')(format=".")
|
||||
|
||||
:marked
|
||||
This code repeats the `<option>` 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 ***ng-model**
|
||||
Running the app right now would be disappointing.
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/forms/hf-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 the techniques we already know, but
|
||||
instead we'll introduce something new: the `NgModel` directive, which
|
||||
makes binding the form to the model super easy.
|
||||
|
||||
Find the `<input>` tag for Name and update it like this:
|
||||
|
||||
+makeExample('forms/dart/lib/hero_form_component_ngmodel_ngfor.html', 'ng-model-1', 'lib/hero_form_component.html (excerpt)')(format=".")
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We added 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: `[(ng-model)]="..."`.
|
||||
|
||||
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="ng-model in action")
|
||||
:marked
|
||||
The diagnostic is evidence that values really are flowing from the input box to the model and
|
||||
back again. **That's two-way data binding!**
|
||||
|
||||
Let's add similar `[(ng-model)]` 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 the form should have three `[(ng-model)]` bindings that
|
||||
look much like this:
|
||||
|
||||
+makeExample('forms/dart/lib/hero_form_component_ngmodel2.html', 'ng-model-2', 'lib/hero_form_component.html (excerpt)')(format=".")
|
||||
|
||||
:marked
|
||||
If we ran the app right now and changed every Hero model property, the form might look like this:
|
||||
figure.image-display
|
||||
img(src="images/ng-model-in-action-2.png" width="500px" alt="ng-model in super action")
|
||||
:marked
|
||||
The diagnostic near the top of the form
|
||||
confirms that our changes to the values are reflected in the model.
|
||||
|
||||
** We're done with the diagnostic binding. Delete it now.**
|
||||
|
||||
|
||||
.l-sub-section
|
||||
h3 Inside [(ng-model)]
|
||||
:marked
|
||||
*This section is an optional deep dive into [(ng-model)]. 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.
|
||||
|
||||
A property binding makes data flow 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**.
|
||||
|
||||
An event binding makes data flow from the target property onscreen 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 uses the combined punctuation, <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 in this rewrite of the Name `<input>` binding:
|
||||
+makeExample('forms/dart/lib/hero_form_component_ngmodelchange.html', 'ng-model-3')(format=".")
|
||||
|
||||
:marked
|
||||
<br>The property binding should feel familiar. The event binding might seem strange.
|
||||
|
||||
The name `ng-model-change` specifies 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 `ng-model-change` 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? `[(ng-model)]` is usually what we want, but
|
||||
we might split the binding when the event handling has to do something special
|
||||
such as debounce or throttle the keystrokes.
|
||||
|
||||
<!-- TODO: Add the following once template-syntax.html exists.
|
||||
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 **ng-control**
|
||||
|
||||
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` instance whether
|
||||
the user touched the control, the value changed, or the value is valid.
|
||||
|
||||
`NgControl` doesn't just track state; it updates the control with special
|
||||
Angular CSS classes, such as `ng-valid` or `ng-invalid`.
|
||||
We can use those class names to change the appearance of the
|
||||
control and make messages appear or disappear.
|
||||
|
||||
We'll explore those effects soon. Right now
|
||||
let's **add `ng-control`to all three form controls**,
|
||||
starting with the Name input box.
|
||||
+makeExample('forms/dart/lib/hero_form_component_ngcontrol.html', 'ng-control-1', 'lib/hero_form_component.html (excerpt)')(format=".")
|
||||
:marked
|
||||
Be sure to assign a unique name to each `ng-control` directive.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Angular registers controls under their `ng-control` names
|
||||
with the `NgForm` directive.
|
||||
We didn't add the `NgForm` directive explicitly but it's here;
|
||||
we'll talk about it [later in this chapter](#ng-form).
|
||||
|
||||
|
||||
.l-main-section
|
||||
h2 Add custom CSS for visual feedback
|
||||
|
||||
p.
|
||||
<code>NgControl</code> doesn't just track state. It updates the control to
|
||||
have three classes that describe the control's 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
|
||||
<!-- TODO: make that a link to (./template-syntax.html#local-vars) -->
|
||||
named **spy**
|
||||
to the Name `<input>` tag and use the spy to display those classes.
|
||||
|
||||
+makeExample('forms/dart/lib/hero_form_component_spy.html', 'ng-control-2')(format=".")
|
||||
|
||||
:marked
|
||||
Now run the app, and look at the Name input box.
|
||||
Follow the next four steps *precisely*:
|
||||
|
||||
1. Look but don't touch.
|
||||
1. Click inside the name box, then click outside it.
|
||||
1. Add slashes to the end of the name.
|
||||
1. Erase the name.
|
||||
|
||||
The classes are displayed as follows:
|
||||
|
||||
1. `form-control ng-untouched ng-pristine ng-valid` (initial state)
|
||||
1. `form-control ng-pristine ng-valid ng-touched` (after clicking)
|
||||
1. `form-control ng-valid ng-touched ng-dirty` (after changing)
|
||||
1. `form-control ng-touched ng-dirty ng-invalid` (after erasing)
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition")
|
||||
|
||||
:marked
|
||||
The (`ng-valid` | `ng-invalid`) pair are the most interesting to us, because we want to send a
|
||||
strong visual signal when the values are bad. We also want to mark required fields.
|
||||
|
||||
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
|
||||
(under `web/`).
|
||||
|
||||
+makeExample('forms/dart/web/styles.css', null, 'web/styles.css')
|
||||
:marked
|
||||
These styles select for the two Angular validity classes and the HTML 5 "required" attribute.
|
||||
|
||||
To add the styles to the app,
|
||||
update the `<head>` of `index.html` to link to `styles.css`.
|
||||
- var stylePattern = { otl: /(.*styles.css.*$)/gm };
|
||||
+makeExample('forms/dart/web/index.html', 'styles', 'web/index.html (excerpt)', stylePattern)(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 need to add two chunks of code:
|
||||
1. A local template variable in the `<input>` tag.
|
||||
<!-- TODO: link to (./template-syntax.html#local-vars)-->
|
||||
1. The "*is required*" message in a nearby `<div>`.
|
||||
We'll display this message only if the control is invalid.
|
||||
|
||||
Here's an example of adding an error message to the "name" input box:
|
||||
- var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm };
|
||||
+makeExample('forms/dart/lib/hero_form_component.html', 'name-with-error-msg', 'lib/hero_form_component.html (excerpt)', stylePattern)(format=".")
|
||||
|
||||
:marked
|
||||
We initialized the template local variable with the string "form"
|
||||
(`#name="form"`).
|
||||
|
||||
Angular recognizes that syntax and sets the `name` variable
|
||||
to the `Control` object identified by the `ng-control` directive that,
|
||||
not coincidentally, we called "name".
|
||||
|
||||
We bind the `Control` object's `valid` property to the element's `hidden` property.
|
||||
While the control is valid, the message is hidden;
|
||||
if it becomes invalid, the message is revealed.
|
||||
<a id="ng-form"></a>
|
||||
.l-sub-section
|
||||
h3 The NgForm directive
|
||||
|
||||
:marked
|
||||
Recall from the previous section that `ng-control` registered this input box with the
|
||||
`NgForm` directive as "name".
|
||||
|
||||
We didn't add the `NgForm`<!-- TODO: link to (../api/core/NgForm-class.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 `ng-control` directive)
|
||||
and monitors their properties including their validity.
|
||||
It has its own `valid` property, which is true only if every contained
|
||||
control is valid.
|
||||
|
||||
In this example, we are pulling the "name" control out of its `controls` collection
|
||||
and assigning it to a template local variable so that we can
|
||||
access the control's properties—such as the control's own `valid` property.
|
||||
:marked
|
||||
The 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 values.
|
||||
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Submit the form with **ng-submit**
|
||||
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 meaningless at the moment. To make it meaningful,
|
||||
we'll update the `<form>` tag with another Angular directive, `NgSubmit`,
|
||||
and bind it to the `HeroFormComponent.onSubmit()` method:
|
||||
+makeExample('forms/dart/lib/hero_form_component.html', 'ng-submit')(format=".")
|
||||
|
||||
:marked
|
||||
We slipped in something extra there at the end! We defined a
|
||||
template local variable, **`#hf`**, and initialized it with the value "form".
|
||||
|
||||
The variable `hf` is now a handle to the `NgForm` as we [discussed earlier](#ng-form)
|
||||
with respect to `ng-control`, although this time we have a reference to the form
|
||||
rather than a control.
|
||||
|
||||
We'll bind the form's overall validity via
|
||||
the `hf` variable to the button's `disabled` property
|
||||
using an event binding. Here's the code:
|
||||
+makeExample('forms/dart/lib/hero_form_component.html', 'submit-button')(format=".")
|
||||
:marked
|
||||
If we run the application now, we find that the button is enabled.
|
||||
It doesn't do anything useful yet but it's alive.
|
||||
|
||||
Now if we delete the Name, we violate the "required" rule, which
|
||||
is duly noted in the error message.
|
||||
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. Refer to that variable in a button many 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 aren't interested, go ahead and skip to this chapter's conclusion.
|
||||
: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 binding
|
||||
its `hidden` property to the `HeroFormComponent.submitted` property.
|
||||
|
||||
+makeExample('forms/dart/lib/hero_form_component.html', 'edit-div')(format=".")
|
||||
|
||||
:marked
|
||||
The main form is visible from the start because the
|
||||
`submitted` property is false until we submit the form,
|
||||
as the following code from `hero_form_component.dart` shows:
|
||||
+makeExample('forms/dart/lib/hero_form_component.dart', 'submitted')(format=".")
|
||||
|
||||
:marked
|
||||
When we click the Submit button, the `submitted` flag becomes true and the form disappears
|
||||
as planned.
|
||||
|
||||
Now the app needs 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/dart/lib/hero_form_component.html', 'submitted')(format=".")
|
||||
|
||||
:marked
|
||||
There's our hero again, displayed read-only with interpolation bindings.
|
||||
This slug of HTML appears only while the component is in the submitted state.
|
||||
|
||||
The HTML includes an Edit button whose click event is bound to an expression
|
||||
that clears the `submitted` flag.
|
||||
|
||||
When we click the Edit button, 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 `ng-submit` directive for handling the form submission.
|
||||
- Template local variables such as `#hf`, `#name`, `#alter-ego`, and `#power`.
|
||||
- The `ng-model` directive for two-way data binding.
|
||||
- The `ng-control` directive 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.
|
||||
- Property binding to disable the submit button when the form is invalid.
|
||||
- Custom CSS classes that provide visual feedback to users about required invalid controls.
|
||||
|
||||
Here’s the code for the final version of the application:
|
||||
|
||||
+makeTabs(
|
||||
`forms/dart/lib/hero_form_component.dart,
|
||||
forms/dart/lib/hero_form_component.html,
|
||||
forms/dart/lib/hero.dart,
|
||||
forms/dart/pubspec.yaml,
|
||||
forms/dart/web/index.html,
|
||||
forms/dart/web/main.dart,
|
||||
forms/dart/web/styles.css`,
|
||||
'no-todo,,all,,,,',
|
||||
`lib/hero_form_component.dart,
|
||||
lib/hero_form_component.html,
|
||||
lib/hero.dart,
|
||||
pubspec.yaml,
|
||||
web/index.html,
|
||||
web/main.dart,
|
||||
web/styles.css`)
|
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
Loading…
Reference in New Issue