docs(devguide/forms): add "new hero" with form reset workaround and msg hiding
closes #792
This commit is contained in:
		
							parent
							
								
									9a21e0f50b
								
							
						
					
					
						commit
						b0ebc3897c
					
				| @ -2,19 +2,21 @@ | ||||
|  <!-- #docregion final --> | ||||
| <div class="container"> | ||||
|   <!-- #docregion edit-div --> | ||||
|   <div [hidden]="submitted"> | ||||
|   <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"> | ||||
|         <!-- #docregion name-with-error-msg --> | ||||
|         <label for="name">Name</label> | ||||
|          <!-- #docregion name-with-error-msg --> | ||||
|         <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> | ||||
|   <!-- #enddocregion submit-button --> | ||||
|       <!-- #docregion 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> | ||||
| </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
 | ||||
|     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 | ||||
| @ -502,7 +513,58 @@ figure.image-display | ||||
|   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** | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user