parent
347c79c514
commit
f7b2913231
|
@ -184,7 +184,7 @@
|
||||||
<input type="text" class="form-control" required
|
<input type="text" class="form-control" required
|
||||||
[(ng-model)]="model.name"
|
[(ng-model)]="model.name"
|
||||||
ng-control="name" #spy >
|
ng-control="name" #spy >
|
||||||
TODO: remove this: {{spy.className}}
|
<br>TODO: remove this: {{spy.className}}
|
||||||
<!-- #enddocregion ng-control-2 -->
|
<!-- #enddocregion ng-control-2 -->
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
// #docplaster
|
// #docplaster
|
||||||
// #docregion
|
// #docregion
|
||||||
// #docregion first, final
|
// #docregion first, final
|
||||||
import {Component, CORE_DIRECTIVES, FORM_DIRECTIVES} from 'angular2/angular2';
|
import {Component} from 'angular2/angular2';
|
||||||
|
|
||||||
import { Hero } from './hero';
|
import { Hero } from './hero';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hero-form',
|
selector: 'hero-form',
|
||||||
templateUrl: 'app/hero-form.component.html',
|
templateUrl: 'app/hero-form.component.html'
|
||||||
// #docregion directives
|
|
||||||
directives: [CORE_DIRECTIVES, FORM_DIRECTIVES]
|
|
||||||
// #enddocregion
|
|
||||||
})
|
})
|
||||||
export class HeroFormComponent {
|
export class HeroFormComponent {
|
||||||
|
|
||||||
|
|
|
@ -18,29 +18,29 @@ include ../../../../_includes/_util-fns
|
||||||
|
|
||||||
We will build a simple form from scratch, one step at a time. Along the way we'll learn
|
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
|
- 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-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 `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 we can use them to provide strong visual feedback
|
- The special CSS classes that `ng-control` adds to form controls and how we can use them to provide strong visual feedback
|
||||||
|
|
||||||
- how to display validation errors to users and enable/disable form controls
|
- How to display validation errors to users and enable/disable form controls
|
||||||
|
|
||||||
- how to share information across controls with template local variables
|
- How to share information across controls with template local variables
|
||||||
|
|
||||||
.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 chapter.
|
the form-specific directives and techniques described in this chapter.
|
||||||
.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 chapter.
|
That's not the only way to create a form but it's the way we'll cover in this chapter.
|
||||||
: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 $mdash; login forms, contact forms ... pretty much any business forms.
|
||||||
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
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.
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ include ../../../../_includes/_util-fns
|
||||||
We'll discuss and learn to build the following template-driven form:
|
We'll discuss and learn to build the following template-driven form:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/hf-1.png" alt="Clean Form")
|
img(src="/resources/images/devguide/forms/hf-1.png" width="400px" alt="Clean Form")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
||||||
|
@ -61,7 +61,7 @@ figure.image-display
|
||||||
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/hf-2.png" alt="Invalid, Name Required")
|
img(src="/resources/images/devguide/forms/hf-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.
|
||||||
|
@ -95,20 +95,23 @@ figure.image-display
|
||||||
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 called `hero.ts` and give it the following class definition:
|
Create a new file in the app folder called `hero.ts` and give it the following class definition:
|
||||||
|
|
||||||
+makeExample('forms/ts/src/app/hero.ts')
|
+makeExample('forms/ts/src/app/hero.ts', null, '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.
|
||||||
|
|
||||||
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 like this:
|
assigns the parameter’s value to that field automatically when we create new heroes.
|
||||||
|
|
||||||
|
The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`.
|
||||||
|
|
||||||
|
We can create a new hero like this:
|
||||||
```
|
```
|
||||||
let myHero = new Hero(42, 'SkyDog', 'Fetch any object at any distance', 'Leslie Rollover');
|
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"
|
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
|
||||||
```
|
```
|
||||||
The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`.
|
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
|
@ -120,17 +123,14 @@ figure.image-display
|
||||||
|
|
||||||
Create a new file called `hero-form.component.ts` and give it the following definition:
|
Create a new file called `hero-form.component.ts` and give it the following definition:
|
||||||
|
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.ts', 'first')
|
+makeExample('forms/ts/src/app/hero-form.component.ts', 'first', 'app/hero-form.component.ts (v.1)')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
There’s nothing special about this component, 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.
|
||||||
nothing form-specific about it ... except, perhaps, the tell-tale `FORM_DIRECTIVES` import.
|
|
||||||
|
|
||||||
Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters
|
Understanding this component requires only the Angular 2 concepts we’ve learned in previous chapters
|
||||||
|
|
||||||
1. We import a standard set of symbols from the Angular library.
|
1. We import the `Component` decorator from the Angular library as we usually do.
|
||||||
We don't have a template yet but we usually import `CORE_DIRECTIVES` and it doesn't surprise us to
|
|
||||||
import something called `FORM_DIRECTIVES`, given that we'll be writing a form
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
@ -144,15 +144,17 @@ figure.image-display
|
||||||
1. We threw in a `diagnostic` property at the end 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.
|
||||||
|
|
||||||
We may wonder why we aren't writing the template inline in the component file as we have often done
|
Why don't we write the template inline in the component file as we often do
|
||||||
elsewhere in the Developer Guide.
|
elsewhere in the Developer Guide?
|
||||||
|
|
||||||
There is no “right” answer for all occasions. We kind of like inline templates when they are short.
|
There is no “right” answer for all occasions. We like inline templates when they are short.
|
||||||
Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to
|
Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to
|
||||||
write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code.
|
write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code.
|
||||||
We also like short files with a clear and obvious purpose like this one.
|
We also like short files with a clear and obvious purpose like this one.
|
||||||
|
|
||||||
We made a good choice to put the HTML template elsewhere. Let's write it.
|
We made a good choice to put the HTML template elsewhere.
|
||||||
|
We'll write that template in a moment. Before we do, we'll take a step back
|
||||||
|
and revise the `app.ts` to make use of our new `HeroFormComponent`.
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
|
@ -161,7 +163,7 @@ figure.image-display
|
||||||
`app.ts` is the application's root component. It will host our new `HeroFormComponent`.
|
`app.ts` 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/src/app/app.ts')
|
+makeExample('forms/ts/src/app/app.ts', null, 'app.ts')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
|
@ -181,43 +183,41 @@ figure.image-display
|
||||||
|
|
||||||
Create a new template file called `hero-form.component.html` and give it the following definition:
|
Create a new template file called `hero-form.component.html` and give it the following definition:
|
||||||
|
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'start')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'start', 'app/hero-form.component.html (v.1)')
|
||||||
|
|
||||||
: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
|
||||||
opening them up for user input in input boxes.
|
opening them up for user input in input boxes.
|
||||||
|
|
||||||
The "Name" `<input>` control has the HTML5 `required` attribute;
|
The *Name* `<input>` control has the HTML5 `required` attribute;
|
||||||
the "Alter Ego" `<input>` control does not because `alterEgo` is optional.
|
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'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.
|
**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 CSS Bootstrap. Purely cosmetic.
|
The `container`,`form-group`, `form-control`, and `btn` classes
|
||||||
|
come from [Twitter Boostrap](http://getbootstrap.com/css/). Purely cosmetic.
|
||||||
We're using Bootstrap to gussy up our form.
|
We're using Bootstrap to gussy up our form.
|
||||||
Hey, what's a form without a little style!
|
Hey, what's a form without a little style!
|
||||||
|
|
||||||
.l-sub-section
|
|
||||||
:marked
|
|
||||||
Since we're using [CSS Boostrap](http://getbootstrap.com/css/).
|
|
||||||
now might be a good time to install it into our project.
|
|
||||||
We can do that with npm.
|
|
||||||
|
|
||||||
Open a terminal window and enter the command:
|
|
||||||
code-example(language="html" escape="html").
|
|
||||||
npm install bootstrap
|
|
||||||
:marked
|
|
||||||
<br>Open the `index.html` and add the following line wherever we like to put our CSS
|
|
||||||
+makeExample('forms/ts/src/index.html', 'bootstrap')(format=".")
|
|
||||||
|
|
||||||
.callout.is-important
|
.callout.is-important
|
||||||
header Angular Forms Does 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. We are welcome to use the CSS library we choose
|
the styles of any external library. Angular apps can use any CSS library
|
||||||
... or none at all.
|
... or none at all.
|
||||||
|
|
||||||
|
:marked
|
||||||
|
Let's add the stylesheet.
|
||||||
|
|
||||||
|
ol
|
||||||
|
li Open a terminal window and enter the command:
|
||||||
|
code-example(language="html" escape="html").
|
||||||
|
npm install bootstrap --save
|
||||||
|
li Open <code>index.html</code> and add the following link to the <code><head></code>.
|
||||||
|
+makeExample('forms/ts/src/index.html', 'bootstrap')(format=".")
|
||||||
|
:marked
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Add Powers with ***ng-for**
|
## Add Powers with ***ng-for**
|
||||||
|
@ -226,10 +226,10 @@ figure.image-display
|
||||||
|
|
||||||
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 we might have seen before in the ["Displaying Data"](./displaying-data.html) chapter.
|
a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter.
|
||||||
|
|
||||||
Add the following HTML *immediately below* the "Alter Ego" group.
|
Add the following HTML *immediately below* the *Alter Ego* group.
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'powers')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
We are repeating the `<options>` tag for each power in the list of Powers.
|
We are repeating the `<options>` tag for each power in the list of Powers.
|
||||||
|
@ -239,17 +239,16 @@ figure.image-display
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Two-way data binding with ***ng-model**
|
## Two-way data binding with ***ng-model**
|
||||||
We might be disappointed if we ran the app right now.
|
Running the app right now would be disappointing.
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/hf-3.png" alt="Early form with no binding")
|
img(src="/resources/images/devguide/forms/hf-3.png" width="400px" alt="Early form with no binding")
|
||||||
:marked
|
:marked
|
||||||
We quickly realize that we are not binding to the `Hero` yet.
|
We don't see hero data because we are not binding to the `Hero` yet.
|
||||||
We know how to do that from earlier chapters.
|
We know how to do that from earlier chapters.
|
||||||
We learned show data on screen with a Property Binding in "[Displaying Data](./displaying-data.html)".
|
[Displaying Data](./displaying-data.html) taught us Property Binding.
|
||||||
We learned 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 extract values from the screen
|
Event Binding and how to update a component property with the displayed value.
|
||||||
in "[User Input](./user-input.html)".
|
|
||||||
|
|
||||||
Now we need to display, listen, and extract at the same time.
|
Now we need to display, listen, and extract at the same time.
|
||||||
|
|
||||||
|
@ -270,54 +269,48 @@ figure.image-display
|
||||||
:marked
|
:marked
|
||||||
Focus on the binding syntax: `[(ng-model)]="..."`.
|
Focus on the binding syntax: `[(ng-model)]="..."`.
|
||||||
|
|
||||||
If we ran the app right now and started typing in the "Name" input box,
|
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
|
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" alt="ng-model in action")
|
img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ng-model in action")
|
||||||
:marked
|
:marked
|
||||||
The diagnostic is evidence that we really are flowing values from the input box to the model and
|
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!**
|
back again. **That's two-way data binding!**
|
||||||
|
|
||||||
Let's add similar `[(ng-model)]` bindings to "Alter Ego" and "Hero Power".
|
Let's add similar `[(ng-model)]` bindings 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 we are in fact two-way data binding *to 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 `[(ng-model)]` bindings that
|
After revision the core of our form should have three `[(ng-model)]` bindings that
|
||||||
look much like this:
|
look much like this:
|
||||||
|
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-model-2')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-model-2', 'app/hero-form.component.html (excerpt)')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
If we ran the app right now and made a bunch of changes at some point it might look like this.
|
If we ran the app right 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" alt="ng-model in super action")
|
img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ng-model in super action")
|
||||||
:marked
|
:marked
|
||||||
We've changed every Hero model property and the diagnostic near the top of the form
|
The diagnostic near the top of the form
|
||||||
confirms that our changes are reflected in the model.
|
confirms that all of our changes are reflected in the model.
|
||||||
|
|
||||||
** We're done with the diagnostic binding. Delete it now.**
|
** We're done with the diagnostic binding. Delete it now.**
|
||||||
|
|
||||||
.alert.is-helpful
|
|
||||||
:marked
|
|
||||||
Although `NgModel` is officially a "Forms" directive we can use `[(ng-model)]` and two-way binding outside of forms too.
|
|
||||||
:marked
|
|
||||||
## Inside [(ng-model)]
|
|
||||||
Do we *really want* to know? If we're just happy that it works, move on to the next topic in this chapter.
|
|
||||||
|
|
||||||
Otherwise, stick around for this note.
|
|
||||||
|
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
|
### Inside [(ng-model)]
|
||||||
|
*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.
|
The punctuation in the binding syntax, <span style="font-family:courier"><b>[()]</b></span>, is a good clue to what's going on.
|
||||||
|
|
||||||
We write a Property Binding to flow data from the model to a target property on screen.
|
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>.
|
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**.
|
This is a one-way data binding **from the model to the view**.
|
||||||
|
|
||||||
We write an Event Binding to flow data from the target property on screen to the model.
|
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>.
|
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**.
|
This is a one-way data binding in the opposite direction **from the view to the model**.
|
||||||
|
|
||||||
|
@ -331,18 +324,18 @@ figure.image-display
|
||||||
:marked
|
:marked
|
||||||
<br>The Property Binding should feel familiar. The Event Binding might seem strange.
|
<br>The Property Binding should feel familiar. The Event Binding might seem strange.
|
||||||
|
|
||||||
The name `ng-model-change` is not an event we recognize.
|
The `ng-model-change` is not an `<input>` element event.
|
||||||
It is a real event property ... of the `NgModel` directive.
|
It is actually an event property of the `NgModel` directive.
|
||||||
When Angular sees a binding target in the form <span style="font-family:courier">[(abc)]</span>,
|
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.
|
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`.
|
The other oddity is the template expression, `model.name = $event`.
|
||||||
We're used to seeing an `$event` object coming from a DOM 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`
|
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
|
property that returns the input box value when it fires — which is precisely what
|
||||||
we should assign to the model's `name' property.
|
we should assign to the model's `name' property.
|
||||||
|
|
||||||
Nice to know but is it practical? We'd always prefer the `[(ng-model)]`.
|
Nice to know but is it practical? We almost always prefer `[(ng-model)]`.
|
||||||
We might split the binding if we had to do something special in
|
We might split the binding if we had to do something special in
|
||||||
the event handling such as debounce or throttle the key strokes.
|
the event handling such as debounce or throttle the key strokes.
|
||||||
|
|
||||||
|
@ -362,10 +355,8 @@ figure.image-display
|
||||||
The `NgControl` is one of a family of `NgForm` directives that can only be applied to
|
The `NgControl` is one of a family of `NgForm` directives that can only be applied to
|
||||||
a control within a `<form`> tag.
|
a control within a `<form`> tag.
|
||||||
:marked
|
:marked
|
||||||
Our application can ask an `NgControl` instance if
|
Our application can ask an `NgControl` if the user touched the control,
|
||||||
* the user touched the control (`ng-touched` | `ng-untouched`)
|
if the value changed, or if the value became invalid.
|
||||||
* the value changed (`ng-pristine` | `ng-dirty`)
|
|
||||||
* is the value is valid (`ng-valid` | `ng-invalid`)
|
|
||||||
|
|
||||||
`NgControl` doesn't just track state; it updates the control with special
|
`NgControl` doesn't just track state; it updates the control with special
|
||||||
Angular CSS classes from the set we listed above.
|
Angular CSS classes from the set we listed above.
|
||||||
|
@ -373,9 +364,9 @@ figure.image-display
|
||||||
control and make messages appear or disappear.
|
control and make messages appear or disappear.
|
||||||
|
|
||||||
We'll explore those effects soon. Right now
|
We'll explore those effects soon. Right now
|
||||||
we should **add `ng-control`to all three of our form controls**,
|
we should **add `ng-control`to all three form controls**,
|
||||||
starting with the "Name" input box
|
starting with the *Name* input box
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-control-1')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-control-1', 'app/hero-form.component.html (excerpt)')
|
||||||
:marked
|
:marked
|
||||||
Be sure to assign a unique name to each `ng-control` directive.
|
Be sure to assign a unique name to each `ng-control` directive.
|
||||||
|
|
||||||
|
@ -384,34 +375,54 @@ figure.image-display
|
||||||
Angular registers controls under their `ng-control` names
|
Angular registers controls under their `ng-control` names
|
||||||
with the `NgForm`.
|
with the `NgForm`.
|
||||||
We didn't add the `NgForm` directive explicitly but it's here
|
We didn't add the `NgForm` directive explicitly but it's here
|
||||||
and we'll talk it [later in this chapter](#ng-form).
|
and we'll talk about it [later in this chapter](#ng-form).
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
:marked
|
:marked
|
||||||
## Add Custom CSS for Visual Feedback
|
## Add Custom CSS for Visual Feedback
|
||||||
|
|
||||||
`NgControl` doesn't just track state. It updates the control with three classes, one
|
`NgControl` doesn't just track state.
|
||||||
each from the following pairs of Angular form CSS classes.
|
It updates the control with three classes that reflect the state.
|
||||||
* control visited: (`ng-touched` | `ng-untouched`)
|
|
||||||
* value changed: (`ng-pristine` | `ng-dirty`)
|
table
|
||||||
* validity: (`ng-valid` | `ng-invalid`)
|
tr
|
||||||
|
th State
|
||||||
|
th Class if true
|
||||||
|
th Class if false
|
||||||
|
tr
|
||||||
|
td Control has been visited
|
||||||
|
td <code>ng-touched</code>
|
||||||
|
td <code>ng-untouched</code>
|
||||||
|
tr
|
||||||
|
td Control's value has changed
|
||||||
|
td <code>ng-dirty</code>
|
||||||
|
td <code>ng-pristine</code>
|
||||||
|
tr
|
||||||
|
td Control's value is valid
|
||||||
|
td <code>ng-valid</code>
|
||||||
|
td <code>ng-invalid</code>
|
||||||
|
:marked
|
||||||
Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy**
|
Let's add a temporary [local template variable](./template-syntax.html#local-vars) named **spy**
|
||||||
to the "Name" `<input>` tag and use the spy to display those classes with an interpolation binding.
|
to the "Name" `<input>` tag and use the spy to display those classes.
|
||||||
|
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-control-2')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-control-2')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
If we ran the app, focused our attention on the "Name" input box, and followed the next four steps *precisely*
|
Now run the app and focus on the *Name* input box.
|
||||||
|
Follow the next four steps *precisely*
|
||||||
|
|
||||||
1. Look but don't touched
|
1. Look but don't touched
|
||||||
1. Click in the input box, then click outside the text input box
|
1. Click in the input box, then click outside the text input box
|
||||||
1. Add slashes to the end of the name
|
1. Add slashes to the end of the name
|
||||||
1. Erase the name
|
1. Erase the name
|
||||||
|
|
||||||
... we would see the following four sets of class names and their transitions:
|
The actions and effects are as follows:
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/ng-control-class-changes.png" alt="Invalid Form")
|
img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition")
|
||||||
|
:marked
|
||||||
|
We should be able to see the following four sets of class names and their transitions:
|
||||||
|
figure.image-display
|
||||||
|
img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a
|
The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a
|
||||||
|
@ -420,18 +431,18 @@ figure.image-display
|
||||||
We realize we can do both at the same time with a colored bar on the left of the input box:
|
We realize we can do both at the same time with a colored bar on the left of the input box:
|
||||||
|
|
||||||
figure.image-display
|
figure.image-display
|
||||||
img(src="/resources/images/devguide/forms/validity-required-indicator.png" 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 `styles.css` file
|
We achieve this effect by adding two styles to a new `styles.css` file
|
||||||
that we add to our project as a sibling to `index.html`.
|
that we add to our project as a sibling to `index.html`.
|
||||||
|
|
||||||
+makeExample('forms/ts/src/styles.css')
|
+makeExample('forms/ts/src/styles.css',null,'styles.css')
|
||||||
:marked
|
:marked
|
||||||
These styles select for the two Angular validity classes and the HTML 5 "required" attribute.
|
These styles select for the two Angular validity classes and the HTML 5 "required" attribute.
|
||||||
|
|
||||||
We update the `<head>` of the `index.html` to include this style sheet.
|
We update the `<head>` of the `index.html` to include this style sheet.
|
||||||
+makeExample('forms/ts/src/index.html', 'styles')(format=".")
|
+makeExample('forms/ts/src/index.html', 'styles', 'index.html (excerpt)')(format=".")
|
||||||
:marked
|
:marked
|
||||||
## Show and Hide Validation Error messages
|
## Show and Hide Validation Error messages
|
||||||
|
|
||||||
|
@ -443,15 +454,19 @@ figure.image-display
|
||||||
|
|
||||||
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" alt="Name required")
|
img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required")
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
To achieve this effect we extend the `<input>` tag with
|
To achieve this effect we extend the `<input>` tag with
|
||||||
1. a [local template variable](./template-syntax.html#local-vars)
|
1. a [local template variable](./template-syntax.html#local-vars)
|
||||||
1. the "*is required*" message in a nearby `<div>` which we'll display only if the control is invalid.
|
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 how we do it for the *name* input box:
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'name-with-error-msg')
|
-var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm };
|
||||||
|
+makeExample('forms/ts/src/app/hero-form.component.html',
|
||||||
|
'name-with-error-msg',
|
||||||
|
'app/hero-form.component.html (excerpt)',
|
||||||
|
stylePattern)
|
||||||
:marked
|
:marked
|
||||||
We initialized the template local variable with the word "form" (`#name="form"`)
|
We initialized the template local variable with the word "form" (`#name="form"`)
|
||||||
|
|
||||||
|
@ -465,16 +480,13 @@ figure.image-display
|
||||||
<a id="ng-form"></a>
|
<a id="ng-form"></a>
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
|
### The NgForm directive
|
||||||
Recall from the previous section that `ng-control` registered this input box with the
|
Recall from the previous section that `ng-control` registered this input box with the
|
||||||
`NgForm` directive as "name".
|
`NgForm` directive as "name".
|
||||||
|
|
||||||
We didn't add the **[`NgForm`](../api/core/NgForm-class.html) directive** explicitly.
|
We didn't add the **[`NgForm`](../api/core/NgForm-class.html) directive** explicitly.
|
||||||
Angular added it surreptiously, wrapping it around the `<form>` element when we
|
Angular added it surreptiously, wrapping it around the `<form>` element
|
||||||
told the `HeroFormComponent` to use the `FORM_DIRECTIVES` like this
|
|
||||||
|
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.ts', 'directives')
|
|
||||||
<br>
|
|
||||||
:marked
|
|
||||||
The `NgForm` directive supplements the `form` element with additional features.
|
The `NgForm` directive supplements the `form` element with additional features.
|
||||||
It collects `Controls` (elements identified by an `ng-control` directive)
|
It collects `Controls` (elements identified by an `ng-control` directive)
|
||||||
and monitors their properties including their validity.
|
and monitors their properties including their validity.
|
||||||
|
@ -483,11 +495,11 @@ figure.image-display
|
||||||
|
|
||||||
In this example, we are pulling the "name" control out of its `controls` collection
|
In this example, we are pulling the "name" control out of its `controls` collection
|
||||||
and assigning it to the template local variable so that we can
|
and assigning it to the template local variable so that we can
|
||||||
access the control's properties ... such as the control's own `valid` property.
|
access the control's properties — such as the control's own `valid` property.
|
||||||
:marked
|
:marked
|
||||||
The "AlterEgo" is optional so we can leave that be.
|
The Hero *Alter Ego* is optional so we can leave that be.
|
||||||
|
|
||||||
"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 value.
|
||||||
|
@ -496,14 +508,14 @@ figure.image-display
|
||||||
:marked
|
:marked
|
||||||
## Submit the form with **ng-submit**
|
## Submit the form with **ng-submit**
|
||||||
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 meaningless at the moment.
|
A "form submit" is meaningless at the moment.
|
||||||
We'll update the `<form>` tag with another Angular directive, `NgSubmit`,
|
To make it meaningful, we'll update the `<form>` tag with another Angular directive, `NgSubmit`,
|
||||||
and bind it to our `HeroFormComponent.submit()` method with an EventBinding
|
and bind it to our `HeroFormComponent.submit()` method with an EventBinding
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-submit')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'ng-submit')(format=".")
|
||||||
|
|
||||||
: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
|
||||||
|
@ -521,10 +533,8 @@ figure.image-display
|
||||||
If we run the application now, we find that the button is enabled.
|
If we run the application now, we find that the button is enabled.
|
||||||
It doesn't do anything useful yet but it's alive.
|
It doesn't do anything useful yet but it's alive.
|
||||||
|
|
||||||
Now if we delete the "Name", we violate the "required" rule which
|
Now if we delete the *Name*, we violate the "required" rule which
|
||||||
is duely noted in our error message.
|
is duely noted in our error message. The Submit button is also disabled.
|
||||||
|
|
||||||
Check the "Submit" button. It should be 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?
|
||||||
|
@ -535,7 +545,7 @@ figure.image-display
|
||||||
|
|
||||||
.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
|
||||||
|
@ -543,40 +553,40 @@ figure.image-display
|
||||||
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 we're not interested, we can skip to the chapter's conclusion
|
If you're not interested, you can skip to the chapter's conclusion
|
||||||
and not miss a thing.
|
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.
|
||||||
|
|
||||||
Start by wrapping the form in a `<div>` and binding
|
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/src/app/hero-form.component.html', 'edit-div')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'edit-div', 'app/hero-form.component.html (excerpt)')
|
||||||
|
|
||||||
: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
|
the `submitted` property is false until we submit the form,
|
||||||
... as this fragment from the `HeroFormComponent` reminds us:
|
as this fragment from the `HeroFormComponent` reminds us:
|
||||||
|
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.ts', 'submitted')
|
+makeExample('forms/ts/src/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 we need to show something else while the form is in the submitted state.
|
||||||
Add the following block of HTML below the `<div>` wrapper we just wrote:
|
Add the following block of HTML below the `<div>` wrapper we just wrote:
|
||||||
+makeExample('forms/ts/src/app/hero-form.component.html', 'submitted')
|
+makeExample('forms/ts/src/app/hero-form.component.html', 'submitted', 'app/hero-form.component.html (excerpt)')
|
||||||
|
|
||||||
: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 slug of HTML only appears while the component is in the submitted state.
|
||||||
|
|
||||||
There's an "Edit" button whose click event we bound to an expression
|
We added an Edit button whose click event is bound to an expression
|
||||||
that clears the `submitted` flag.
|
that clears the `submitted` flag.
|
||||||
|
|
||||||
Click it and this block disappears and the editable form reappears.
|
When we click it, 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.
|
||||||
|
|
||||||
|
@ -596,7 +606,7 @@ figure.image-display
|
||||||
- Property Binding to disable the submit button when the form is invalid.
|
- 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.
|
- Custom CSS classes that provide visual feedback to users about required invalid controls.
|
||||||
|
|
||||||
Here’s the final version of the application includes all of these framework features:
|
Here’s the final version of the application:
|
||||||
|
|
||||||
+makeTabs(
|
+makeTabs(
|
||||||
`forms/ts/src/app/hero-form.component.html,
|
`forms/ts/src/app/hero-form.component.html,
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 221 KiB |
Loading…
Reference in New Issue