docs(devguide/forms): add "new hero" with form reset workaround and msg hiding
closes #792
This commit is contained in:
parent
9a21e0f50b
commit
b0ebc3897c
|
@ -5,16 +5,18 @@
|
|||
<div [hidden]="submitted">
|
||||
<h1>Hero Form</h1>
|
||||
<!-- #docregion ngSubmit -->
|
||||
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
|
||||
<form *ngIf="active" (ngSubmit)="onSubmit()" #heroForm="ngForm">
|
||||
<!-- #enddocregion ngSubmit -->
|
||||
<!-- #enddocregion edit-div -->
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion name-with-error-msg -->
|
||||
<label for="name">Name</label>
|
||||
<input type="text" class="form-control" required
|
||||
[(ngModel)]="model.name"
|
||||
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
|
||||
</div>
|
||||
<!-- #enddocregion name-with-error-msg -->
|
||||
|
@ -34,15 +36,27 @@
|
|||
ngControl="power" #power="ngForm" >
|
||||
<option *ngFor="#p of powers" [value]="p">{{p}}</option>
|
||||
</select>
|
||||
<div [hidden]="power.valid" class="alert alert-danger">
|
||||
<div [hidden]="power.valid || power.pristine" class="alert alert-danger">
|
||||
Power is required
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- #docregion submit-button -->
|
||||
<button type="submit" class="btn btn-default"
|
||||
[disabled]="!heroForm.form.valid">Submit</button>
|
||||
<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">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>
|
||||
</div>
|
||||
|
||||
|
@ -117,6 +131,7 @@
|
|||
<!-- #enddocregion powers -->
|
||||
<!-- #docregion start -->
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- #enddocregion start -->
|
||||
|
@ -172,7 +187,10 @@
|
|||
TODO: remove this: {{model.name}}
|
||||
<!-- #enddocregion ngModel-3-->
|
||||
<hr>
|
||||
<form>
|
||||
<!-- #docregion form-active -->
|
||||
<form *ngIf="active">
|
||||
<!-- #enddocregion form-active -->
|
||||
|
||||
<!-- #docregion ngControl-1 -->
|
||||
<input type="text" class="form-control" required
|
||||
[(ngModel)]="model.name"
|
||||
|
@ -187,9 +205,4 @@
|
|||
<!-- #enddocregion ngControl-2 -->
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<hr>
|
||||
Name via form.controls = {{showFormControls(heroForm)}}
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -27,17 +27,37 @@ export class HeroFormComponent {
|
|||
get diagnostic() { return JSON.stringify(this.model); }
|
||||
// #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:
|
||||
// AlterEgo via form.controls = {{showFormControls(hf)}}
|
||||
// Name via form.controls = {{showFormControls(heroForm)}}
|
||||
showFormControls(form:NgForm){
|
||||
return form.controls['alterEgo'] &&
|
||||
|
||||
return form && form.controls['name'] &&
|
||||
// #docregion form-controls
|
||||
form.controls['name'].value; // Dr. IQ
|
||||
// #enddocregion form-controls
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
|
||||
// #docregion first, final
|
||||
|
|
|
@ -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.
|
||||
|
||||
Here's how we do it for the *name* input box:
|
||||
-var stylePattern = { otl: /(#name="form")|(.*div.*$)|(Name is required)/gm };
|
||||
+makeExample('forms/ts/app/hero-form.component.html',
|
||||
'name-with-error-msg',
|
||||
'app/hero-form.component.html (excerpt)',
|
||||
stylePattern)
|
||||
'app/hero-form.component.html (excerpt)')(format=".")
|
||||
:marked
|
||||
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
|
||||
for this input box.
|
||||
|
||||
Now we can control visibility of the "name" error message by binding the message `<div>` element's `hidden` property
|
||||
to the `ngControl` object's `valid` property. The message is hidden while the control is valid;
|
||||
the message is revealed when the control becomes invalid.
|
||||
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.
|
||||
<a id="ngForm"></a>
|
||||
.l-sub-section
|
||||
:marked
|
||||
|
@ -503,6 +514,57 @@ figure.image-display
|
|||
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**
|
||||
|
@ -616,7 +678,7 @@ figure.image-display
|
|||
.file main.ts
|
||||
.file index.html
|
||||
.file package.json
|
||||
.file styles.cs
|
||||
.file styles.css
|
||||
.file tsconfig.json
|
||||
:marked
|
||||
Here’s the final version of the source:
|
||||
|
|
|
@ -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`:
|
||||
+makeExample('toh-4/ts/app/app.component.1.ts', 'ctor', 'hero-detail.component.ts (constructor)')
|
||||
: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.
|
||||
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.
|
||||
|
||||
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=".")
|
||||
:marked
|
||||
Like `getHeroes`, it also returns a promise.
|
||||
|
|
Loading…
Reference in New Issue