docs(forms): copyedits (#3149)
* - Copyedits, some from Kathy (from an early revision of the forms page). - Removed side note detailing ngModel; now referring reader to template-syntax page instead. * post-review edits
This commit is contained in:
parent
b793d1d531
commit
806c658253
|
@ -1,20 +1,19 @@
|
||||||
<!-- #docplaster -->
|
<!-- #docplaster -->
|
||||||
<!-- #docregion final -->
|
<!-- #docregion final -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- #docregion edit-div -->
|
<!-- #docregion edit-div -->
|
||||||
<div [hidden]="submitted">
|
<div [hidden]="submitted">
|
||||||
<h1>Hero Form</h1>
|
<h1>Hero Form</h1>
|
||||||
<!-- #docregion ngSubmit -->
|
<!-- #docregion ngSubmit -->
|
||||||
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
|
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
|
||||||
<!-- #enddocregion ngSubmit -->
|
<!-- #enddocregion ngSubmit, edit-div -->
|
||||||
<!-- #enddocregion edit-div -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<!-- #docregion name-with-error-msg -->
|
<!-- #docregion name-with-error-msg -->
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input type="text" class="form-control" id="name"
|
<input type="text" class="form-control" id="name"
|
||||||
required
|
required
|
||||||
[(ngModel)]="model.name" name="name"
|
[(ngModel)]="model.name" name="name"
|
||||||
#name="ngModel" >
|
#name="ngModel">
|
||||||
<!-- #docregion hidden-error-msg -->
|
<!-- #docregion hidden-error-msg -->
|
||||||
<div [hidden]="name.valid || name.pristine"
|
<div [hidden]="name.valid || name.pristine"
|
||||||
class="alert alert-danger">
|
class="alert alert-danger">
|
||||||
|
@ -27,7 +26,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="alterEgo">Alter Ego</label>
|
<label for="alterEgo">Alter Ego</label>
|
||||||
<input type="text" class="form-control" id="alterEgo"
|
<input type="text" class="form-control" id="alterEgo"
|
||||||
[(ngModel)]="model.alterEgo" name="alterEgo" >
|
[(ngModel)]="model.alterEgo" name="alterEgo">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -35,7 +34,7 @@
|
||||||
<select class="form-control" id="power"
|
<select class="form-control" id="power"
|
||||||
required
|
required
|
||||||
[(ngModel)]="model.power" name="power"
|
[(ngModel)]="model.power" name="power"
|
||||||
#power="ngModel" >
|
#power="ngModel">
|
||||||
<option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
|
<option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
|
||||||
</select>
|
</select>
|
||||||
<div [hidden]="power.valid || power.pristine" class="alert alert-danger">
|
<div [hidden]="power.valid || power.pristine" class="alert alert-danger">
|
||||||
|
@ -185,25 +184,25 @@
|
||||||
<!-- EXTRA MATERIAL FOR DOCUMENTATION -->
|
<!-- EXTRA MATERIAL FOR DOCUMENTATION -->
|
||||||
<hr>
|
<hr>
|
||||||
<!-- #docregion ngModelName-1 -->
|
<!-- #docregion ngModelName-1 -->
|
||||||
<input type="text" class="form-control" id="name"
|
<input type="text" class="form-control" id="name"
|
||||||
required
|
required
|
||||||
[(ngModel)]="model.name" name="name">
|
[(ngModel)]="model.name" name="name">
|
||||||
TODO: remove this: {{model.name}}
|
TODO: remove this: {{model.name}}
|
||||||
<!-- #enddocregion ngModelName-1 -->
|
<!-- #enddocregion ngModelName-1 -->
|
||||||
<hr>
|
<hr>
|
||||||
<!-- #docregion ngModel-3-->
|
<!-- #docregion ngModel-3-->
|
||||||
<input type="text" class="form-control" id="name"
|
<input type="text" class="form-control" id="name"
|
||||||
required
|
required
|
||||||
[ngModel]="model.name" name="name"
|
[ngModel]="model.name" name="name"
|
||||||
(ngModelChange)="model.name = $event" >
|
(ngModelChange)="model.name = $event">
|
||||||
TODO: remove this: {{model.name}}
|
TODO: remove this: {{model.name}}
|
||||||
<!-- #enddocregion ngModel-3-->
|
<!-- #enddocregion ngModel-3-->
|
||||||
<hr>
|
<hr>
|
||||||
<!-- #docregion ngModelName-2 -->
|
<!-- #docregion ngModelName-2 -->
|
||||||
<input type="text" class="form-control" id="name"
|
<input type="text" class="form-control" id="name"
|
||||||
required
|
required
|
||||||
[(ngModel)]="model.name" name="name"
|
[(ngModel)]="model.name" name="name"
|
||||||
#spy >
|
#spy>
|
||||||
<br>TODO: remove this: {{spy.className}}
|
<br>TODO: remove this: {{spy.className}}
|
||||||
<!-- #enddocregion ngModelName-2 -->
|
<!-- #enddocregion ngModelName-2 -->
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// #docplaster
|
// #docplaster
|
||||||
// #docregion
|
// #docregion , v1, final
|
||||||
// #docregion first, final
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
import { Hero } from './hero';
|
import { Hero } from './hero';
|
||||||
|
@ -26,21 +25,29 @@ export class HeroFormComponent {
|
||||||
// #enddocregion final
|
// #enddocregion final
|
||||||
// TODO: Remove this when we're done
|
// TODO: Remove this when we're done
|
||||||
get diagnostic() { return JSON.stringify(this.model); }
|
get diagnostic() { return JSON.stringify(this.model); }
|
||||||
// #enddocregion first
|
// #enddocregion v1
|
||||||
|
|
||||||
// #docregion final
|
// #docregion final, new-hero
|
||||||
// #docregion new-hero
|
|
||||||
newHero() {
|
newHero() {
|
||||||
this.model = new Hero(42, '', '');
|
this.model = new Hero(42, '', '');
|
||||||
}
|
}
|
||||||
// #enddocregion new-hero
|
// #enddocregion final, new-hero
|
||||||
// #enddocregion final
|
|
||||||
|
skyDog(): Hero {
|
||||||
|
// #docregion SkyDog
|
||||||
|
let myHero = new Hero(42, 'SkyDog',
|
||||||
|
'Fetch any object at any distance',
|
||||||
|
'Leslie Rollover');
|
||||||
|
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
|
||||||
|
// #enddocregion SkyDog
|
||||||
|
return myHero;
|
||||||
|
}
|
||||||
|
|
||||||
//////// NOT SHOWN IN DOCS ////////
|
//////// NOT SHOWN IN DOCS ////////
|
||||||
|
|
||||||
// Reveal in html:
|
// Reveal in html:
|
||||||
// Name via form.controls = {{showFormControls(heroForm)}}
|
// Name via form.controls = {{showFormControls(heroForm)}}
|
||||||
showFormControls(form: any) {
|
showFormControls(form: any) {
|
||||||
|
|
||||||
return form && form.controls['name'] &&
|
return form && form.controls['name'] &&
|
||||||
// #docregion form-controls
|
// #docregion form-controls
|
||||||
form.controls['name'].value; // Dr. IQ
|
form.controls['name'].value; // Dr. IQ
|
||||||
|
@ -49,6 +56,5 @@ export class HeroFormComponent {
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
|
||||||
// #docregion first, final
|
// #docregion v1, final
|
||||||
}
|
}
|
||||||
// #enddocregion first, final
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ include ../_util-fns
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
We’ve all used a form to log in, submit a help request, place an order, book a flight,
|
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.
|
schedule a meeting, and perform countless other data entry tasks.
|
||||||
Forms are the mainstay of business applications.
|
Forms are the mainstay of business applications.
|
||||||
|
|
||||||
Any seasoned web developer can slap together an HTML form with all the right tags.
|
Any seasoned web developer can slap together an HTML form with all the right tags.
|
||||||
|
@ -15,91 +15,89 @@ include ../_util-fns
|
||||||
**two-way data binding, change tracking, validation, and error handling**
|
**two-way data binding, change tracking, validation, and error handling**
|
||||||
... which we shall cover in this guide on Angular forms.
|
... which we shall cover in this guide on Angular forms.
|
||||||
|
|
||||||
We will build a simple form from scratch, one step at a time. Along the way we'll learn how to
|
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
|
- Build an Angular form with a component and template
|
||||||
|
- Use `ngModel` to create two-way data bindings for reading and writing input control values
|
||||||
- two-way data bind with `[(ngModel)]` syntax for reading and writing values to input controls
|
- Track state changes and the validity of form controls
|
||||||
|
- Provide visual feedback using special CSS classes that track the state of the controls
|
||||||
- track the change state and validity of form controls using `ngModel` in combination with a form
|
- Display validation errors to users and enable/disable form controls
|
||||||
|
- Share information across HTML elements using template reference variables
|
||||||
- provide strong visual feedback using special CSS classes that track the state of the controls
|
|
||||||
|
|
||||||
- display validation errors to users and enable/disable form controls
|
|
||||||
|
|
||||||
- use [template reference variables](./template-syntax.html#ref-vars) for sharing information among HTML elements
|
|
||||||
|
|
||||||
Run the <live-example></live-example>.
|
Run the <live-example></live-example>.
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Template-Driven Forms
|
## Template-driven forms
|
||||||
|
|
||||||
Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with
|
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 guide.
|
the form-specific directives and techniques described in this guide.
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
That's not the only way to create a form but it's the way we'll cover in this guide.
|
That's not the only way to create a form but it's the way we'll cover in this guide.
|
||||||
:marked
|
:marked
|
||||||
We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms.
|
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,
|
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 repetitive, boiler plate tasks we'd
|
It will be pretty easy because Angular handles many of the repetitive, boilerplate 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 a template-driven form that looks like this:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/hero-form-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 heroes.
|
||||||
heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis!
|
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.
|
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:
|
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/hero-form-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.
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
p We'll customize the colors and location of the "required" bar with standard CSS.
|
:marked
|
||||||
|
We'll customize the colors and location of the "required" bar with standard CSS.
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
We will build this form in the following sequence of small steps
|
We'll build this form in small steps:
|
||||||
|
|
||||||
1. Create the `Hero` model class
|
1. Create the `Hero` model class.
|
||||||
1. Create the component that controls the form
|
1. Create the component that controls the form.
|
||||||
1. Create a template with the initial form layout
|
1. Create a template with the initial form layout.
|
||||||
1. Bind data properties to each form input control with the `ngModel` two-way data binding syntax
|
1. Bind data properties to each form control using the `ngModel` two-way data binding syntax.
|
||||||
1. Add the `name` attribute to each form input control
|
1. Add a `name` attribute to each form input control.
|
||||||
1. Add custom CSS to provide visual feedback
|
1. Add custom CSS to provide visual feedback.
|
||||||
1. Show and hide validation error messages
|
1. Show and hide validation error messages.
|
||||||
1. Handle form submission with **ngSubmit**
|
1. Handle form submission with **ngSubmit**.
|
||||||
1. Disable the form’s submit button until the form is valid
|
1. Disable the form’s submit button until the form is valid.
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Follow the [setup](setup.html) instructions for creating a new project
|
Follow the [setup](setup.html) instructions for creating a new project
|
||||||
named <span ngio-ex>angular-forms</span>.
|
named <span ngio-ex>angular-forms</span>.
|
||||||
|
|
||||||
## Create the Hero Model Class
|
## Create the Hero model class
|
||||||
|
|
||||||
As users enter form data, we capture their changes and update an instance of a model.
|
As users enter form data, we'll capture their changes and update an instance of a model.
|
||||||
We can't layout the form until we know what the model looks like.
|
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.
|
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`)
|
That describes well our `Hero` class with its three required fields (`id`, `name`, `power`)
|
||||||
and one optional field (`alterEgo`).
|
and one optional field (`alterEgo`).
|
||||||
|
|
||||||
Create a new file in the app folder called `hero.ts` and give it the following class definition:
|
In the `!{_appDir}` directory, create the following file with the given content:
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero.ts', null, 'app/hero.ts')
|
+makeExample('app/hero.ts')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
||||||
|
@ -107,49 +105,40 @@ figure.image-display
|
||||||
The TypeScript compiler generates a public field for each `public` constructor parameter and
|
The TypeScript compiler generates a public field for each `public` constructor parameter and
|
||||||
assigns the parameter’s value to that field automatically when we create new heroes.
|
assigns the parameter’s value to that field automatically when we create new heroes.
|
||||||
|
|
||||||
The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`.
|
The `alterEgo` is optional, so the constructor lets us omit it; note the (?) in `alterEgo?`.
|
||||||
|
|
||||||
We can create a new hero like this:
|
We can create a new hero like this:
|
||||||
code-example(format="").
|
|
||||||
let myHero = new Hero(42, 'SkyDog',
|
+makeExcerpt('app/hero-form.component.ts', 'SkyDog', '')
|
||||||
'Fetch any object at any distance',
|
|
||||||
'Leslie Rollover');
|
|
||||||
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
|
|
||||||
:marked
|
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Create a Form component
|
## Create a form component
|
||||||
|
|
||||||
An Angular form has two parts: an HTML-based _template_ and a component _class_
|
An Angular form has two parts: an HTML-based _template_ and a component _class_
|
||||||
to handle data and user interactions programmatically.
|
to handle data and user interactions programmatically.
|
||||||
We begin with the class because it states, in brief, what the hero editor can do.
|
We begin with the class because it states, in brief, what the hero editor can do.
|
||||||
|
|
||||||
Create a new file called `hero-form.component.ts` and give it the following definition:
|
Create the following file with the given content:
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.ts', 'first', 'app/hero-form.component.ts')
|
+makeExcerpt('app/hero-form.component.ts', 'v1')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before.
|
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 concepts we’ve learned in previous guides
|
Understanding this component requires only the Angular concepts covered in previous guides.
|
||||||
|
|
||||||
1. We import the `Component` decorator from the Angular library as we usually do.
|
|
||||||
|
|
||||||
1. We import the `Hero` model we just created.
|
|
||||||
|
|
||||||
|
1. The code imports the Angular core library, and the `Hero` model we just created.
|
||||||
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 `@Component` selector value of "hero-form" means we can drop this form in a parent template with a `<hero-form>` tag.
|
||||||
|
|
||||||
1. The `moduleId: module.id` property sets the base for module-relative loading of the `templateUrl`.
|
1. The `moduleId: module.id` property sets the base for module-relative loading of the `templateUrl`.
|
||||||
|
1. The `templateUrl` property points to a separate file for the template HTML.
|
||||||
1. The `templateUrl` property points to a separate file for the template HTML called `hero-form.component.html`.
|
1. We defined dummy data for `model` and `powers`, as befits a demo.
|
||||||
|
|
||||||
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
|
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
|
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.
|
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 JSON representation of our model.
|
||||||
1. We threw in a `diagnostic` property at the end to return a JSON representation of our model.
|
|
||||||
It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later.
|
It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later.
|
||||||
|
|
||||||
### Why the separate template file?
|
### Why the separate template file?
|
||||||
|
@ -168,7 +157,7 @@ code-example(format="").
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Revise the *app.module.ts*
|
## Revise *app.module.ts*
|
||||||
|
|
||||||
`app.module.ts` defines the application's root module. In it we identify the external modules we'll use in our application
|
`app.module.ts` defines the application's root module. In it we identify the external modules we'll use in our application
|
||||||
and declare the components that belong to this module, such as our `HeroFormComponent`.
|
and declare the components that belong to this module, such as our `HeroFormComponent`.
|
||||||
|
@ -199,27 +188,29 @@ code-example(format="").
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Revise the *app.component.ts*
|
## Revise *app.component.ts*
|
||||||
|
|
||||||
`app.component.ts` is the application's root component. It will host our new `HeroFormComponent`.
|
`AppComponent` is the application's root component. It will host our new `HeroFormComponent`.
|
||||||
|
|
||||||
Replace the contents of the "QuickStart" version with the following:
|
Replace the contents of the "QuickStart" version with the following:
|
||||||
+makeExample('forms/ts/app/app.component.ts', null, 'app/app.component.ts')
|
|
||||||
|
+makeExample('app/app.component.ts')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
There is only one change.
|
There are only two changes.
|
||||||
The `template` is simply the new element tag identified by the component's `selector` property.
|
The `template` is simply the new element tag identified by the component's `selector` property.
|
||||||
This will display the hero form when the application component is loaded.
|
This will display the hero form when the application component is loaded.
|
||||||
|
We've also dropped the `name` field from the class body.
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Create an initial HTML Form Template
|
## Create an initial HTML form template
|
||||||
|
|
||||||
Create a new template file called `hero-form.component.html` and give it the following definition:
|
Create the new template file with the following contents:
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'start', 'app/hero-form.component.html')
|
+makeExample('app/hero-form.component.html', 'start')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and
|
That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and
|
||||||
|
@ -230,45 +221,47 @@ code-example(format="").
|
||||||
|
|
||||||
We've got a *Submit* button at the bottom with some classes on it for styling.
|
We've got a *Submit* button at the bottom with some classes on it for styling.
|
||||||
|
|
||||||
**We are not using Angular yet**. There are no bindings. No extra directives. Just layout.
|
**We are not using Angular yet**. There are no bindings, no extra directives, just layout.
|
||||||
|
|
||||||
The `container`, `form-group`, `form-control`, and `btn` classes
|
The `container`, `form-group`, `form-control`, and `btn` classes
|
||||||
come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic.
|
come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic.
|
||||||
We're using Bootstrap to give the form a little style!
|
We're using Bootstrap to give the form a little style!
|
||||||
|
|
||||||
.callout.is-important
|
.callout.is-important
|
||||||
header Angular Forms Do Not Require A Style Library
|
header Angular forms do not require a style library
|
||||||
:marked
|
:marked
|
||||||
Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or
|
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
|
the styles of any external library. Angular apps can use any CSS library, or none at all.
|
||||||
... or none at all.
|
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Let's add the stylesheet. Open <code>index.html</code> and add the following link to the <code><head></code>.
|
Let's add the stylesheet. Open `index.html` and add the following link to the `<head>`:
|
||||||
+makeExample('forms/ts/index.html', 'bootstrap')(format=".")
|
|
||||||
|
+makeExcerpt('index.html', 'bootstrap')
|
||||||
|
|
||||||
:marked
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Add Powers with ***ngFor**
|
## Add powers with _*ngFor_
|
||||||
Our hero may choose one super power from a fixed list of Agency-approved powers.
|
|
||||||
|
Our hero must choose one super power from a fixed list of Agency-approved powers.
|
||||||
We maintain that list internally (in `HeroFormComponent`).
|
We maintain that list internally (in `HeroFormComponent`).
|
||||||
|
|
||||||
We'll add a `select` to our
|
We'll add a `select` to our
|
||||||
form and bind the options to the `powers` list using `ngFor`,
|
form and bind the options to the `powers` list using `ngFor`,
|
||||||
a technique seen previously in the [Displaying Data](./displaying-data.html) guide.
|
a technique seen previously in the [Displaying Data](./displaying-data.html) guide.
|
||||||
|
|
||||||
Add the following HTML *immediately below* the *Alter Ego* group.
|
Add the following HTML *immediately below* the *Alter Ego* group:
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".")
|
|
||||||
|
+makeExcerpt('app/hero-form.component.html (powers)')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
We are repeating the `<options>` tag for each power in the list of Powers.
|
This code repeats the `<option>` tag for each power in the list of powers.
|
||||||
The `pow` template input variable is a different power in each iteration;
|
The `pow` template input variable is a different power in each iteration;
|
||||||
we display its name using the interpolation syntax with the double-curly-braces.
|
we display its name using the interpolation syntax.
|
||||||
|
|
||||||
.l-main-section#ngModel
|
.l-main-section#ngModel
|
||||||
:marked
|
:marked
|
||||||
## Two-way data binding with **_ngModel_**
|
## Two-way data binding with _ngModel_
|
||||||
|
|
||||||
Running the app right now would be disappointing.
|
Running the app right now would be disappointing.
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
|
@ -276,23 +269,23 @@ figure.image-display
|
||||||
: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 guides.
|
We know how to do that from earlier guides.
|
||||||
[Displaying Data](./displaying-data.html) taught us Property Binding.
|
[Displaying Data](./displaying-data.html) taught us property binding.
|
||||||
[User Input](./user-input.html) showed us how to listen for DOM events with an
|
[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.
|
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.
|
Now we need to display, listen, and extract at the same time.
|
||||||
|
|
||||||
We could use those techniques again in our form.
|
We could use the techniques we already know, but
|
||||||
Instead we'll introduce something new, the `[(ngModel)]` syntax, that
|
instead we'll introduce something new: the `[(ngModel)]` syntax, which
|
||||||
makes binding our form to the model super-easy.
|
makes binding the form to the model super easy.
|
||||||
|
|
||||||
Find the `<input>` tag for the "Name" and update it like this
|
Find the `<input>` tag for *Name* and update it like this:
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-1','app/hero-form.component.html (excerpt)')(format=".")
|
+makeExcerpt('app/hero-form.component.html (excerpt)', 'ngModelName-1')
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
We appended a diagnostic interpolation after the input tag
|
We added a diagnostic interpolation after the input tag
|
||||||
so we can see what we're doing.
|
so we can see what we're doing.
|
||||||
We left ourselves a note to throw it away when we're done.
|
We left ourselves a note to throw it away when we're done.
|
||||||
|
|
||||||
|
@ -303,103 +296,71 @@ figure.image-display
|
||||||
adding and deleting characters, we'd see them appearing and disappearing
|
adding and deleting characters, we'd see them appearing and disappearing
|
||||||
from the interpolated text.
|
from the interpolated text.
|
||||||
At some point it might look like this.
|
At some point it might look like this.
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action")
|
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!**
|
|
||||||
|
|
||||||
|
:marked
|
||||||
|
The diagnostic is evidence that values really are flowing from the input box to the model and
|
||||||
|
back again.
|
||||||
|
|
||||||
|
.l-sub-section
|
||||||
|
:marked
|
||||||
|
That's **two-way data binding**!
|
||||||
|
For more information about `[(ngModel)]` and two-way data bindings, see
|
||||||
|
the [Template Syntax](template-syntax.html#ngModel) page.
|
||||||
|
|
||||||
|
:marked
|
||||||
Notice that we also added a `name` attribute to our `<input>` tag and set it to "name"
|
Notice that we also added a `name` attribute to our `<input>` tag and set it to "name"
|
||||||
which makes sense for the hero's name. Any unique value will do, but using a descriptive name is helpful.
|
which makes sense for the hero's name. Any unique value will do, but using a descriptive name is helpful.
|
||||||
Defining a `name` attribute is a requirement when using `[(ngModel)]` in combination with a form.
|
Defining a `name` attribute is a requirement when using `[(ngModel)]` in combination with a form.
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
Internally Angular creates `FormControls` and registers them with an `NgForm` directive that Angular
|
Internally Angular creates `FormControl` instances and
|
||||||
attached to the `<form>` tag. Each `FormControl` is registered under the name we assigned to the `name` attribute.
|
registers them with an `NgForm` directive that Angular attached to the `<form>` tag.
|
||||||
|
Each `FormControl` is registered under the name we assigned to the `name` attribute.
|
||||||
We'll talk about `NgForm` [later in this guide](#ngForm).
|
We'll talk about `NgForm` [later in this guide](#ngForm).
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Let's add similar `[(ngModel)]` bindings and `name` attributes to *Alter Ego* and *Hero Power*.
|
Let's add similar `[(ngModel)]` bindings and `name` attributes to *Alter Ego* and *Hero Power*.
|
||||||
We'll ditch the input box binding message
|
We'll ditch the input box binding message
|
||||||
and add a new binding at the top to the component's `diagnostic` property.
|
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*.
|
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 and `name` attributes that
|
After revision, the core of our form should look like this:
|
||||||
look much like this:
|
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)')
|
+makeExcerpt('app/hero-form.component.html (excerpt)', 'ngModel-2')
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
- Each input element has an `id` property that is used by the `label` element's `for` attribute
|
- Each input element has an `id` property that is used by the `label` element's `for` attribute
|
||||||
to match the label to its input control.
|
to match the label to its input control.
|
||||||
- Each input element has a `name` property that is required by Angular Forms to register the control with the form.
|
- Each input element has a `name` property that is required by Angular forms to register the control with the form.
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
If we run the app right now and changed every hero model property, the form might display like this:
|
If we run the app now and changed every hero model property, the form might display like this:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action")
|
img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in action")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
The diagnostic near the top of the form
|
The diagnostic near the top of the form
|
||||||
confirms that all of our changes are reflected in the model.
|
confirms that all of our changes are reflected in the model.
|
||||||
|
|
||||||
**Delete** the `{{diagnostic}}` binding at the top as it has served its purpose.
|
**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/ts/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
|
||||||
<br>The Property Binding should feel familiar. The Event Binding might seem strange.
|
|
||||||
|
|
||||||
The `ngModelChange` is not an `<input>` element event.
|
|
||||||
It is actually an event property of the `NgModel` directive.
|
|
||||||
When Angular sees a binding target in the form <span style="font-family:courier">[(x)]</span>,
|
|
||||||
it expects the `x` directive to have an `x` input property and an `xChange` output property.
|
|
||||||
|
|
||||||
The other oddity is the template expression, `model.name = $event`.
|
|
||||||
We're used to seeing an `$event` object coming from a DOM event.
|
|
||||||
The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter`
|
|
||||||
property that returns the input box value when it fires — which is precisely what
|
|
||||||
we should assign to the model's `name` property.
|
|
||||||
|
|
||||||
Nice to know but is it practical? We almost always prefer `[(ngModel)]`.
|
|
||||||
We might split the binding if we had to do something special in
|
|
||||||
the event handling such as debounce or throttle the key strokes.
|
|
||||||
|
|
||||||
Learn more about `NgModel` and other template syntax in the
|
|
||||||
[Template Syntax](./template-syntax.html) guide.
|
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Track change-state and validity with **_ngModel_**
|
## Track control state and validity with _ngModel_
|
||||||
|
|
||||||
A form isn't just about data binding. We'd also like to know the state of the controls on our form.
|
A form isn't just about data binding. We'd also like to know the state of the controls in our form.
|
||||||
|
|
||||||
Using `ngModel` in a form gives us more than just two way data binding. It also tells us if the user touched the control, if the value changed, or if the value became invalid.
|
Using `ngModel` in a form gives us more than just a two way data binding. It also tells
|
||||||
|
us if the user touched the control, if the value changed, or if the value became invalid.
|
||||||
|
|
||||||
The *NgModel* directive doesn't just track state; it updates the control with special Angular CSS classes that reflect the state.
|
The *NgModel* directive doesn't just track state; it updates the control with special Angular CSS classes that reflect the state.
|
||||||
We can leverage those class names to change the appearance of the
|
We can leverage those class names to change the appearance of the control.
|
||||||
control and make messages appear or disappear.
|
|
||||||
|
|
||||||
table
|
table
|
||||||
tr
|
tr
|
||||||
|
@ -418,65 +379,70 @@ table
|
||||||
td Control's value is valid
|
td Control's value is valid
|
||||||
td <code>ng-valid</code>
|
td <code>ng-valid</code>
|
||||||
td <code>ng-invalid</code>
|
td <code>ng-invalid</code>
|
||||||
:marked
|
|
||||||
Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy**
|
|
||||||
to the "Name" `<input>` tag and use the spy to display those classes.
|
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-2','app/hero-form.component.html (excerpt)')(format=".")
|
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Now run the app and focus on the *Name* input box.
|
Let's temporarily add a [template reference variable](./template-syntax.html#ref-vars) named `spy`
|
||||||
Follow the next four steps *precisely*
|
to the _Name_ `<input>` tag and use it to display the input's CSS classes.
|
||||||
|
|
||||||
1. Look but don't touch
|
+makeExcerpt('app/hero-form.component.html (excerpt)', 'ngModelName-2')
|
||||||
1. Click in the input box, then click outside the text input box
|
|
||||||
1. Add slashes to the end of the name
|
:marked
|
||||||
1. Erase the name
|
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 actions and effects are as follows:
|
The actions and effects are as follows:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition")
|
img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
We should be able to see the following four sets of class names and their transitions:
|
We should see the following transitions and class names:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions")
|
img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="500px" alt="Control state transitions")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a
|
The `ng-valid`/`ng-invalid` pair is the most interesting to us, because we want to send a
|
||||||
strong visual signal when the data are invalid and we want to mark required fields.
|
strong visual signal when the values are invalid. We also want to mark required fields.
|
||||||
So we add custom CSS for visual feedback.
|
To create such visual feedback, let's add definitions for the `ng-*` CSS classes.
|
||||||
|
|
||||||
**Delete** the `#spy` template reference variable and `TODO` as they have served their purpose.
|
**Delete** the `#spy` template reference variable and the `TODO` as they have served their purpose.
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Add Custom CSS for Visual Feedback
|
## Add custom CSS for visual feedback
|
||||||
We realize we can mark required fields and invalid data at the same time with a colored bar
|
|
||||||
|
We can mark required fields and invalid data at the same time with a colored bar
|
||||||
on the left of the input box:
|
on the left of the input box:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form")
|
img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
We achieve this effect by adding two styles to a new `forms.css` file
|
We achieve this effect by adding these class definitions to a new `forms.css` file
|
||||||
that we add to our project as a sibling to `index.html`.
|
that we add to our project as a sibling to `index.html`:
|
||||||
|
|
||||||
|
+makeExample('forms.css')
|
||||||
|
|
||||||
+makeExample('forms/ts/forms.css',null,'forms.css')(format=".")
|
|
||||||
:marked
|
:marked
|
||||||
These styles select for the two Angular validity classes and the HTML 5 "required" attribute.
|
Update the `<head>` of `index.html` to include this style sheet:
|
||||||
|
|
||||||
|
+makeExcerpt('index.html', 'styles')
|
||||||
|
|
||||||
We update the `<head>` of the `index.html` to include this style sheet.
|
|
||||||
+makeExample('forms/ts/index.html', 'styles', 'index.html (excerpt)')(format=".")
|
|
||||||
:marked
|
:marked
|
||||||
## Show and Hide Validation Error messages
|
## Show and hide validation error messages
|
||||||
|
|
||||||
We can do better.
|
We can do better. The _Name_ input box is required and 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.
|
||||||
The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we
|
We can leverage the control's state to reveal a helpful message.
|
||||||
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:
|
Here's the way it should look when the user deletes the name:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required")
|
img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required")
|
||||||
|
|
||||||
|
@ -485,13 +451,14 @@ figure.image-display
|
||||||
1. a [template reference variable](./template-syntax.html#ref-vars)
|
1. a [template reference variable](./template-syntax.html#ref-vars)
|
||||||
1. the "*is required*" message in a nearby `<div>` which we'll display only if the control is invalid.
|
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:
|
Here's an example of adding an error message to the _name_ input box:
|
||||||
+makeExample('forms/ts/app/hero-form.component.html',
|
|
||||||
'name-with-error-msg',
|
+makeExcerpt('app/hero-form.component.html (excerpt)', 'name-with-error-msg')
|
||||||
'app/hero-form.component.html (excerpt)')(format=".")
|
|
||||||
:marked
|
:marked
|
||||||
We need a template reference variable to access the input box's Angular control from within the template.
|
We need a template reference variable to access the input box's Angular control from within the template.
|
||||||
Here we created a variable called `name` and gave it the value "ngModel".
|
Here we created a variable called `name` and gave it the value "ngModel".
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
Why "ngModel"?
|
Why "ngModel"?
|
||||||
|
@ -499,10 +466,12 @@ figure.image-display
|
||||||
tells Angular how to link the reference variable to the directive.
|
tells Angular how to link the reference variable to the directive.
|
||||||
We set `name` to `ngModel` because the `ngModel` directive's `exportAs` property happens to be "ngModel".
|
We set `name` to `ngModel` because the `ngModel` directive's `exportAs` property happens to be "ngModel".
|
||||||
|
|
||||||
Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `<div>` element's `hidden` property.
|
:marked
|
||||||
+makeExample('forms/ts/app/hero-form.component.html',
|
We control visibility of the name error message by binding properties of the `name`
|
||||||
'hidden-error-msg',
|
control to the message `<div>` element's `hidden` property.
|
||||||
'app/hero-form.component.html (excerpt)')(format='.')
|
|
||||||
|
+makeExcerpt('app/hero-form.component.html', 'hidden-error-msg', '')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
In this example, we hide the message when the control is valid or pristine;
|
In this example, we hide the message when the control is valid or pristine;
|
||||||
pristine means the user hasn't changed the value since it was displayed in this form.
|
pristine means the user hasn't changed the value since it was displayed in this form.
|
||||||
|
@ -512,31 +481,29 @@ figure.image-display
|
||||||
If we arrive in this component with a new (blank) hero or an invalid hero,
|
If we arrive in this component with a new (blank) hero or an invalid hero,
|
||||||
we'll see the error message immediately, before we've done anything.
|
we'll see the error message immediately, before we've done anything.
|
||||||
|
|
||||||
Some folks find that behavior disconcerting. They only want to see the message when the user makes an invalid change.
|
Some folks find that behavior disconcerting.
|
||||||
|
They only want to see the message when the user makes an invalid change.
|
||||||
Hiding the message while the control is "pristine" achieves that goal.
|
Hiding the message while the control is "pristine" achieves that goal.
|
||||||
We'll see the significance of this choice when we [add a new hero](#new-hero) to the form.
|
We'll see the significance of this choice when we [add a new hero](#new-hero) to the form.
|
||||||
|
|
||||||
The hero *Alter Ego* is optional so we can leave that be.
|
The hero *Alter Ego* is optional so we can leave that be.
|
||||||
|
|
||||||
Hero *Power* selection is required.
|
Hero *Power* selection is required.
|
||||||
We can add the same kind of error handling to the `<select>` if we want
|
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
|
but it's not imperative because the selection box already constrains the
|
||||||
power to valid value.
|
power to valid values.
|
||||||
|
|
||||||
<a id="new-hero"></a>
|
|
||||||
<a id="reset"></a>
|
|
||||||
.l-main-section
|
|
||||||
:marked
|
|
||||||
## Add a hero and reset the form
|
|
||||||
We'd like to add a new hero in this form.
|
We'd like to add a new hero in this form.
|
||||||
We place a "New Hero" button at the bottom of the form and bind its click event to a `newHero` component method.
|
We place a "New Hero" button at the bottom of the form and bind its click event to a `newHero` component method.
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.html',
|
+makeExample('forms/ts/app/hero-form.component.html',
|
||||||
'new-hero-button-no-reset',
|
'new-hero-button-no-reset',
|
||||||
'app/hero-form.component.html (New Hero button)')
|
'app/hero-form.component.html (New Hero button)')
|
||||||
:marked
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.ts',
|
+makeExample('forms/ts/app/hero-form.component.ts',
|
||||||
'new-hero',
|
'new-hero',
|
||||||
'app/hero-form.component.ts (New Hero method)')(format=".")
|
'app/hero-form.component.ts (New Hero method)')(format=".")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Run the application again, click the *New Hero* button, and the form clears.
|
Run the application again, click the *New Hero* button, and the form clears.
|
||||||
The *required* bars to the left of the input box are red, indicating invalid `name` and `power` properties.
|
The *required* bars to the left of the input box are red, indicating invalid `name` and `power` properties.
|
||||||
|
@ -554,74 +521,87 @@ figure.image-display
|
||||||
|
|
||||||
We have to clear all of the flags imperatively which we can do
|
We have to clear all of the flags imperatively which we can do
|
||||||
by calling the form's `reset()` method after calling the `newHero()` method.
|
by calling the form's `reset()` method after calling the `newHero()` method.
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.html',
|
+makeExample('forms/ts/app/hero-form.component.html',
|
||||||
'new-hero-button-form-reset',
|
'new-hero-button-form-reset',
|
||||||
'app/hero-form.component.html (Reset the form)')
|
'app/hero-form.component.html (Reset the form)')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Now clicking "New Hero" both resets the form and its control flags.
|
Now clicking "New Hero" both resets the form and its control flags.
|
||||||
:marked
|
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Submit the form with **_ngSubmit_**
|
## Submit the form with _ngSubmit_
|
||||||
|
|
||||||
The user should be able to submit this form after filling it in.
|
The user should be able to submit this form after filling it in.
|
||||||
The Submit button at the bottom of the form
|
The Submit button at the bottom of the form
|
||||||
does nothing on its own but it will
|
does nothing on its own, but it will
|
||||||
trigger a form submit because of its type (`type="submit"`).
|
trigger a form submit because of its type (`type="submit"`).
|
||||||
|
|
||||||
A "form submit" is useless at the moment.
|
A "form submit" is useless at the moment.
|
||||||
To make it useful, bind the `NgForm` directive's `ngSubmit` event property (in the `<form>` tag)
|
To make it useful, bind the form's `ngSubmit` event property
|
||||||
to the `HeroFormComponent.submit()` method:
|
to the hero form component's `onSubmit()` method:
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'ngSubmit')(format=".")
|
|
||||||
|
+makeExcerpt('forms/ts/app/hero-form.component.html (ngSubmit)')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
We slipped in something extra there at the end! We defined a
|
We slipped in something extra there at the end! We defined a
|
||||||
template reference variable, **`#heroForm`**, and initialized it with the value, "ngForm".
|
template reference variable, **`#heroForm`**, and initialized it with the value "ngForm".
|
||||||
|
|
||||||
The variable `heroForm` is now a reference to the `NgForm` directive that governs the form as a whole.
|
The variable `heroForm` is now a reference to the `NgForm` directive that governs the form as a whole.
|
||||||
<a id="ngForm"></a>
|
|
||||||
.l-sub-section
|
.l-sub-section#ngForm
|
||||||
:marked
|
:marked
|
||||||
### The _NgForm_ directive
|
### The _NgForm_ directive
|
||||||
What `NgForm` directive? We didn't add an [NgForm](../api/forms/index/NgForm-directive.html) directive!
|
|
||||||
|
What `NgForm` directive?
|
||||||
|
We didn't add an [NgForm](../api/forms/index/NgForm-directive.html) directive!
|
||||||
|
|
||||||
Angular did. Angular creates and attaches an `NgForm` directive to the `<form>` tag automatically.
|
Angular did. Angular creates and attaches an `NgForm` directive to the `<form>` tag automatically.
|
||||||
|
|
||||||
The `NgForm` directive supplements the `form` element with additional features.
|
The `NgForm` directive supplements the `form` element with additional features.
|
||||||
It holds the controls we created for the elements with `ngModel` directive and `name` attribute
|
It holds the controls we created for the elements with an `ngModel` directive
|
||||||
and monitors their properties including their validity.
|
and `name` attribute, and monitors their properties including their validity.
|
||||||
It also has its own `valid` property which is true only *if every contained
|
It also has its own `valid` property which is true only *if every contained
|
||||||
control* is valid.
|
control* is valid.
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Later in the template we bind the button's `disabled` property to the form's over-all validity via
|
We'll bind the form's overall validity via
|
||||||
the `heroForm` variable. Here's that bit of markup:
|
the `heroForm` variable to the button's `disabled` property
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'submit-button')
|
using an event binding. Here's the code:
|
||||||
: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
|
+makeExcerpt('app/hero-form.component.html', 'submit-button', '')
|
||||||
is duly noted in our error message as before. And now the Submit button is also disabled.
|
|
||||||
|
:marked
|
||||||
|
If we run the application now, we find that the button is enabled
|
||||||
|
— although it doesn't do anything useful yet.
|
||||||
|
|
||||||
|
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
|
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?
|
wire the button's enable/disabled state to the form's validity without Angular's help?
|
||||||
|
|
||||||
For us, it was as simple as
|
For us, it was as simple as:
|
||||||
1. Define a template reference variable on the (enhanced) form element
|
|
||||||
2. Reference that variable in a button some 50 lines away.
|
1. Define a template reference variable on the (enhanced) form element.
|
||||||
|
2. Refer to that variable in a button many lines away.
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Toggle two form regions (extra credit)
|
## Toggle two form regions (extra credit)
|
||||||
|
|
||||||
Submitting the form isn't terribly dramatic at the moment.
|
Submitting the form isn't terribly dramatic at the moment.
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
An unsurprising observation for a demo. To be honest,
|
An unsurprising observation for a demo. To be honest,
|
||||||
jazzing it up won't teach us anything new about forms.
|
jazzing it up won't teach us anything new about forms.
|
||||||
But this is an opportunity to exercise some of our newly won
|
But this is an opportunity to exercise some of our newly won
|
||||||
binding skills.
|
binding skills.
|
||||||
If you're not interested, you can skip to the guide's conclusion
|
If you aren't interested, go ahead and skip to this guide's conclusion.
|
||||||
and not miss a thing.
|
|
||||||
:marked
|
:marked
|
||||||
Let's do something more strikingly visual.
|
Let's do something more strikingly visual.
|
||||||
Let's hide the data entry area and display something else.
|
Let's hide the data entry area and display something else.
|
||||||
|
@ -629,31 +609,32 @@ figure.image-display
|
||||||
Start by wrapping the form in a `<div>` and bind
|
Start by wrapping the form in a `<div>` and bind
|
||||||
its `hidden` property to the `HeroFormComponent.submitted` property.
|
its `hidden` property to the `HeroFormComponent.submitted` property.
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'edit-div', 'app/hero-form.component.html (excerpt)')(format=".")
|
+makeExcerpt('app/hero-form.component.html (excerpt)', 'edit-div')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
The main form is visible from the start because the
|
The main form is visible from the start because the
|
||||||
the `submitted` property is false until we submit the form,
|
`submitted` property is false until we submit the form,
|
||||||
as this fragment from the `HeroFormComponent` reminds us:
|
as this fragment from the `HeroFormComponent` shows:
|
||||||
|
|
||||||
+makeExample('forms/ts/app/hero-form.component.ts', 'submitted')(format=".")
|
+makeExcerpt('app/hero-form.component.ts', 'submitted')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
When we click the Submit button, the `submitted` flag becomes true and the form disappears
|
When we click the Submit button, the `submitted` flag becomes true and the form disappears
|
||||||
as planned.
|
as planned.
|
||||||
|
|
||||||
Now we need to show something else while the form is in the submitted state.
|
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:
|
Add the following HTML below the `<div>` wrapper we just wrote:
|
||||||
+makeExample('forms/ts/app/hero-form.component.html', 'submitted', 'app/hero-form.component.html (excerpt)')
|
|
||||||
|
+makeExcerpt('app/hero-form.component.html (excerpt)', 'submitted')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
There's our hero again, displayed read-only with interpolation bindings.
|
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.
|
This `<div>` appears only while the component is in the submitted state.
|
||||||
|
|
||||||
We added an Edit button whose click event is bound to an expression
|
The HTML includes an _Edit_ button whose click event is bound to an expression
|
||||||
that clears the `submitted` flag.
|
that clears the `submitted` flag.
|
||||||
|
|
||||||
When we click it, this block disappears and the editable form reappears.
|
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.
|
That's as much drama as we can muster for now.
|
||||||
|
|
||||||
|
@ -661,19 +642,21 @@ figure.image-display
|
||||||
:marked
|
:marked
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
The Angular form techniques discussed in this guide take
|
The Angular form discussed in this guide takes advantage of the following
|
||||||
advantage of the following framework features to provide support for data modification, validation and more:
|
framework features to provide support for data modification, validation, and more:
|
||||||
|
|
||||||
- An Angular HTML form template.
|
- An Angular HTML form template.
|
||||||
- A form component class with a `Component` decorator.
|
- A form component class with a `@Component` decorator.
|
||||||
- Handling form submission by binding to the `NgForm.ngSubmit` event property.
|
- Handling form submission by binding to the `NgForm.ngSubmit` event property.
|
||||||
- Template reference variables such as `#heroForm`, `#name` and `#power`.
|
- Template reference variables such as `#heroForm` and `#name`.
|
||||||
- The `[(ngModel)]` syntax and a `name` attribute for two-way data binding, validation and change tracking.
|
- `[(ngModel)]` syntax for two-way data binding.
|
||||||
|
- The use of `name` attributes for validation and form element change tracking.
|
||||||
- The reference variable’s `valid` property on input controls to check if a control is valid and show/hide error messages.
|
- The reference variable’s `valid` property on input controls to check if a control is valid and show/hide error messages.
|
||||||
- Controlling the submit button's enabled state by binding to `NgForm` validity.
|
- Controlling the submit button's enabled state by binding to `NgForm` validity.
|
||||||
- Custom CSS classes that provide visual feedback to users about invalid controls.
|
- Custom CSS classes that provide visual feedback to users about invalid controls.
|
||||||
|
|
||||||
Our final project folder structure should look like this:
|
Our final project folder structure should look like this:
|
||||||
|
|
||||||
.filetree
|
.filetree
|
||||||
.file angular-forms
|
.file angular-forms
|
||||||
.children
|
.children
|
||||||
|
@ -690,7 +673,7 @@ figure.image-display
|
||||||
.file package.json
|
.file package.json
|
||||||
.file tsconfig.json
|
.file tsconfig.json
|
||||||
:marked
|
:marked
|
||||||
Here’s the final version of the source:
|
Here’s the code for the final version of the application:
|
||||||
|
|
||||||
+makeTabs(
|
+makeTabs(
|
||||||
`forms/ts/app/hero-form.component.ts,
|
`forms/ts/app/hero-form.component.ts,
|
||||||
|
@ -710,4 +693,3 @@ figure.image-display
|
||||||
main.ts,
|
main.ts,
|
||||||
index.html,
|
index.html,
|
||||||
forms.css`)
|
forms.css`)
|
||||||
:marked
|
|
||||||
|
|
Loading…
Reference in New Issue