commit 17c7e154672dbf5feeeb971ed3b594d318703748 Author: Filipe Silva <filipematossilva@gmail.com> Date: Mon Nov 14 23:11:37 2016 +0000 chore: update to 2.2.0 (#2797) commit 1e5facfb980b85313d51559b3272950f662ce20c Merge: 5c4cc9a db0fac9 Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 12:44:51 2016 -0800 Merge pull request #2799 from IdeaBlade/docs-changelog-2.2.0 chore(changelog): update docs changelog for Ng v.2.2.0 commit 5c4cc9a3c8b2718b71ab7c3d1b5dd02bfdd5f1fc Merge: 43457e9 1afe5dc Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 12:43:09 2016 -0800 docs(router): Updated usage of observables in router tutorial and developer guide (#2696) Moved route configuration into separate variable for consistency Added async pipe to handle subscriptions for list items commit 43457e9765c63b3889ab449959fe295327390315 Merge: a423a5a 2649647 Author: Jesús Rodríguez <Foxandxss@gmail.com> Date: Mon Nov 14 21:38:49 2016 +0100 chore: add upgrade/static to API reference (#2755) commit 1afe5dc97d457e045adb6c0e03d27023aa97eea5 Author: Brandon Roberts <robertsbt@gmail.com> Date: Sat Oct 29 16:08:54 2016 -0500 docs(router): Updated usage of observables in router tutorial and developer guide Moved route configuration into separate variable for consistency Added async pipe to handle subscriptions for list items commit db0fac94c980c99d9575a90d4b6bb15f572f9161 Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 10:48:09 2016 -0800 chore(changelog): update docs changelog for Ng v.2.2.0 commit a423a5abc704280794b6dea69cf1c9e76bbc9da1 Author: ikilled <ikilled@users.noreply.github.com> Date: Mon Nov 14 18:39:46 2016 +0000 clicking on Books & Training sends user to Education (#2701) When user clicks in Books & Training item in footer the website should take them to Education section (anchor) in the middle of the page, not to the top where Development section is. commit d63b1ccea36f4cf52333c846184b8e6876284a0a Merge: f627706 8508140 Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 10:35:48 2016 -0800 docs(router): fixed verbiage about router-outlet (#2746) commit f627706779c418ca8357aacd52aba5b5b7d05cb0 Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 10:26:13 2016 -0800 docs(cookbook-aot-compiler): improve Ahead-of-Time compilation cookbook (#2798) closes #2790 commit 75464d585cac944e3cffb4401663e4c1185b7cb5 Merge: 78e2584 02f5559 Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 09:34:36 2016 -0800 Merge pull request #2794 from IdeaBlade/chalin-guide-misc-fix-1113 docs: guide/index misc Jade fixes commit 78e25840b229b0fe345feacf786f34e62b2cb963 Merge: 182493f 85062c4 Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 09:33:55 2016 -0800 Merge pull request #2795 from IdeaBlade/chalin-util-js-getExampleName-1114 chore(util.js): getExampleName - support optional .html suffix commit 182493f8779fa37422dd942813fc5681dbdac55f Merge: 9e9666b 53f5538 Author: Ward Bell <wardbell@hotmail.com> Date: Mon Nov 14 09:28:40 2016 -0800 feat(cb-ts-to-js): add es6 examples (#2606) * feat(cb-ts-to-js): add es6 examples update docshredder to shred .es6 optimized focus gulp task convert imports and metadate sections add DI section add host and query metadata section add intro fix capitalization and grammar * docs(ts-to-js): ward's edits (incomplete) * docs(ts-to-js): add separate template files for some components * docs(cb-ts-to-js): refactor sample code commit 53f5538859c4d243552d288872e8c8fc8156f446 Author: Ward Bell <wardbell@hotmail.com> Date: Sun Nov 13 14:09:28 2016 -0800 docs(cb-ts-to-js): refactor sample code commit 9e9666b2cc3ce57cac172894e935f0c6b9f7d6f4 Author: Patrice Chalin <chalin@users.noreply.github.com> Date: Mon Nov 14 08:34:10 2016 -0800 docs(template-syntax/dart): updates to match TS (#2751) * docs(template-syntax): refresh _cache * docs(template-syntax/dart): updates to match TS - Propagates TS-side changes: - update #2639 - new two-way binding section, and - fix #2687 - invalid attr syntax - Fixes - #1898 - currency symbols - #2748 - Dart template-syntax e2e is failing - #2749 - deprecated `[className]` * updated _cache file following Kathy's post-review edits * Post Ward's review w/ cache updated - Keep `my-` and `my` prefixes on selectors (for components and directives, respectively). - Drop `my-` from file names. - Drop `My` as component class prefix. commit 5dcffd69dc09b48d99af4acd0dd06b029416e103 Author: Patrice Chalin <pchalin@gmail.com> Date: Sun Nov 13 19:37:13 2016 -0800 docs: dart glossary - fix misnamed Jade block commit 6680acc513a0178bb522f5bc99d66f366542d33c Merge: 14db838 3b03573 Author: Kathy Walrath <kathyw@google.com> Date: Mon Nov 14 08:31:11 2016 -0800 docs(toh): avoid dup header title (#2796) * remove redundant headings * update _cache * misc: make block comment a Jade comment (This prevents the text from appearing in the generated HTML as an HTML comment.) commit 3b03573f340cc7fc43e2642bdf52fb1bae61aff0 Author: Patrice Chalin <pchalin@gmail.com> Date: Mon Nov 14 05:30:05 2016 -0800 misc: make block comment a Jade comment (This prevents the text from appearing in the generated HTML as an HTML comment.) commit 470426d5e03829442449cb59c0528811c6011c37 Author: Patrice Chalin <pchalin@gmail.com> Date: Mon Nov 14 05:25:36 2016 -0800 update _cache commit c12d75a477a1e33ea37886c0bd5942ddb9c955b3 Author: Patrice Chalin <pchalin@gmail.com> Date: Mon Nov 14 05:23:35 2016 -0800 remove redundant headings commit 85062c47cac44d63b06b97ae86b2a4f596889ad6 Author: Patrice Chalin <pchalin@gmail.com> Date: Mon Nov 14 04:56:12 2016 -0800 chore(util.js): getExampleName - support optional .html suffix commit 02f55592b232618407a3a8fce70845589ee78978 Author: Patrice Chalin <pchalin@gmail.com> Date: Mon Nov 14 04:29:17 2016 -0800 docs: guide/index misc Jade fixes - Eliminate use of deprecated `clear=“all”` in `<br>`. - No need for local `langName`; use global `_Lang` var instead. - Remove duplicate id `learning-path`. commit 14db838f8b76309597140d9857acb92c76c864f6 Author: Naomi Black <naomitraveller@gmail.com> Date: Sun Nov 13 21:48:52 2016 -0500 news(nov): Some news and a blog post update commit eff32ecbdd5a52d2240afd5ad4e14bdab21c9a8f Author: Naomi Black <naomitraveller@gmail.com> Date: Sun Nov 13 21:48:37 2016 -0500 chore(bios): update some bios for leads commit 3ee36fbba211d115414d322b17a67626ed90d450 Author: koyner <markburrett@gmail.com> Date: Sun Nov 13 22:59:59 2016 +0100 docs(forms): grammar fix (#2764) commit b11438f211ed7009335bc18f5cbf2950fc4014fa Author: Ward Bell <wardbell@hotmail.com> Date: Fri Nov 11 19:44:00 2016 -0800 docs(ts-to-js): add separate template files for some components commit 33b61977b2cd0bc35b257ce526d5dd2fd460cd10 Author: Ward Bell <wardbell@hotmail.com> Date: Thu Nov 3 01:37:55 2016 -0700 docs(ts-to-js): ward's edits (incomplete) commit 12eb19fa3c34bb48b28da8bd33d94d7cc0526cf5 Author: Filipe Silva <filipematossilva@gmail.com> Date: Thu Oct 13 17:59:00 2016 +0100 feat(cb-ts-to-js): add es6 examples update docshredder to shred .es6 optimized focus gulp task convert imports and metadate sections add DI section add host and query metadata section add intro fix capitalization and grammar commit 64a8754386c0aa727e61d140d86bd31b6891ea99 Author: Patrice Chalin <chalin@users.noreply.github.com> Date: Thu Nov 10 20:01:36 2016 -0800 example(template-syntax): follow style-guide and other updates (#2750) commit 7619cdf4a4ac592d74c25a372234c2d3964355dc Author: Jesús Rodríguez <Foxandxss@gmail.com> Date: Thu Nov 10 23:47:30 2016 +0100 chore: ability to open a plunker on a specific file (#2778) commit 0161d9db39b08be6a29e5a00b362c1eff12bf6b9 Author: Filipe Silva <filipematossilva@gmail.com> Date: Thu Nov 10 22:45:22 2016 +0000 chore: ignore debug.log file (#2785) This file is generated when running `gulp e2e` and often enough committed by mistake. /cc @foxandxss commit f92983cc6f7a895b751a6023791a283bb2e41073 Author: Jesús Rodríguez <Foxandxss@gmail.com> Date: Thu Nov 10 23:44:51 2016 +0100 docs(ngmodule): fix plunkers (#2786) commit 03db4bbc4873d8b68d08ef32bfa1b2b43cf46224 Author: Martin Eckardt <m.eckardt@outlook.com> Date: Wed Nov 9 17:43:40 2016 +0100 docs(a1-a2): fix link to Filter/Pipes (#2770) commit 60565a5cf1dfa7e43cb18b448c7d61c5f765f0de Author: Pavol Pitonak <pavol@pitonak.com> Date: Wed Nov 9 17:42:57 2016 +0100 docs(testing): configureTestModule -> configureTestingModule (#2767) commit ec471974a776dc208b2c56115cf1105fa39c8f88 Author: Catalin Zalog <xxxxxcata@yahoo.com> Date: Wed Nov 9 18:41:56 2016 +0200 docs(style-guide): fix missing *.ts (#2763) commit 234e468d5d6a11782e306901f80546d8989f17b3 Author: Patrice Chalin <pchalin@gmail.com> Date: Tue Nov 8 08:21:02 2016 -0800 docs: intra-site links should be relative Contributes to #2772. commit 6b37da78e4901a57b6ebf77720ec4796f2bb5f5f Author: Patrice Chalin <pchalin@gmail.com> Date: Tue Nov 8 09:27:10 2016 -0800 docs(forms/dart): remove mention of FORM_DIRECTIVES Fixes #2752 commit c24dd074a6acbceb87f8e973969c9bd94f769d8f Author: Patrice Chalin <chalin@users.noreply.github.com> Date: Tue Nov 8 14:48:03 2016 -0800 docs(toh-5/dart): use routerLink in dashboard (#2744) * docs(toh-5/dart): use routerLink in dashboard * minor edits to TS jade * remove dart/toh-pt5 from bad-code-excerpt-skip-patterns commit 2808878c36d7429611e4f0584323e99c9b83219e Author: Patrice Chalin <pchalin@gmail.com> Date: Tue Nov 8 07:41:27 2016 -0800 chore(deploy): don't name project in firebase deploy Naming the project would sometimes cause gulp to report `An unexpected error has occurred` with exit code 2. commit 2649647ecb14a28932405d6b0bc22afd1f931089 Author: Jesus Rodriguez <Foxandxss@gmail.com> Date: Sat Nov 5 00:37:47 2016 +0100 chore: add upgrade/static to API reference commit 850814097f9908781ce954d13878d20eed779c87 Merge: 37f93bc b1c2c27 Author: Adrian Irwin <mr.irwin@gmail.com> Date: Thu Nov 3 17:27:43 2016 -0700 Merge branch 'router' of https://github.com/adrianirwin/angular.io into router commit 37f93bc0cbc2a766846b7715b1d8ddeeec43de39 Author: Adrian Irwin <mr.irwin@gmail.com> Date: Thu Nov 3 17:25:55 2016 -0700 docs(router): fixed verbiage and example of how routed views are related to the router outlet commit b1c2c27d365186d4231a470945a0975482df58e9 Author: Adrian Irwin <adrianirwin@kpmg.com> Date: Thu Nov 3 16:57:56 2016 -0700 docs(router): fixed verbiage and example of how routed views are related to the router outlet
734 lines
32 KiB
Plaintext
734 lines
32 KiB
Plaintext
include ../_util-fns
|
||
|
||
:marked
|
||
We’ve all used a form to log in, submit a help request, place an order, book a flight,
|
||
schedule a meeting and perform countless other data entry tasks.
|
||
Forms are the mainstay of business applications.
|
||
|
||
Any seasoned web developer can slap together an HTML form with all the right tags.
|
||
It's more challenging to create a cohesive data entry experience that guides the
|
||
user efficiently and effectively through the workflow behind the form.
|
||
|
||
*That* takes design skills that are, to be frank, well out of scope for this chapter.
|
||
|
||
It also takes framework support for
|
||
**two-way data binding, change tracking, validation, and error handling**
|
||
... which we shall cover in this chapter on Angular forms.
|
||
|
||
We will build a simple form from scratch, one step at a time. Along the way we'll learn how to
|
||
|
||
- build an Angular form with a component and template
|
||
|
||
- two-way data bind with `[(ngModel)]` syntax for reading and writing values to input controls
|
||
|
||
- track the change state and validity of form controls using `ngModel` in combination with a form
|
||
|
||
- 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>.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Template-Driven Forms
|
||
|
||
Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with
|
||
the form-specific directives and techniques described in this chapter.
|
||
.l-sub-section
|
||
:marked
|
||
That's not the only way to create a form but it's the way we'll cover in this chapter.
|
||
:marked
|
||
We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms.
|
||
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
||
conditionally enable or disable specific controls, trigger built-in visual feedback, and much more.
|
||
|
||
It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd
|
||
otherwise wrestle with ourselves.
|
||
|
||
We'll discuss and learn to build the following template-driven form:
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
||
|
||
:marked
|
||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
||
heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis!
|
||
|
||
Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot.
|
||
|
||
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="Invalid, Name Required")
|
||
|
||
:marked
|
||
Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red.
|
||
|
||
.l-sub-section
|
||
p We'll customize the colors and location of the "required" bar with standard CSS.
|
||
|
||
:marked
|
||
We will build this form in the following sequence of small steps
|
||
|
||
1. Create the `Hero` model class
|
||
1. Create the component that controls the form
|
||
1. Create a template with the initial form layout
|
||
1. Bind data properties to each form input control with the `ngModel` two-way data binding syntax
|
||
1. Add the `name` attribute to each form input control
|
||
1. Add custom CSS to provide visual feedback
|
||
1. Show and hide validation error messages
|
||
1. Handle form submission with **ngSubmit**
|
||
1. Disable the form’s submit button until the form is valid
|
||
|
||
:marked
|
||
## Setup
|
||
Create a new project folder (`angular-forms`) and follow the steps in the [QuickStart](../quickstart.html).
|
||
|
||
include ../_quickstart_repo
|
||
:marked
|
||
## Create the Hero Model Class
|
||
|
||
As users enter form data, we capture their changes and update an instance of a model.
|
||
We can't layout the form until we know what the model looks like.
|
||
|
||
A model can be as simple as a "property bag" that holds facts about a thing of application importance.
|
||
That describes well our `Hero` class with its three required fields (`id`, `name`, `power`)
|
||
and one optional field (`alterEgo`).
|
||
|
||
Create a new file in the app folder called `hero.ts` and give it the following class definition:
|
||
|
||
+makeExample('forms/ts/app/hero.ts', null, 'app/hero.ts')
|
||
|
||
:marked
|
||
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
||
|
||
The TypeScript compiler generates a public field for each `public` constructor parameter and
|
||
assigns the parameter’s value to that field automatically when we create new heroes.
|
||
|
||
The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`.
|
||
|
||
We can create a new hero like this:
|
||
code-example(format="").
|
||
let myHero = new Hero(42, 'SkyDog',
|
||
'Fetch any object at any distance',
|
||
'Leslie Rollover');
|
||
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
|
||
:marked
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Create a Form component
|
||
|
||
An Angular form has two parts: an HTML-based template and a code-based Component to handle data and user interactions.
|
||
|
||
We begin with the Component because it states, in brief, what the Hero editor can do.
|
||
|
||
Create a new file called `hero-form.component.ts` and give it the following definition:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.ts', 'first', 'app/hero-form.component.ts')
|
||
|
||
:marked
|
||
There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before.
|
||
|
||
Understanding this component requires only the Angular concepts we’ve learned in previous chapters
|
||
|
||
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 `@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 `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.
|
||
Down the road, we can inject a data service to get and save real data
|
||
or perhaps expose these properties as [inputs and outputs](./template-syntax.html#inputs-outputs) for binding to a
|
||
parent component. None of this concerns us now and these future changes won't affect our form.
|
||
|
||
1. We threw in a `diagnostic` property at the end to return a JSON representation of our model.
|
||
It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later.
|
||
|
||
Why don't we write the template inline in the component file as we often do
|
||
elsewhere in the Developer Guide?
|
||
|
||
There is no “right” answer for all occasions. We like inline templates when they are short.
|
||
Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to
|
||
write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code.
|
||
We also like short files with a clear and obvious purpose like this one.
|
||
|
||
We made a good choice to put the HTML template elsewhere.
|
||
We'll write that template in a moment. Before we do, we'll take a step back
|
||
and revise the `app.module.ts` and `app.component.ts` to make use of our new `HeroFormComponent`.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Revise the *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
|
||
and declare the components that belong to this module, such as our `HeroFormComponent`.
|
||
|
||
Because template-driven forms are in their own module, we need to add the `FormsModule` to the array of
|
||
`imports` for our application module before we can use forms.
|
||
|
||
Replace the contents of the "QuickStart" version with the following:
|
||
+makeExample('forms/ts/app/app.module.ts', null, 'app/app.module.ts')
|
||
|
||
:marked
|
||
.l-sub-section
|
||
:marked
|
||
There are three changes:
|
||
|
||
1. We import `FormsModule` and our new `HeroFormComponent`.
|
||
|
||
1. We add the `FormsModule` to the list of `imports` defined in the `ngModule` decorator. This gives our application
|
||
access to all of the template-driven forms features, including `ngModel`.
|
||
|
||
1. We add the `HeroFormComponent` to the list of `declarations` defined in the `ngModule` decorator. This makes
|
||
the `HeroFormComponent` component visible throughout this module.
|
||
|
||
.alert.is-important
|
||
:marked
|
||
If a component, directive, or pipe belongs to a module in the `imports` array, _DON'T_ declare it in the `declarations` array.
|
||
If you wrote it and it should belong to this module, _DO_ declare it in the `declarations` array.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Revise the *app.component.ts*
|
||
|
||
`app.component.ts` is the application's root component. It will host our new `HeroFormComponent`.
|
||
|
||
Replace the contents of the "QuickStart" version with the following:
|
||
+makeExample('forms/ts/app/app.component.ts', null, 'app/app.component.ts')
|
||
|
||
:marked
|
||
.l-sub-section
|
||
:marked
|
||
There is only one change:
|
||
|
||
1. 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.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Create an initial HTML Form Template
|
||
|
||
Create a new template file called `hero-form.component.html` and give it the following definition:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'start', 'app/hero-form.component.html')
|
||
|
||
:marked
|
||
That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and
|
||
opening them up for user input in input boxes.
|
||
|
||
The *Name* `<input>` control has the HTML5 `required` attribute;
|
||
the *Alter Ego* `<input>` control does not because `alterEgo` is optional.
|
||
|
||
We've got a *Submit* button at the bottom with some classes on it for styling.
|
||
|
||
**We are not using Angular yet**. There are no bindings. No extra directives. Just layout.
|
||
|
||
The `container`, `form-group`, `form-control`, and `btn` classes
|
||
come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic.
|
||
We're using Bootstrap to gussy up our form.
|
||
Hey, what's a form without a little style!
|
||
|
||
.callout.is-important
|
||
header Angular Forms Do Not Require A Style Library
|
||
:marked
|
||
Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or
|
||
the styles of any external library. Angular apps can use any CSS library
|
||
... or none at all.
|
||
|
||
:marked
|
||
Let's add the stylesheet.
|
||
|
||
ol
|
||
li Open a terminal window in the application root folder and enter the command:
|
||
code-example(language="html" escape="html").
|
||
npm install bootstrap --save
|
||
li Open <code>index.html</code> and add the following link to the <code><head></code>.
|
||
+makeExample('forms/ts/index.html', 'bootstrap')(format=".")
|
||
:marked
|
||
.l-main-section
|
||
:marked
|
||
## Add Powers with ***ngFor**
|
||
Our hero may choose one super power from a fixed list of Agency-approved powers.
|
||
We maintain that list internally (in `HeroFormComponent`).
|
||
|
||
We'll add a `select` to our
|
||
form and bind the options to the `powers` list using `ngFor`,
|
||
a technique we might have seen before in the [Displaying Data](./displaying-data.html) chapter.
|
||
|
||
Add the following HTML *immediately below* the *Alter Ego* group.
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
:marked
|
||
We are repeating the `<options>` tag for each power in the list of Powers.
|
||
The `p` template input variable is a different power in each iteration;
|
||
we display its name using the interpolation syntax with the double-curly-braces.
|
||
|
||
<a id="ngModel"></a>
|
||
.l-main-section
|
||
:marked
|
||
## Two-way data binding with **ngModel**
|
||
Running the app right now would be disappointing.
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="Early form with no binding")
|
||
:marked
|
||
We don't see hero data because we are not binding to the `Hero` yet.
|
||
We know how to do that from earlier chapters.
|
||
[Displaying Data](./displaying-data.html) taught us Property Binding.
|
||
[User Input](./user-input.html) showed us how to listen for DOM events with an
|
||
Event Binding and how to update a component property with the displayed value.
|
||
|
||
Now we need to display, listen, and extract at the same time.
|
||
|
||
We could use those techniques again in our form.
|
||
Instead we'll introduce something new, the `[(ngModel)]` syntax, that
|
||
makes binding our form to the model super-easy.
|
||
|
||
Find the `<input>` tag for the "Name" and update it like this
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModel-1','app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
.l-sub-section
|
||
:marked
|
||
We appended a diagnostic interpolation after the input tag
|
||
so we can see what we're doing.
|
||
We left ourselves a note to throw it away when we're done.
|
||
|
||
:marked
|
||
Focus on the binding syntax: `[(ngModel)]="..."`.
|
||
|
||
If we ran the app right now and started typing in the *Name* input box,
|
||
adding and deleting characters, we'd see them appearing and disappearing
|
||
from the interpolated text.
|
||
At some point it might look like this.
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="ngModel in action")
|
||
:marked
|
||
The diagnostic is evidence that we really are flowing values from the input box to the model and
|
||
back again. **That's two-way data binding!**
|
||
|
||
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.
|
||
Defining a `name` attribute is a requirement when using `[(ngModel)]` in combination with a form.
|
||
|
||
.l-sub-section
|
||
:marked
|
||
Internally Angular creates `FormControls` and 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 chapter](#ngForm).
|
||
|
||
:marked
|
||
Let's add similar `[(ngModel)]` bindings and `name` attributes to *Alter Ego* and *Hero Power*.
|
||
We'll ditch the input box binding message
|
||
and add a new binding at the top to the component's `diagnostic` property.
|
||
Then we can confirm that two-way data binding works *for the entire Hero model*.
|
||
|
||
After revision the core of our form should have three `[(ngModel)]` bindings and `name` attributes that
|
||
look much like this:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (excerpt)')
|
||
|
||
.l-sub-section
|
||
:marked
|
||
- 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.
|
||
- Each input element has a `name` property that is required by Angular Forms to register the control with the form.
|
||
|
||
:marked
|
||
If we ran the app right now and changed every Hero model property, the form might display like this:
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action")
|
||
:marked
|
||
The diagnostic near the top of the form
|
||
confirms that all of our changes are reflected in the model.
|
||
|
||
**Delete** the `{{diagnostic}}` binding at the top as it has served its purpose.
|
||
|
||
.l-sub-section
|
||
:marked
|
||
### Inside [(ngModel)]
|
||
*This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!*
|
||
|
||
The punctuation in the binding syntax, <span style="font-family:courier"><b>[()]</b></span>, is a good clue to what's going on.
|
||
|
||
In a Property Binding, a value flows from the model to a target property on screen.
|
||
We identify that target property by surrounding its name in brackets, <span style="font-family:courier"><b>[]</b></span>.
|
||
This is a one-way data binding **from the model to the view**.
|
||
|
||
In an Event Binding, we flow the value from the target property on screen to the model.
|
||
We identify that target property by surrounding its name in parentheses, <span style="font-family:courier"><b>()</b></span>.
|
||
This is a one-way data binding in the opposite direction **from the view to the model**.
|
||
|
||
No wonder Angular chose to combine the punctuation as <span style="font-family:courier"><b>[()]</b></span>
|
||
to signify a two-way data binding and a **flow of data in both directions**.
|
||
|
||
In fact, we can break the `NgModel` binding into its two separate modes
|
||
as we do in this re-write of the "Name" `<input>` binding:
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
:marked
|
||
<br>The Property Binding should feel familiar. The Event Binding might seem strange.
|
||
|
||
The `ngModelChange` is not an `<input>` element event.
|
||
It is actually an event property of the `NgModel` directive.
|
||
When Angular sees a binding target in the form <span style="font-family:courier">[(x)]</span>,
|
||
it expects the `x` directive to have an `x` input property and an `xChange` output property.
|
||
|
||
The other oddity is the template expression, `model.name = $event`.
|
||
We're used to seeing an `$event` object coming from a DOM event.
|
||
The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter`
|
||
property that returns the input box value when it fires — which is precisely what
|
||
we should assign to the model's `name` property.
|
||
|
||
Nice to know but is it practical? We almost always prefer `[(ngModel)]`.
|
||
We might split the binding if we had to do something special in
|
||
the event handling such as debounce or throttle the key strokes.
|
||
|
||
Learn more about `NgModel` and other template syntax in the
|
||
[Template Syntax](./template-syntax.html) chapter.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Track change-state and validity with **ngModel**
|
||
|
||
A form isn't just about data binding. We'd also like to know the state of the controls on 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.
|
||
|
||
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
|
||
control and make messages appear or disappear.
|
||
|
||
table
|
||
tr
|
||
th State
|
||
th Class if true
|
||
th Class if false
|
||
tr
|
||
td Control has been visited
|
||
td <code>ng-touched</code>
|
||
td <code>ng-untouched</code>
|
||
tr
|
||
td Control's value has changed
|
||
td <code>ng-dirty</code>
|
||
td <code>ng-pristine</code>
|
||
tr
|
||
td Control's value is valid
|
||
td <code>ng-valid</code>
|
||
td <code>ng-invalid</code>
|
||
:marked
|
||
Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy**
|
||
to the "Name" `<input>` tag and use the spy to display those classes.
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-2','app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
:marked
|
||
Now run the app and focus on the *Name* input box.
|
||
Follow the next four steps *precisely*
|
||
|
||
1. Look but don't touch
|
||
1. Click in the input box, then click outside the text input box
|
||
1. Add slashes to the end of the name
|
||
1. Erase the name
|
||
|
||
The actions and effects are as follows:
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="Control State Transition")
|
||
:marked
|
||
We should be able to see the following four sets of class names and their transitions:
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="Control State Transitions")
|
||
|
||
:marked
|
||
The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a
|
||
strong visual signal when the data are invalid and we want to mark required fields.
|
||
So we add custom CSS for visual feedback.
|
||
|
||
**Delete** the `#spy` template reference variable and `TODO` as they have served their purpose.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Add Custom CSS for Visual Feedback
|
||
We realize we can mark required fields and invalid data at the same time with a colored bar
|
||
on the left of the input box:
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="Invalid Form")
|
||
|
||
:marked
|
||
We achieve this effect by adding two styles to a new `forms.css` file
|
||
that we add to our project as a sibling to `index.html`.
|
||
|
||
+makeExample('forms/ts/forms.css',null,'forms.css')(format=".")
|
||
:marked
|
||
These styles select for the two Angular validity classes and the HTML 5 "required" attribute.
|
||
|
||
We update the `<head>` of the `index.html` to include this style sheet.
|
||
+makeExample('forms/ts/index.html', 'styles', 'index.html (excerpt)')(format=".")
|
||
:marked
|
||
## Show and Hide Validation Error messages
|
||
|
||
We can do better.
|
||
|
||
The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we
|
||
don't know *what* is wrong or what to do about it.
|
||
We can leverage the `ng-invalid` class to reveal a helpful message.
|
||
|
||
Here's the way it should look when the user deletes the name:
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="Name required")
|
||
|
||
:marked
|
||
To achieve this effect we extend the `<input>` tag with
|
||
1. a [template reference variable](./template-syntax.html#ref-vars)
|
||
1. the "*is required*" message in a nearby `<div>` which we'll display only if the control is invalid.
|
||
|
||
Here's how we do it for the *name* input box:
|
||
+makeExample('forms/ts/app/hero-form.component.html',
|
||
'name-with-error-msg',
|
||
'app/hero-form.component.html (excerpt)')(format=".")
|
||
:marked
|
||
We need a template reference variable to access the input box's Angular control from within the template.
|
||
Here we created a variable called `name` and gave it the value "ngModel".
|
||
.l-sub-section
|
||
:marked
|
||
Why "ngModel"?
|
||
A directive's [exportAs](../api/core/index/Directive-decorator.html) property
|
||
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".
|
||
|
||
Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `<div>` element's `hidden` property.
|
||
+makeExample('forms/ts/app/hero-form.component.html',
|
||
'hidden-error-msg',
|
||
'app/hero-form.component.html (excerpt)')
|
||
:marked
|
||
In this example, we hide the message when the control is valid or pristine;
|
||
pristine means the user hasn't changed the value since it was displayed in this form.
|
||
|
||
This user experience is the developer's choice. Some folks want to see the message at all times.
|
||
If we ignore the `pristine` state, we would hide the message only when the value is valid.
|
||
If we arrive in this component with a new (blank) hero or an invalid hero,
|
||
we'll see the error message immediately, before we've done anything.
|
||
|
||
Some folks find that behavior disconcerting. They only want to see the message when the user makes an invalid change.
|
||
Hiding the message while the control is "pristine" achieves that goal.
|
||
We'll see the significance of this choice when we [add a new hero](#new-hero) to the form.
|
||
|
||
The Hero *Alter Ego* is optional so we can leave that be.
|
||
|
||
Hero *Power* selection is required.
|
||
We can add the same kind of error handling to the `<select>` if we want
|
||
but it's not imperative because the selection box already constrains the
|
||
power to valid value.
|
||
|
||
<a id="new-hero"></a>
|
||
<a id="reset"></a>
|
||
.l-main-section
|
||
:marked
|
||
## Add a hero and reset the form
|
||
We'd like to add a new hero in this form.
|
||
We place a "New Hero" button at the bottom of the form and bind its click event to a component method.
|
||
+makeExample('forms/ts/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',
|
||
'new-hero-v1',
|
||
'app/hero-form.component.ts (New Hero method - v1)')(format=".")
|
||
:marked
|
||
Run the application again, click the *New Hero* button, and the form clears.
|
||
The *required* bars to the left of the input box are red, indicating invalid `name` and `power` properties.
|
||
That's understandable as these are required fields.
|
||
The error messages are hidden because the form is pristine; we haven't changed anything yet.
|
||
|
||
Enter a name and click *New Hero* again.
|
||
This time we see an error message! Why? We don't want that when we display a new (empty) hero.
|
||
|
||
Inspecting the element in the browser tools reveals that the *name* input box is no longer pristine.
|
||
Replacing the hero *did not restore the pristine state* of the control.
|
||
.l-sub-section
|
||
:marked
|
||
Upon reflection, we realize that Angular cannot distinguish between
|
||
replacing the entire hero and clearing the `name` property programmatically.
|
||
Angular makes no assumptions and leaves the control in its current, dirty state.
|
||
:marked
|
||
We'll have to reset the form controls manually with a small trick.
|
||
We add an `active` flag to the component, initialized to `true`. When we add a new hero,
|
||
we toggle `active` false and then immediately back to true with a quick `setTimeout`.
|
||
+makeExample('forms/ts/app/hero-form.component.ts',
|
||
'new-hero',
|
||
'app/hero-form.component.ts (New Hero method - final)')(format=".")
|
||
:marked
|
||
Then we bind the form element to this `active` flag.
|
||
+makeExample('forms/ts/app/hero-form.component.html',
|
||
'form-active',
|
||
'app/hero-form.component.html (Form tag)')
|
||
:marked
|
||
With `NgIf` bound to the `active` flag,
|
||
clicking "New Hero" removes the form from the DOM and recreates it in a blink of an eye.
|
||
The re-created form is in a pristine state. The error message is hidden.
|
||
.l-sub-section
|
||
:marked
|
||
This is a temporary workaround while we await a proper form reset feature.
|
||
:marked
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Submit the form with **ngSubmit**
|
||
The user should be able to submit this form after filling it in.
|
||
The Submit button at the bottom of the form
|
||
does nothing on its own but it will
|
||
trigger a form submit because of its type (`type="submit"`).
|
||
|
||
A "form submit" is useless at the moment.
|
||
To make it useful, we'll update the `<form>` tag with another Angular directive, `NgSubmit`,
|
||
and bind it to the `HeroFormComponent.submit()` method with an event binding
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngSubmit')(format=".")
|
||
|
||
:marked
|
||
We slipped in something extra there at the end! We defined a
|
||
template reference variable, **`#heroForm`**, and initialized it with the value, "ngForm".
|
||
|
||
The variable `heroForm` is now a reference to the `NgForm` directive that governs the form as a whole.
|
||
<a id="ngForm"></a>
|
||
.l-sub-section
|
||
:marked
|
||
### The NgForm directive
|
||
What `NgForm` directive? We didn't add an [NgForm](../api/forms/index/NgForm-directive.html) directive!
|
||
|
||
Angular did. Angular creates and attaches an `NgForm` directive to the `<form>` tag automatically.
|
||
|
||
The `NgForm` directive supplements the `form` element with additional features.
|
||
It holds the controls we created for the elements with `ngModel` directive and `name` attribute
|
||
and monitors their properties including their validity.
|
||
It also has its own `valid` property which is true only *if every contained
|
||
control* is valid.
|
||
|
||
:marked
|
||
Later in the template we bind the button's `disabled` property to the form's over-all validity via
|
||
the `heroForm` variable. Here's that bit of markup:
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'submit-button')
|
||
:marked
|
||
Re-run the application. The form opens in a valid state and the button is enabled.
|
||
|
||
Now delete the *Name*. We violate the "name required" rule which
|
||
is duly noted in our error message as before. And now the Submit button is also disabled.
|
||
|
||
Not impressed? Think about it for a moment. What would we have to do to
|
||
wire the button's enable/disabled state to the form's validity without Angular's help?
|
||
|
||
For us, it was as simple as
|
||
1. Define a template reference variable on the (enhanced) form element
|
||
2. Reference that variable in a button some 50 lines away.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Toggle two form regions (extra credit)
|
||
Submitting the form isn't terribly dramatic at the moment.
|
||
.l-sub-section
|
||
:marked
|
||
An unsurprising observation for a demo. To be honest,
|
||
jazzing it up won't teach us anything new about forms.
|
||
But this is an opportunity to exercise some of our newly won
|
||
binding skills.
|
||
If you're not interested, you can skip to the chapter's conclusion
|
||
and not miss a thing.
|
||
:marked
|
||
Let's do something more strikingly visual.
|
||
Let's hide the data entry area and display something else.
|
||
|
||
Start by wrapping the form in a `<div>` and bind
|
||
its `hidden` property to the `HeroFormComponent.submitted` property.
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'edit-div', 'app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
:marked
|
||
The main form is visible from the start because the
|
||
the `submitted` property is false until we submit the form,
|
||
as this fragment from the `HeroFormComponent` reminds us:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.ts', 'submitted')(format=".")
|
||
|
||
:marked
|
||
When we click the Submit button, the `submitted` flag becomes true and the form disappears
|
||
as planned.
|
||
|
||
Now we need to show something else while the form is in the submitted state.
|
||
Add the following block of HTML below the `<div>` wrapper we just wrote:
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'submitted', 'app/hero-form.component.html (excerpt)')
|
||
|
||
:marked
|
||
There's our hero again, displayed read-only with interpolation bindings.
|
||
This slug of HTML only appears while the component is in the submitted state.
|
||
|
||
We added an Edit button whose click event is bound to an expression
|
||
that clears the `submitted` flag.
|
||
|
||
When we click it, this block disappears and the editable form reappears.
|
||
|
||
That's as much drama as we can muster for now.
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Conclusion
|
||
|
||
The Angular form techniques discussed in this chapter take
|
||
advantage of the following framework features to provide support for data modification, validation and more:
|
||
|
||
- An Angular HTML form template.
|
||
- A form component class with a `Component` decorator.
|
||
- The `ngSubmit` directive for handling the form submission.
|
||
- Template reference variables such as `#heroForm`, `#name` and `#power`.
|
||
- The `[(ngModel)]` syntax and a `name` attribute for two-way data binding, validation and change tracking.
|
||
- The reference variable’s `valid` property on input controls to check if a control is valid and show/hide error messages.
|
||
- Controlling the submit button's enabled state by binding to `NgForm` validity.
|
||
- Custom CSS classes that provide visual feedback to users about invalid controls.
|
||
|
||
Our final project folder structure should look like this:
|
||
.filetree
|
||
.file angular-forms
|
||
.children
|
||
.file app
|
||
.children
|
||
.file app.component.ts
|
||
.file app.module.ts
|
||
.file hero.ts
|
||
.file hero-form.component.html
|
||
.file hero-form.component.ts
|
||
.file main.ts
|
||
.file node_modules ...
|
||
.file index.html
|
||
.file package.json
|
||
.file tsconfig.json
|
||
:marked
|
||
Here’s the final version of the source:
|
||
|
||
+makeTabs(
|
||
`forms/ts/app/hero-form.component.ts,
|
||
forms/ts/app/hero-form.component.html,
|
||
forms/ts/app/hero.ts,
|
||
forms/ts/app/app.module.ts,
|
||
forms/ts/app/app.component.ts,
|
||
forms/ts/app/main.ts,
|
||
forms/ts/index.html,
|
||
forms/ts/forms.css`,
|
||
'final, final,,,,,',
|
||
`hero-form.component.ts,
|
||
hero-form.component.html,
|
||
hero.ts,
|
||
app.module.ts,
|
||
app.component.ts,
|
||
main.ts,
|
||
index.html,
|
||
forms.css`)
|
||
:marked
|