docs(devguide/forms): add "new hero" with form reset workaround and msg hiding

closes #792
This commit is contained in:
Ward Bell 2016-02-01 10:52:20 -08:00
parent 9a21e0f50b
commit b0ebc3897c
4 changed files with 125 additions and 30 deletions

View File

@ -2,19 +2,21 @@
<!-- #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 *ngIf="active" (ngSubmit)="onSubmit()" #heroForm="ngForm">
<!-- #enddocregion ngSubmit --> <!-- #enddocregion ngSubmit -->
<!-- #enddocregion edit-div --> <!-- #enddocregion edit-div -->
<div class="form-group"> <div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label> <label for="name">Name</label>
<!-- #docregion name-with-error-msg -->
<input type="text" class="form-control" required <input type="text" class="form-control" required
[(ngModel)]="model.name" [(ngModel)]="model.name"
ngControl="name" #name="ngForm" > ngControl="name" #name="ngForm" >
<div [hidden]="name.valid" class="alert alert-danger"> <!-- #docregion hidden-error-msg -->
<div [hidden]="name.valid || name.pristine" class="alert alert-danger">
<!-- #enddocregion hidden-error-msg -->
Name is required Name is required
</div> </div>
<!-- #enddocregion name-with-error-msg --> <!-- #enddocregion name-with-error-msg -->
@ -34,15 +36,27 @@
ngControl="power" #power="ngForm" > ngControl="power" #power="ngForm" >
<option *ngFor="#p of powers" [value]="p">{{p}}</option> <option *ngFor="#p of powers" [value]="p">{{p}}</option>
</select> </select>
<div [hidden]="power.valid" class="alert alert-danger"> <div [hidden]="power.valid || power.pristine" class="alert alert-danger">
Power is required Power is required
</div> </div>
</div> </div>
<!-- #docregion submit-button --> <!-- #docregion submit-button -->
<button type="submit" class="btn btn-default" <button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>
[disabled]="!heroForm.form.valid">Submit</button> <!-- #enddocregion submit-button -->
<!-- #enddocregion submit-button -->
<!-- #docregion new-hero-button -->
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
<!-- #enddocregion new-hero-button -->
<!-- #enddocregion final -->
<!-- NOT SHOWN IN DOCS -->
<div>
<hr>
Name via form.controls = {{showFormControls(heroForm)}}
</div>
<!-- - -->
<!-- #docregion final -->
</form> </form>
</div> </div>
@ -117,6 +131,7 @@
<!-- #enddocregion powers --> <!-- #enddocregion powers -->
<!-- #docregion start --> <!-- #docregion start -->
<button type="submit" class="btn btn-default">Submit</button> <button type="submit" class="btn btn-default">Submit</button>
</form> </form>
</div> </div>
<!-- #enddocregion start --> <!-- #enddocregion start -->
@ -172,7 +187,10 @@
TODO: remove this: {{model.name}} TODO: remove this: {{model.name}}
<!-- #enddocregion ngModel-3--> <!-- #enddocregion ngModel-3-->
<hr> <hr>
<form> <!-- #docregion form-active -->
<form *ngIf="active">
<!-- #enddocregion form-active -->
<!-- #docregion ngControl-1 --> <!-- #docregion ngControl-1 -->
<input type="text" class="form-control" required <input type="text" class="form-control" required
[(ngModel)]="model.name" [(ngModel)]="model.name"
@ -187,9 +205,4 @@
<!-- #enddocregion ngControl-2 --> <!-- #enddocregion ngControl-2 -->
</form> </form>
<div> </div>
<hr>
Name via form.controls = {{showFormControls(heroForm)}}
</div>
</div>

View File

@ -27,17 +27,37 @@ export class HeroFormComponent {
get diagnostic() { return JSON.stringify(this.model); } get diagnostic() { return JSON.stringify(this.model); }
// #enddocregion first // #enddocregion first
// #docregion final
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
// #docregion new-hero
active = true;
//////// DO NOT SHOW IN DOCS //////// // #docregion new-hero-v1
newHero() {
this.model = new Hero(42, '', '');
// #enddocregion new-hero-v1
this.active = false;
setTimeout(()=> this.active=true, 0);
// #docregion new-hero-v1
}
// #enddocregion new-hero-v1
// #enddocregion new-hero
// #enddocregion final
//////// NOT SHOWN IN DOCS ////////
// Reveal in html: // Reveal in html:
// AlterEgo via form.controls = {{showFormControls(hf)}} // Name via form.controls = {{showFormControls(heroForm)}}
showFormControls(form:NgForm){ showFormControls(form:NgForm){
return form.controls['alterEgo'] &&
return form && form.controls['name'] &&
// #docregion form-controls // #docregion form-controls
form.controls['name'].value; // Dr. IQ form.controls['name'].value; // Dr. IQ
// #enddocregion form-controls // #enddocregion form-controls
} }
///////////////////////////// /////////////////////////////
// #docregion first, final // #docregion first, final

View File

@ -464,11 +464,9 @@ figure.image-display
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:
-var stylePattern = { otl: /(#name=&quot;form&quot;)|(.*div.*$)|(Name is required)/gm };
+makeExample('forms/ts/app/hero-form.component.html', +makeExample('forms/ts/app/hero-form.component.html',
'name-with-error-msg', 'name-with-error-msg',
'app/hero-form.component.html (excerpt)', 'app/hero-form.component.html (excerpt)')(format=".")
stylePattern)
:marked :marked
When we added the `ngControl` directive, we bound it to the the model's `name` property. When we added the `ngControl` directive, we bound it to the the model's `name` property.
@ -478,9 +476,22 @@ figure.image-display
In other words, the `name` local template variable becomes a handle on the `ngControl` object In other words, the `name` local template variable becomes a handle on the `ngControl` object
for this input box. for this input box.
Now we can control visibility of the "name" error message by binding the message `<div>` element's `hidden` property 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.
to the `ngControl` object's `valid` property. The message is hidden while the control is valid; +makeExample('forms/ts/app/hero-form.component.html',
the message is revealed when the control becomes invalid. '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.
<a id="ngForm"></a> <a id="ngForm"></a>
.l-sub-section .l-sub-section
:marked :marked
@ -502,7 +513,58 @@ figure.image-display
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.
<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 .l-main-section
:marked :marked
## Submit the form with **ngSubmit** ## Submit the form with **ngSubmit**
@ -616,7 +678,7 @@ figure.image-display
.file main.ts .file main.ts
.file index.html .file index.html
.file package.json .file package.json
.file styles.cs .file styles.css
.file tsconfig.json .file tsconfig.json
:marked :marked
Heres the final version of the source: Heres the final version of the source:

View File

@ -213,7 +213,7 @@ code-example(format="." language="html").
it would ask Angular to inject the service into its constructor which would look just like the one for `AppComponent`: it would ask Angular to inject the service into its constructor which would look just like the one for `AppComponent`:
+makeExample('toh-4/ts/app/app.component.1.ts', 'ctor', 'hero-detail.component.ts (constructor)') +makeExample('toh-4/ts/app/app.component.1.ts', 'ctor', 'hero-detail.component.ts (constructor)')
:marked :marked
The `HeroDetailComponent` must *not* repeat it's parent's `providers` array! Guess [why](#shadow-provider). The `HeroDetailComponent` must *not* repeat its parent's `providers` array! Guess [why](#shadow-provider).
The `AppComponent` is the top level component of our application. The `AppComponent` is the top level component of our application.
There should be only one instance of that component and only one instance of the `HeroService` in our entire app. There should be only one instance of that component and only one instance of the `HeroService` in our entire app.
@ -371,7 +371,7 @@ code-example(format="." language="html").
We can simulate a slow connection. We can simulate a slow connection.
Add the following `getHeroesSlowly` method to the `HeroService` Import the `Hero` symbol and add the following `getHeroesSlowly` method to the `HeroService`
+makeExample('toh-4/ts/app/hero.service.ts', 'get-heroes-slowly', 'hero.service.ts (getHeroesSlowy)')(format=".") +makeExample('toh-4/ts/app/hero.service.ts', 'get-heroes-slowly', 'hero.service.ts (getHeroesSlowy)')(format=".")
:marked :marked
Like `getHeroes`, it also returns a promise. Like `getHeroes`, it also returns a promise.