docs(reactive-forms): add reactive forms guide (#2835)
| @ -43,7 +43,7 @@ | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- #docregion submit-button --> | ||||
|       <button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button> | ||||
|       <button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button> | ||||
|       <!-- #enddocregion submit-button --> | ||||
|       <!-- #docregion new-hero-button-form-reset --> | ||||
|       <button type="button" class="btn btn-default" (click)="newHero(); heroForm.reset()">New Hero</button> | ||||
| @ -83,7 +83,7 @@ | ||||
|       <div class="col-xs-9 pull-left">{{ model.power }}</div> | ||||
|     </div> | ||||
|     <br> | ||||
|     <button class="btn btn-default" (click)="submitted=false">Edit</button> | ||||
|     <button class="btn btn-primary" (click)="submitted=false">Edit</button> | ||||
|   </div> | ||||
|   <!-- #enddocregion submitted --> | ||||
| </div> | ||||
| @ -137,7 +137,7 @@ | ||||
| 
 | ||||
|         <!-- #enddocregion powers --> | ||||
|     <!-- #docregion start --> | ||||
|         <button type="submit" class="btn btn-default">Submit</button> | ||||
|         <button type="submit" class="btn btn-success">Submit</button> | ||||
| 
 | ||||
|       </form> | ||||
|   </div> | ||||
| @ -175,7 +175,7 @@ | ||||
|         </div> | ||||
| 
 | ||||
|     <!-- #enddocregion ngModel-2--> | ||||
|         <button type="submit" class="btn btn-default">Submit</button> | ||||
|         <button type="submit" class="btn btn-success">Submit</button> | ||||
| 
 | ||||
|       </form> | ||||
|   </div> | ||||
|  | ||||
							
								
								
									
										1020
									
								
								public/docs/_examples/reactive-forms/e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | ||||
| // #docregion
 | ||||
| import { Component } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'my-app', | ||||
|   template: ` | ||||
|   <div class="container"> | ||||
|     <h1>Reactive Forms</h1> | ||||
|     <hero-detail></hero-detail> | ||||
|   </div>` | ||||
| }) | ||||
| export class AppComponent { } | ||||
							
								
								
									
										13
									
								
								public/docs/_examples/reactive-forms/ts/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | ||||
| // #docregion
 | ||||
| import { Component } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'my-app', | ||||
|   template: ` | ||||
|   <div class="container"> | ||||
|     <h1>Reactive Forms</h1> | ||||
|     <hero-list></hero-list> | ||||
|   </div>` | ||||
| }) | ||||
| export class AppComponent { } | ||||
							
								
								
									
										39
									
								
								public/docs/_examples/reactive-forms/ts/app/app.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,39 @@ | ||||
| // #docplaster
 | ||||
| // #docregion
 | ||||
| // #docregion v1
 | ||||
| import { NgModule }            from '@angular/core'; | ||||
| import { BrowserModule }       from '@angular/platform-browser'; | ||||
| import { ReactiveFormsModule } from '@angular/forms';  // <-- #1 import module
 | ||||
| 
 | ||||
| import { AppComponent }        from './app.component'; | ||||
| import { HeroDetailComponent } from './hero-detail.component'; // <-- #1 import component
 | ||||
| // #enddocregion v1
 | ||||
| import { HeroListComponent }   from './hero-list.component'; | ||||
| 
 | ||||
| import { HeroService }         from './hero.service'; //  <-- #1 import service
 | ||||
| // #docregion v1
 | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|     ReactiveFormsModule // <-- #2 add to Angular module imports
 | ||||
|   ], | ||||
|   declarations: [ | ||||
|     AppComponent, | ||||
|     HeroDetailComponent, // <-- #3 declare app component
 | ||||
| // #enddocregion v1
 | ||||
|     HeroListComponent | ||||
| // #docregion v1
 | ||||
|   ], | ||||
| // #enddocregion v1
 | ||||
|   exports: [ // export for the DemoModule
 | ||||
|     AppComponent, | ||||
|     HeroDetailComponent, | ||||
|     HeroListComponent | ||||
|   ], | ||||
|   providers: [ HeroService ], // <-- #4 provide HeroService
 | ||||
| // #docregion v1
 | ||||
|   bootstrap: [ AppComponent ] | ||||
| }) | ||||
| export class AppModule { } | ||||
| // #enddocregion v1
 | ||||
							
								
								
									
										40
									
								
								public/docs/_examples/reactive-forms/ts/app/data-model.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,40 @@ | ||||
| // #docregion
 | ||||
| // #docregion model-classes
 | ||||
| export class Hero { | ||||
|   id = 0; | ||||
|   name = ''; | ||||
|   addresses: Address[]; | ||||
| } | ||||
| 
 | ||||
| export class Address { | ||||
|   street = ''; | ||||
|   city   = ''; | ||||
|   state  = ''; | ||||
|   zip    = ''; | ||||
| } | ||||
| // #enddocregion model-classes
 | ||||
| 
 | ||||
| export const heroes: Hero[] = [ | ||||
|   { | ||||
|     id: 1, | ||||
|     name: 'Whirlwind', | ||||
|     addresses: [ | ||||
|       {street: '123 Main',  city: 'Anywhere', state: 'CA',  zip: '94801'}, | ||||
|       {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'}, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     id: 2, | ||||
|     name: 'Bombastic', | ||||
|     addresses: [ | ||||
|       {street: '789 Elm',  city: 'Smallville', state: 'OH',  zip: '04501'}, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     id: 3, | ||||
|     name: 'Magneta', | ||||
|     addresses: [ ] | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| export const states = ['CA', 'MD', 'OH', 'VA']; | ||||
| @ -0,0 +1,40 @@ | ||||
| <div class="container"> | ||||
| <h1>Reactive Forms</h1> | ||||
| <h4><i>Pick a demo:</i> | ||||
|   <select [selectedIndex]="demo - 1" (change)="selectDemo($event.target.selectedIndex)"> | ||||
|     <option *ngFor="let demo of demos">{{demo}}</option> | ||||
|   </select> | ||||
| </h4> | ||||
| 
 | ||||
| <hr> | ||||
| 
 | ||||
| <div class="demo"> | ||||
|   <hero-list *ngIf="demo===final"></hero-list> | ||||
|   <hero-detail-1 *ngIf="demo===1"></hero-detail-1> | ||||
|   <hero-detail-2 *ngIf="demo===2"></hero-detail-2> | ||||
|   <hero-detail-3 *ngIf="demo===3"></hero-detail-3> | ||||
|   <hero-detail-4 *ngIf="demo===4"></hero-detail-4> | ||||
|   <hero-detail-5 *ngIf="demo===5"></hero-detail-5> | ||||
| 
 | ||||
|   <div *ngIf="demo >= 6 && demo !== final" > | ||||
| 
 | ||||
|     <h3 *ngIf="isLoading"><i>Loading heroes ... </i></h3> | ||||
|     <h3 *ngIf="!isLoading">Select a hero:</h3> | ||||
| 
 | ||||
|     <nav> | ||||
|       <button (click)="getHeroes()" class="btn btn-primary">Refresh</button> | ||||
|       <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a> | ||||
|     </nav> | ||||
| 
 | ||||
|     <div *ngIf="selectedHero"> | ||||
|       <hr> | ||||
|       <h2>Hero Detail</h2> | ||||
|       <h3>Editing: {{selectedHero.name}}</h3> | ||||
|       <hero-detail-6 [hero]=selectedHero *ngIf="demo===6"></hero-detail-6> | ||||
|       <hero-detail-7 [hero]=selectedHero *ngIf="demo===7"></hero-detail-7> | ||||
|       <hero-detail-8 [hero]=selectedHero *ngIf="demo===8"></hero-detail-8> | ||||
| 
 | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
| @ -0,0 +1,48 @@ | ||||
| /* tslint:disable:member-ordering */ | ||||
| import { Component }  from '@angular/core'; | ||||
| import { Observable } from 'rxjs/Observable'; | ||||
| 
 | ||||
| import { Hero }        from './data-model'; | ||||
| import { HeroService } from './hero.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'my-app', | ||||
|   templateUrl: 'demo.component.html' | ||||
| }) | ||||
| export class DemoComponent { | ||||
| 
 | ||||
|   demos: string[] = [ | ||||
|     'Just a FormControl', | ||||
|     'FormControl in a FormGroup', | ||||
|     'Simple FormBuilder group', | ||||
|     'Group with multiple controls', | ||||
|     'Nested FormBuilder group', | ||||
|     'PatchValue', | ||||
|     'SetValue', | ||||
|     'FormArray', | ||||
|     'Final'].map(n => n + ' Demo'); | ||||
| 
 | ||||
|   final = this.demos.length; | ||||
|   demo = this.final; // current demo
 | ||||
| 
 | ||||
|   heroes: Observable<Hero[]>; | ||||
|   isLoading = false; | ||||
|   selectedHero: Hero; | ||||
| 
 | ||||
|   constructor(private heroService: HeroService) { } | ||||
| 
 | ||||
|   getHeroes() { | ||||
|     this.isLoading = true; | ||||
|     this.heroes = this.heroService.getHeroes() | ||||
|                       .finally(() => this.isLoading = false); | ||||
|     this.selectedHero = undefined; | ||||
|   } | ||||
| 
 | ||||
|   select(hero: Hero) { this.selectedHero = hero; } | ||||
| 
 | ||||
|   selectDemo(demo: number) { | ||||
|     this.demo = demo + 1; | ||||
|     this.getHeroes(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										33
									
								
								public/docs/_examples/reactive-forms/ts/app/demo.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,33 @@ | ||||
| import { NgModule }             from '@angular/core'; | ||||
| import { BrowserModule }        from '@angular/platform-browser'; | ||||
| import { ReactiveFormsModule }  from '@angular/forms'; | ||||
| 
 | ||||
| import { AppModule }            from './app.module'; | ||||
| import { DemoComponent }        from './demo.component'; | ||||
| import { HeroDetailComponent1 } from './hero-detail-1.component'; | ||||
| import { HeroDetailComponent2 } from './hero-detail-2.component'; | ||||
| import { HeroDetailComponent3 } from './hero-detail-3.component'; | ||||
| import { HeroDetailComponent4 } from './hero-detail-4.component'; | ||||
| import { HeroDetailComponent5 } from './hero-detail-5.component'; | ||||
| import { HeroDetailComponent6 } from './hero-detail-6.component'; | ||||
| import { HeroDetailComponent7 } from './hero-detail-7.component'; | ||||
| import { HeroDetailComponent8 } from './hero-detail-8.component'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|     ReactiveFormsModule, | ||||
|     AppModule, | ||||
|   ], | ||||
|   declarations: [ DemoComponent, | ||||
|   HeroDetailComponent1, | ||||
|   HeroDetailComponent2, | ||||
|   HeroDetailComponent3, | ||||
|   HeroDetailComponent4, | ||||
|   HeroDetailComponent5, | ||||
|   HeroDetailComponent6, | ||||
|   HeroDetailComponent7, | ||||
|   HeroDetailComponent8], | ||||
|   bootstrap:    [ DemoComponent ] | ||||
| }) | ||||
| export class DemoModule { } | ||||
| @ -0,0 +1,8 @@ | ||||
| <!-- #docregion simple-control--> | ||||
| <h2>Hero Detail</h2> | ||||
| <h3><i>Just a FormControl</i></h3> | ||||
| <label class="center-block">Name: | ||||
|   <input class="form-control" [formControl]="name"> | ||||
| </label> | ||||
| <!-- #enddocregion simple-control--> | ||||
| 
 | ||||
| @ -0,0 +1,15 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docregion imports
 | ||||
| import { Component }              from '@angular/core'; | ||||
| import { FormControl }            from '@angular/forms'; | ||||
| // #enddocregion
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-1', | ||||
|   templateUrl: './hero-detail-1.component.html' | ||||
| }) | ||||
| // #docregion v1
 | ||||
| export class HeroDetailComponent1 { | ||||
|   name = new FormControl(); | ||||
| } | ||||
| @ -0,0 +1,18 @@ | ||||
| <!-- #docregion basic-form--> | ||||
| <h2>Hero Detail</h2> | ||||
| <h3><i>FormControl in a FormGroup</i></h3> | ||||
| <form [formGroup]="heroForm" novalidate> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Name: | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </label> | ||||
|   </div> | ||||
| </form> | ||||
| <!-- #enddocregion basic-form--> | ||||
| 
 | ||||
| <!-- #docregion form-value-json --> | ||||
| <p>Form value: {{ heroForm.value | json }}</p> | ||||
| <!-- #enddocregion form-value-json  --> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,18 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docregion imports
 | ||||
| import { Component }              from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| // #enddocregion imports
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-2', | ||||
|   templateUrl: './hero-detail-2.component.html' | ||||
| }) | ||||
| // #docregion v2
 | ||||
| export class HeroDetailComponent2 { | ||||
|   heroForm = new FormGroup ({ | ||||
|     name: new FormControl() | ||||
|   }); | ||||
| } | ||||
| // #enddocregion v2
 | ||||
| @ -0,0 +1,16 @@ | ||||
| <!-- #docregion basic-form--> | ||||
| <h2>Hero Detail</h2> | ||||
| <h3><i>A FormGroup with a single FormControl using FormBuilder</i></h3> | ||||
| <form [formGroup]="heroForm" novalidate> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Name: | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </label> | ||||
|   </div> | ||||
| </form> | ||||
| <!-- #enddocregion basic-form--> | ||||
| 
 | ||||
| <!-- #docregion form-value-json --> | ||||
| <p>Form value: {{ heroForm.value | json }}</p> | ||||
| <p>Form status: {{ heroForm.status | json }}</p> | ||||
| <!-- #enddocregion form-value-json  --> | ||||
| @ -0,0 +1,28 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docregion imports
 | ||||
| import { Component }                          from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||
| // #enddocregion imports
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-3', | ||||
|   templateUrl: './hero-detail-3.component.html' | ||||
| }) | ||||
| // #docregion v3
 | ||||
| export class HeroDetailComponent3 { | ||||
|   heroForm: FormGroup; // <--- heroForm is of type FormGroup
 | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { // <--- inject FormBuilder
 | ||||
|     this.createForm(); | ||||
|   } | ||||
| 
 | ||||
|   createForm() { | ||||
|     // #docregion required
 | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: ['', Validators.required ], | ||||
|     }); | ||||
|     // #enddocregion required
 | ||||
|   } | ||||
| } | ||||
| // #enddocregion v3
 | ||||
| @ -0,0 +1,26 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docregion imports
 | ||||
| import { Component }              from '@angular/core'; | ||||
| import { FormBuilder, FormGroup } from '@angular/forms'; | ||||
| // #enddocregion imports
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-3', | ||||
|   templateUrl: './hero-detail-3.component.html' | ||||
| }) | ||||
| // #docregion v3a
 | ||||
| export class HeroDetailComponent3 { | ||||
|   heroForm: FormGroup; // <--- heroForm is of type FormGroup
 | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { // <--- inject FormBuilder
 | ||||
|     this.createForm(); | ||||
|   } | ||||
| 
 | ||||
|   createForm() { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', // <--- the FormControl called "name"
 | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| // #enddocregion v3a
 | ||||
| @ -0,0 +1,46 @@ | ||||
| <!-- #docregion --> | ||||
| <h2>Hero Detail</h2> | ||||
| <h3><i>A FormGroup with multiple FormControls</i></h3> | ||||
| <form [formGroup]="heroForm" novalidate> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Name: | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Street: | ||||
|       <input class="form-control" formControlName="street"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">City: | ||||
|       <input class="form-control" formControlName="city"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">State: | ||||
|       <select class="form-control" formControlName="state"> | ||||
|           <option *ngFor="let state of states" [value]="state">{{state}}</option> | ||||
|       </select> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Zip Code: | ||||
|       <input class="form-control" formControlName="zip"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Super power:</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label> | ||||
|   </div> | ||||
|   <div class="checkbox"> | ||||
|     <label class="center-block"> | ||||
|       <input type="checkbox" formControlName="sidekick">I have a sidekick. | ||||
|     </label> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <p>Form value: {{ heroForm.value | json }}</p> | ||||
| @ -0,0 +1,35 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docregion imports
 | ||||
| import { Component }                          from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||
| 
 | ||||
| import { states } from './data-model'; | ||||
| // #enddocregion imports
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-4', | ||||
|   templateUrl: './hero-detail-4.component.html' | ||||
| }) | ||||
| // #docregion v4
 | ||||
| export class HeroDetailComponent4 { | ||||
|   heroForm: FormGroup; | ||||
|   states = states; | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.createForm(); | ||||
|   } | ||||
| 
 | ||||
|   createForm() { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: ['', Validators.required ], | ||||
|       street: '', | ||||
|       city: '', | ||||
|       state: '', | ||||
|       zip: '', | ||||
|       power: '', | ||||
|       sidekick: '' | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| // #enddocregion v4
 | ||||
| @ -0,0 +1,56 @@ | ||||
| 
 | ||||
| <form [formGroup]="heroForm" novalidate> | ||||
|     <div class="form-group"> | ||||
|       <label class="center-block">Name: | ||||
|         <input class="form-control" formControlName="name"> | ||||
|       </label> | ||||
|     </div> | ||||
| <!-- #docregion add-group--> | ||||
|     <div formGroupName="address" class="well well-lg"> | ||||
|       <h4>Secret Lair</h4> | ||||
|       <div class="form-group"> | ||||
|         <label class="center-block">Street: | ||||
|           <input class="form-control" formControlName="street"> | ||||
|         </label> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label class="center-block">City: | ||||
|           <input class="form-control" formControlName="city"> | ||||
|         </label> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label class="center-block">State: | ||||
|           <select class="form-control" formControlName="state"> | ||||
|             <option *ngFor="let state of states" [value]="state">{{state}}</option> | ||||
|           </select> | ||||
|         </label> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label class="center-block">Zip Code: | ||||
|           <input class="form-control" formControlName="zip"> | ||||
|         </label> | ||||
|       </div> | ||||
|     </div> | ||||
| <!-- #enddocregion add-group--> | ||||
|     <div class="form-group"> | ||||
|       <label>Super power:</label> | ||||
|         <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label> | ||||
|         <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label> | ||||
|         <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label> | ||||
|     </div> | ||||
|     <div class="checkbox"> | ||||
|       <label class="center-block"> | ||||
|         <input type="checkbox" formControlName="sidekick">I have a sidekick. | ||||
|       </label> | ||||
|     </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| <h4>Extra info for the curious:</h4> | ||||
| <!-- #docregion inspect-value --> | ||||
| <p>Name value: {{ heroForm.get('name').value }}</p> | ||||
| <!-- #enddocregion inspect-value --> | ||||
| 
 | ||||
| <!-- #docregion inspect-child-control --> | ||||
| <p>Street value: {{ heroForm.get('address.street').value}}</p> | ||||
| <!-- #enddocregion inspect-child-control --> | ||||
| @ -0,0 +1,36 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| import { Component }                          from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||
| 
 | ||||
| import { states } from './data-model'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-5', | ||||
|   templateUrl: './hero-detail-5.component.html' | ||||
| }) | ||||
| // #docregion v5
 | ||||
| export class HeroDetailComponent5 { | ||||
|   heroForm: FormGroup; | ||||
|   states = states; | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.createForm(); | ||||
|   } | ||||
| 
 | ||||
|   createForm() { | ||||
|     this.heroForm = this.fb.group({ // <-- the parent FormGroup
 | ||||
|       name: ['', Validators.required ], | ||||
|       address: this.fb.group({ // <-- the child FormGroup
 | ||||
|         street: '', | ||||
|         city: '', | ||||
|         state: '', | ||||
|         zip: '' | ||||
|       }), | ||||
|       power: '', | ||||
|       sidekick: '' | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| // #enddocregion v5
 | ||||
| 
 | ||||
| @ -0,0 +1,46 @@ | ||||
| <!-- #docregion --> | ||||
| <h2>Hero Detail</h2> | ||||
| <h3><i>PatchValue to initialize a value</i></h3> | ||||
| <form [formGroup]="heroForm" novalidate> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Name: | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Street: | ||||
|       <input class="form-control" formControlName="street"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">City: | ||||
|       <input class="form-control" formControlName="city"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">State: | ||||
|       <select class="form-control" formControlName="state"> | ||||
|         <option *ngFor="let state of states" [value]="state">{{state}}</option> | ||||
|       </select> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Zip Code: | ||||
|       <input class="form-control" formControlName="zip"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Super power:</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label> | ||||
|   </div> | ||||
|   <div class="checkbox"> | ||||
|     <label class="center-block"> | ||||
|       <input type="checkbox" formControlName="sidekick">I have a sidekick. | ||||
|     </label> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <p>Form value: {{ heroForm.value | json }}</p> | ||||
| @ -0,0 +1,59 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docregion import-input
 | ||||
| import { Component, Input, OnChanges }             from '@angular/core'; | ||||
| // #enddocregion import-input
 | ||||
| import { FormBuilder, FormGroup, Validators }      from '@angular/forms'; | ||||
| 
 | ||||
| // #docregion import-hero
 | ||||
| import { Hero, states } from './data-model'; | ||||
| // #enddocregion import-hero
 | ||||
| 
 | ||||
| ////////// 6 ////////////////////
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-6', | ||||
|   templateUrl: './hero-detail-5.component.html' | ||||
| }) | ||||
| // #docregion v6
 | ||||
| export class HeroDetailComponent6 implements OnChanges { | ||||
|   // #docregion hero
 | ||||
|   @Input() hero: Hero; | ||||
|   // #enddocregion hero
 | ||||
| 
 | ||||
|   heroForm: FormGroup; | ||||
|   states = states; | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.createForm(); | ||||
|   } | ||||
| 
 | ||||
|   createForm() { | ||||
|     // #docregion hero-form-model
 | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: ['', Validators.required ], | ||||
|       address: this.fb.group({ | ||||
|         street: '', | ||||
|         city: '', | ||||
|         state: '', | ||||
|         zip: '' | ||||
|       }), | ||||
|       power: '', | ||||
|       sidekick: '' | ||||
|     }); | ||||
|     // #enddocregion hero-form-model
 | ||||
|   } | ||||
| 
 | ||||
|   // #docregion patch-value-on-changes
 | ||||
|   ngOnChanges() { // <-- wrap patchValue in ngOnChanges
 | ||||
|     this.heroForm.reset(); | ||||
|     // #docregion patch-value
 | ||||
|     this.heroForm.patchValue({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|     // #enddocregion patch-value
 | ||||
|   } | ||||
|   // #enddocregion patch-value-on-changes
 | ||||
| } | ||||
| 
 | ||||
| // #enddocregion v6
 | ||||
| @ -0,0 +1,46 @@ | ||||
| <!-- #docregion --> | ||||
| <h2>Hero Detail</h2> | ||||
| <h3><i>A FormGroup with multiple FormControls</i></h3> | ||||
| <form [formGroup]="heroForm" novalidate> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Name: | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Street: | ||||
|       <input class="form-control" formControlName="street"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">City: | ||||
|       <input class="form-control" formControlName="city"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">State: | ||||
|       <select class="form-control" formControlName="state"> | ||||
|         <option *ngFor="let state of states" [value]="state">{{state}}</option> | ||||
|       </select> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label class="center-block">Zip Code: | ||||
|       <input class="form-control" formControlName="zip"> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Super power:</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label> | ||||
|       <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label> | ||||
|   </div> | ||||
|   <div class="checkbox"> | ||||
|     <label class="center-block"> | ||||
|       <input type="checkbox" formControlName="sidekick">I have a sidekick. | ||||
|     </label> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <p>Form value: {{ heroForm.value | json }}</p> | ||||
| @ -0,0 +1,67 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docplaster
 | ||||
| // #docregion imports
 | ||||
| import { Component, Input, OnChanges }             from '@angular/core'; | ||||
| import { FormBuilder, FormGroup, Validators }      from '@angular/forms'; | ||||
| 
 | ||||
| import { Address, Hero, states } from './data-model'; | ||||
| // #enddocregion imports
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-7', | ||||
|   templateUrl: './hero-detail-5.component.html' | ||||
| }) | ||||
| // #docregion v7
 | ||||
| export class HeroDetailComponent7 implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   heroForm: FormGroup; | ||||
|   states = states; | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.createForm(); | ||||
|   } | ||||
| 
 | ||||
|   createForm() { | ||||
|     // #docregion address-form-group
 | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: ['', Validators.required ], | ||||
|       address: this.fb.group(new Address()), // <-- a FormGroup with a new address
 | ||||
|       power: '', | ||||
|       sidekick: '' | ||||
|     }); | ||||
|     // #enddocregion address-form-group
 | ||||
|   } | ||||
| 
 | ||||
|   // #docregion ngOnChanges
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset({ | ||||
|       name: this.hero.name, | ||||
|       address: this.hero.addresses[0] || new Address() | ||||
|     }); | ||||
|   } | ||||
|   // #enddocregion ngOnChanges
 | ||||
| 
 | ||||
|   /* First version of ngOnChanges | ||||
|   // #docregion ngOnChanges-1
 | ||||
|   ngOnChanges() | ||||
|   // #enddocregion ngOnChanges-1
 | ||||
|   */ | ||||
|   ngOnChanges1() { | ||||
|     // #docregion reset
 | ||||
|     this.heroForm.reset(); | ||||
|     // #enddocregion reset
 | ||||
|     // #docregion ngOnChanges-1
 | ||||
|     // #docregion set-value
 | ||||
|     this.heroForm.setValue({ | ||||
|       name:    this.hero.name, | ||||
|       // #docregion set-value-address
 | ||||
|       address: this.hero.addresses[0] || new Address() | ||||
|       // #enddocregion set-value-address
 | ||||
|     }); | ||||
|     // #enddocregion set-value
 | ||||
|   } | ||||
|   // #enddocregion ngOnChanges-1
 | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,70 @@ | ||||
| <!-- #docplaster--> | ||||
| <h3><i>Using FormArray to add groups</i></h3> | ||||
| 
 | ||||
| <form [formGroup]="heroForm" novalidate> | ||||
|     <p>Form Changed: {{ heroForm.dirty }}</p> | ||||
| 
 | ||||
|     <div class="form-group"> | ||||
|       <label class="center-block">Name: | ||||
|         <input class="form-control" formControlName="name"> | ||||
|       </label> | ||||
|     </div> | ||||
|     <!-- #docregion form-array--> | ||||
|     <!-- #docregion form-array-skeleton --> | ||||
|     <div formArrayName="secretLairs" class="well well-lg"> | ||||
|       <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > | ||||
|         <!-- The repeated address template --> | ||||
|     <!-- #enddocregion form-array-skeleton --> | ||||
|         <h4>Address #{{i + 1}}</h4> | ||||
|         <div style="margin-left: 1em;"> | ||||
|           <div class="form-group"> | ||||
|             <label class="center-block">Street: | ||||
|               <input class="form-control" formControlName="street"> | ||||
|             </label> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label class="center-block">City: | ||||
|               <input class="form-control" formControlName="city"> | ||||
|             </label> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label class="center-block">State: | ||||
|               <select class="form-control" formControlName="state"> | ||||
|                 <option *ngFor="let state of states" [value]="state">{{state}}</option> | ||||
|               </select> | ||||
|             </label> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label class="center-block">Zip Code: | ||||
|               <input class="form-control" formControlName="zip"> | ||||
|             </label> | ||||
|           </div> | ||||
|         </div> | ||||
|         <br> | ||||
|         <!-- End of the repeated address template --> | ||||
|     <!-- #docregion form-array-skeleton --> | ||||
|       </div> | ||||
|     <!-- #enddocregion form-array-skeleton --> | ||||
|     <!-- #enddocregion form-array--> | ||||
|       <!-- #docregion add-lair --> | ||||
|       <button (click)="addLair()" type="button">Add a Secret Lair</button> | ||||
|       <!-- #enddocregion add-lair --> | ||||
|     <!-- #docregion form-array--> | ||||
|     <!-- #docregion form-array-skeleton --> | ||||
|     </div> | ||||
|     <!-- #enddocregion form-array-skeleton --> | ||||
|     <!-- #enddocregion form-array--> | ||||
|     <div class="form-group"> | ||||
|         <label>Super power:</label> | ||||
|           <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label> | ||||
|           <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label> | ||||
|           <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label> | ||||
|       </div> | ||||
|       <div class="checkbox"> | ||||
|         <label class="center-block"> | ||||
|           <input type="checkbox" formControlName="sidekick">I have a sidekick. | ||||
|         </label> | ||||
|       </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| @ -0,0 +1,69 @@ | ||||
| /* tslint:disable:component-class-suffix */ | ||||
| // #docregion imports
 | ||||
| import { Component, Input, OnChanges }                   from '@angular/core'; | ||||
| import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||
| 
 | ||||
| import { Address, Hero, states } from './data-model'; | ||||
| // #enddocregion imports
 | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-8', | ||||
|   templateUrl: './hero-detail-8.component.html' | ||||
| }) | ||||
| // #docregion v8
 | ||||
| export class HeroDetailComponent8 implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   heroForm: FormGroup; | ||||
|   states = states; | ||||
| 
 | ||||
|   // #docregion ctor
 | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.createForm(); | ||||
|     this.logNameChange(); | ||||
|   } | ||||
|   // #enddocregion ctor
 | ||||
| 
 | ||||
|   createForm() { | ||||
|     // #docregion secretLairs-form-array
 | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: ['', Validators.required ], | ||||
|       secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray
 | ||||
|       power: '', | ||||
|       sidekick: '' | ||||
|     }); | ||||
|     // #enddocregion secretLairs-form-array
 | ||||
|   } | ||||
| 
 | ||||
|   logNameChange() {/* Coming soon */} | ||||
| 
 | ||||
|   // #docregion onchanges
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|     this.setAddresses(this.hero.addresses); | ||||
|   } | ||||
|   // #enddocregion onchanges
 | ||||
| 
 | ||||
|   // #docregion get-secret-lairs
 | ||||
|   get secretLairs(): FormArray { | ||||
|     return this.heroForm.get('secretLairs') as FormArray; | ||||
|   }; | ||||
|   // #enddocregion get-secret-lairs
 | ||||
| 
 | ||||
|   // #docregion set-addresses
 | ||||
|   setAddresses(addresses: Address[]) { | ||||
|     const addressFGs = addresses.map(address => this.fb.group(address)); | ||||
|     const addressFormArray = this.fb.array(addressFGs); | ||||
|     this.heroForm.setControl('secretLairs', addressFormArray); | ||||
|   } | ||||
|   // #enddocregion set-addresses
 | ||||
| 
 | ||||
|   // #docregion add-lair
 | ||||
|   addLair() { | ||||
|     this.secretLairs.push(this.fb.group(new Address())); | ||||
|   } | ||||
|   // #enddocregion add-lair
 | ||||
| } | ||||
| @ -0,0 +1,73 @@ | ||||
| <!-- #docplaster --> | ||||
| <!-- #docregion --> | ||||
|   <!-- #docregion buttons --> | ||||
| <form [formGroup]="heroForm" (ngSubmit)="onSubmit()" novalidate> | ||||
|   <div style="margin-bottom: 1em"> | ||||
|     <button type="submit" | ||||
|             [disabled]="heroForm.pristine" class="btn btn-success">Save</button>   | ||||
|     <button type="reset" (click)="revert()" | ||||
|             [disabled]="heroForm.pristine" class="btn btn-danger">Revert</button> | ||||
|   </div> | ||||
| 
 | ||||
|   <!-- Hero Detail Controls --> | ||||
|   <!-- #enddocregion buttons --> | ||||
|   <div class="form-group"> | ||||
|       <label>Name: | ||||
|         <input class="form-control" formControlName="name"> | ||||
|       </label> | ||||
|   </div> | ||||
| 
 | ||||
|   <div formArrayName="secretLairs" class="well well-lg"> | ||||
|     <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > | ||||
|       <!-- The repeated address template --> | ||||
|       <h4>Address #{{i + 1}}</h4> | ||||
|       <div style="margin-left: 1em;"> | ||||
|         <div class="form-group"> | ||||
|           <label>Street: | ||||
|             <input class="form-control" formControlName="street"> | ||||
|           </label> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>City: | ||||
|             <input class="form-control" formControlName="city"> | ||||
|           </label> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>State: | ||||
|             <select class="form-control" formControlName="state"> | ||||
|               <option *ngFor="let state of states" [value]="state">{{state}}</option> | ||||
|             </select> | ||||
|           </label> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>Zip Code: | ||||
|             <input class="form-control" formControlName="zip"> | ||||
|           </label> | ||||
|         </div> | ||||
|       </div> | ||||
|       <br> | ||||
|       <!-- End of the repeated address template --> | ||||
|     </div> | ||||
|     <button (click)="addLair()" type="button">Add a Secret Lair</button> | ||||
|   </div> | ||||
|   <!-- #docregion buttons --> | ||||
|   <div class="form-group"> | ||||
|     <label>Super power:</label> | ||||
|     <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label> | ||||
|     <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label> | ||||
|     <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label> | ||||
|   </div> | ||||
|   <div class="checkbox"> | ||||
|     <label class="center-block"> | ||||
|       <input type="checkbox" formControlName="sidekick">I have a sidekick. | ||||
|     </label> | ||||
|   </div> | ||||
| </form> | ||||
|   <!-- #enddocregion buttons --> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| 
 | ||||
| <!-- #docregion name-change-log --> | ||||
| <h4>Name change log</h4> | ||||
| <div *ngFor="let name of nameChangeLog">{{name}}</div> | ||||
| <!-- #enddocregion name-change-log --> | ||||
| @ -0,0 +1,108 @@ | ||||
| // #docplaster
 | ||||
| // #docregion
 | ||||
| import { Component, Input, OnChanges }       from '@angular/core'; | ||||
| import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; | ||||
| 
 | ||||
| import { Address, Hero, states } from './data-model'; | ||||
| // #docregion import-service
 | ||||
| import { HeroService }           from './hero.service'; | ||||
| // #enddocregion import-service
 | ||||
| 
 | ||||
| // #docregion metadata
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail', | ||||
|   templateUrl: './hero-detail.component.html' | ||||
| }) | ||||
| // #enddocregion metadata
 | ||||
| export class HeroDetailComponent implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   heroForm: FormGroup; | ||||
|   // #docregion log-name-change
 | ||||
|   nameChangeLog: string[] = []; | ||||
|   // #enddocregion log-name-change
 | ||||
|   states = states; | ||||
| 
 | ||||
|   // #docregion ctor
 | ||||
|   constructor( | ||||
|     private fb: FormBuilder, | ||||
|     private heroService: HeroService) { | ||||
| 
 | ||||
|     this.createForm(); | ||||
|     this.logNameChange(); | ||||
|   } | ||||
|   // #enddocregion ctor
 | ||||
| 
 | ||||
|   createForm() { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       secretLairs: this.fb.array([]), | ||||
|       power: '', | ||||
|       sidekick: '' | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|     this.setAddresses(this.hero.addresses); | ||||
|   } | ||||
| 
 | ||||
|   get secretLairs(): FormArray { | ||||
|     return this.heroForm.get('secretLairs') as FormArray; | ||||
|   }; | ||||
| 
 | ||||
|   setAddresses(addresses: Address[]) { | ||||
|     const addressFGs = addresses.map(address => this.fb.group(address)); | ||||
|     const addressFormArray = this.fb.array(addressFGs); | ||||
|     this.heroForm.setControl('secretLairs', addressFormArray); | ||||
|   } | ||||
| 
 | ||||
|   addLair() { | ||||
|     this.secretLairs.push(this.fb.group(new Address())); | ||||
|   } | ||||
| 
 | ||||
|   // #docregion on-submit
 | ||||
|   onSubmit() { | ||||
|     this.hero = this.prepareSaveHero(); | ||||
|     this.heroService.updateHero(this.hero).subscribe(/* error handling */); | ||||
|     this.ngOnChanges(); | ||||
|   } | ||||
|   // #enddocregion on-submit
 | ||||
| 
 | ||||
|   // #docregion prepare-save-hero
 | ||||
|   prepareSaveHero(): Hero { | ||||
|     const formModel = this.heroForm.value; | ||||
| 
 | ||||
|     // deep copy of form model lairs
 | ||||
|     const secretLairsDeepCopy: Address[] = formModel.secretLairs.map( | ||||
|       (address: Address) => Object.assign({}, address) | ||||
|     ); | ||||
| 
 | ||||
|     // return new `Hero` object containing a combination of original hero value(s)
 | ||||
|     // and deep copies of changed form model values
 | ||||
|     const saveHero: Hero = { | ||||
|       id: this.hero.id, | ||||
|       name: formModel.name as string, | ||||
|       // addresses: formModel.secretLairs // <-- bad!
 | ||||
|       addresses: secretLairsDeepCopy | ||||
|     }; | ||||
|     return saveHero; | ||||
|   } | ||||
|   // #enddocregion prepare-save-hero
 | ||||
| 
 | ||||
|   // #docregion revert
 | ||||
|   revert() { this.ngOnChanges(); } | ||||
|   // #enddocregion revert
 | ||||
| 
 | ||||
|   // #docregion log-name-change
 | ||||
|   logNameChange() { | ||||
|     const nameControl = this.heroForm.get('name'); | ||||
|     nameControl.valueChanges.forEach( | ||||
|       (value: string) => this.nameChangeLog.push(value) | ||||
|     ); | ||||
|   } | ||||
|   // #enddocregion log-name-change
 | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| <!-- #docregion --> | ||||
| <nav> | ||||
|   <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a> | ||||
| </nav> | ||||
| 
 | ||||
| <div *ngIf="selectedHero"> | ||||
|   <hero-detail [hero]="selectedHero"></hero-detail> | ||||
| </div> | ||||
| @ -0,0 +1,17 @@ | ||||
| <!-- #docregion --> | ||||
| <h3 *ngIf="isLoading"><i>Loading heroes ... </i></h3> | ||||
| <h3 *ngIf="!isLoading">Select a hero:</h3> | ||||
| 
 | ||||
| <nav> | ||||
|   <button (click)="getHeroes()" class="btn btn-primary">Refresh</button> | ||||
|   <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a> | ||||
| </nav> | ||||
| 
 | ||||
| <div *ngIf="selectedHero"> | ||||
|   <hr> | ||||
|   <h2>Hero Detail</h2> | ||||
|   <h3>Editing: {{selectedHero.name}}</h3> | ||||
|   <!-- #docregion hero-binding --> | ||||
|   <hero-detail [hero]="selectedHero"></hero-detail> | ||||
|   <!-- #enddocregion hero-binding --> | ||||
| </div> | ||||
| @ -0,0 +1,32 @@ | ||||
| // #docregion
 | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { Observable }        from 'rxjs/Observable'; | ||||
| import 'rxjs/add/operator/finally'; | ||||
| 
 | ||||
| import { Hero }        from './data-model'; | ||||
| import { HeroService } from './hero.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-list', | ||||
|   templateUrl: 'hero-list.component.html' | ||||
| }) | ||||
| export class HeroListComponent implements OnInit { | ||||
|   heroes: Observable<Hero[]>; | ||||
|   isLoading = false; | ||||
|   selectedHero: Hero; | ||||
| 
 | ||||
|   constructor(private heroService: HeroService) { } | ||||
| 
 | ||||
|   ngOnInit() { this.getHeroes(); } | ||||
| 
 | ||||
|   getHeroes() { | ||||
|     this.isLoading = true; | ||||
|     this.heroes = this.heroService.getHeroes() | ||||
|                       // Todo: error handling
 | ||||
|                       .finally(() => this.isLoading = false); | ||||
|     this.selectedHero = undefined; | ||||
|   } | ||||
| 
 | ||||
|   select(hero: Hero) { this.selectedHero = hero; } | ||||
| } | ||||
							
								
								
									
										26
									
								
								public/docs/_examples/reactive-forms/ts/app/hero.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | ||||
| // #docregion
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { Observable } from 'rxjs/Observable'; | ||||
| import { of }         from 'rxjs/observable/of'; | ||||
| import 'rxjs/add/operator/delay'; | ||||
| 
 | ||||
| import { Hero, heroes } from './data-model'; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class HeroService { | ||||
| 
 | ||||
|   delayMs = 500; | ||||
| 
 | ||||
|   // Fake server get; assume nothing can go wrong
 | ||||
|   getHeroes(): Observable<Hero[]> { | ||||
|     return of(heroes).delay(this.delayMs); // simulate latency with delay
 | ||||
|   } | ||||
| 
 | ||||
|   // Fake server update; assume nothing can go wrong
 | ||||
|   updateHero(hero: Hero): Observable<Hero>  { | ||||
|     const oldHero = heroes.find(h => h.id === hero.id); | ||||
|     const newHero = Object.assign(oldHero, hero); // Demo: mutate cached hero
 | ||||
|     return of(newHero).delay(this.delayMs); // simulate latency with delay
 | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,5 @@ | ||||
| // tslint:disable:no-unused-variable
 | ||||
| import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||||
| import { AppModule } from './app.module'; | ||||
| 
 | ||||
| platformBrowserDynamic().bootstrapModule(AppModule); | ||||
							
								
								
									
										6
									
								
								public/docs/_examples/reactive-forms/ts/app/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,6 @@ | ||||
| // tslint:disable:no-unused-variable
 | ||||
| import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||||
| import { AppModule }  from './app.module';  // just the final version
 | ||||
| import { DemoModule } from './demo.module'; // demo picker
 | ||||
| 
 | ||||
| platformBrowserDynamic().bootstrapModule(DemoModule); // (AppModule);
 | ||||
							
								
								
									
										20
									
								
								public/docs/_examples/reactive-forms/ts/final.plnkr.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,20 @@ | ||||
| { | ||||
|   "description": "Angular Reactive Forms (final)", | ||||
|   "files":[ | ||||
|     "styles.css", | ||||
| 
 | ||||
|     "app/app.component.ts", | ||||
|     "app/app.module.ts", | ||||
|     "app/data-model.ts", | ||||
|     "app/hero.service.ts", | ||||
|     "app/hero-detail.component.html", | ||||
|     "app/hero-detail.component.ts", | ||||
|     "app/hero-list.component.html", | ||||
|     "app/hero-list.component.ts", | ||||
| 
 | ||||
|     "app/main-final.ts", | ||||
|     "index-final.html" | ||||
|   ], | ||||
|   "main": "index-final.html", | ||||
|   "tags": ["reactive", "forms"] | ||||
| } | ||||
							
								
								
									
										31
									
								
								public/docs/_examples/reactive-forms/ts/index-final.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,31 @@ | ||||
| <!DOCTYPE html> | ||||
| <!-- #docregion --> | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Hero Form</title> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| 
 | ||||
|     <link rel="stylesheet" href="styles.css"> | ||||
|      <!-- #docregion bootstrap --> | ||||
|     <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css"> | ||||
|      <!-- #enddocregion bootstrap --> | ||||
| 
 | ||||
|     <!-- Polyfills for older browsers --> | ||||
|     <script src="node_modules/core-js/client/shim.min.js"></script> | ||||
| 
 | ||||
|     <script src="node_modules/zone.js/dist/zone.js"></script> | ||||
|     <script src="node_modules/reflect-metadata/Reflect.js"></script> | ||||
|     <script src="node_modules/systemjs/dist/system.src.js"></script> | ||||
| 
 | ||||
|     <script src="systemjs.config.js"></script> | ||||
|     <script> | ||||
|       System.import('app/main-final').catch(function(err){ console.error(err); }); | ||||
|     </script> | ||||
|   </head> | ||||
| 
 | ||||
|   <body> | ||||
|     <my-app>Loading...</my-app> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
							
								
								
									
										31
									
								
								public/docs/_examples/reactive-forms/ts/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,31 @@ | ||||
| <!DOCTYPE html> | ||||
| <!-- #docregion --> | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Hero Form</title> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| 
 | ||||
|     <link rel="stylesheet" href="styles.css"> | ||||
|      <!-- #docregion bootstrap --> | ||||
|     <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css"> | ||||
|      <!-- #enddocregion bootstrap --> | ||||
| 
 | ||||
|     <!-- Polyfills for older browsers --> | ||||
|     <script src="node_modules/core-js/client/shim.min.js"></script> | ||||
| 
 | ||||
|     <script src="node_modules/zone.js/dist/zone.js"></script> | ||||
|     <script src="node_modules/reflect-metadata/Reflect.js"></script> | ||||
|     <script src="node_modules/systemjs/dist/system.src.js"></script> | ||||
| 
 | ||||
|     <script src="systemjs.config.js"></script> | ||||
|     <script> | ||||
|       System.import('app').catch(function(err){ console.error(err); }); | ||||
|     </script> | ||||
|   </head> | ||||
| 
 | ||||
|   <body> | ||||
|     <my-app>Loading...</my-app> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
							
								
								
									
										14
									
								
								public/docs/_examples/reactive-forms/ts/plnkr.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,14 @@ | ||||
| { | ||||
|   "description": "Angular Reactive Forms (Demo runner)", | ||||
|   "files":[ | ||||
|     "!**/*.d.ts", | ||||
|     "!**/*.js", | ||||
| 
 | ||||
|     "!app/app.component.1.ts", | ||||
|     "!app/hero-list.component.1.html", | ||||
| 
 | ||||
|     "!app/main-final.ts", | ||||
|     "!index-final.html" | ||||
|   ], | ||||
|   "tags": ["reactive", "forms"] | ||||
| } | ||||
| @ -138,6 +138,11 @@ | ||||
|     "intro": "Angular's hierarchical dependency injection system supports nested injectors in parallel with the component tree." | ||||
|   }, | ||||
| 
 | ||||
|   "reactive-forms": { | ||||
|     "title": "Reactive Forms", | ||||
|     "intro": "Create a reactive form using FormBuilder, groups, and arrays." | ||||
|   }, | ||||
| 
 | ||||
|   "server-communication": { | ||||
|     "title": "HTTP Client", | ||||
|     "intro": "Use an HTTP Client to talk to a remote server." | ||||
|  | ||||
| @ -5,6 +5,14 @@ block includes | ||||
|   The Angular documentation is a living document with continuous improvements. | ||||
|   This log calls attention to recent significant changes. | ||||
| 
 | ||||
|   ## NEW: Reactive Forms guide (2017-01-31) | ||||
|   The new [**Reactive Forms**](reactive-forms.html) guide explains how and why to build a "reactive form". | ||||
|   "Reactive Forms" are the code-based counterpart to the declarative "Template Driven" forms approach | ||||
|   introduced in the [Forms](forms.html) guide. | ||||
|   Check it out before you decide how to add forms to your app.  | ||||
|   Remember also that you can use both techniques in the same app,  | ||||
|   choosing the approach that best fits each scenario. | ||||
| 
 | ||||
|   ## NEW: Deployment guide (2017-01-30) | ||||
|   The new [Deployment](deployment.html) guide describes techniques for putting your application on a server. | ||||
|   It includes important advice on optimizing for production. | ||||
|  | ||||
| @ -668,7 +668,7 @@ figure.image-display | ||||
|       .file hero-form.component.html | ||||
|       .file hero-form.component.ts | ||||
|       .file main.ts | ||||
|     .file node_modules ...   | ||||
|     .file node_modules ... | ||||
|     .file index.html | ||||
|     .file package.json | ||||
|     .file tsconfig.json | ||||
|  | ||||
							
								
								
									
										1045
									
								
								public/docs/ts/latest/guide/reactive-forms.jade
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.5 KiB | 
| After Width: | Height: | Size: 3.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/resources/images/devguide/reactive-forms/hero-detail.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/resources/images/devguide/reactive-forms/hero-list.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/resources/images/devguide/reactive-forms/json-output.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 39 KiB | 
| After Width: | Height: | Size: 26 KiB | 
| After Width: | Height: | Size: 6.9 KiB | 
| After Width: | Height: | Size: 59 KiB | 
| @ -0,0 +1,948 @@ | ||||
| <html lang="en"><head></head><body><form id="mainForm" method="post" action="https://embed.plnkr.co?show=preview" target="_self"><input type="hidden" name="entries[app/app.component.ts][content]" value="import { Component } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'my-app', | ||||
|   template: ` | ||||
|   <div class="container"> | ||||
|     <h1>Reactive Forms</h1> | ||||
|     <hero-list></hero-list> | ||||
|   </div>` | ||||
| }) | ||||
| export class AppComponent { } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/app.component.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/app.module.ts][content]" value="import { NgModule }            from '@angular/core'; | ||||
| import { BrowserModule }       from '@angular/platform-browser'; | ||||
| import { ReactiveFormsModule } from '@angular/forms';  // <-- #1 import module | ||||
| 
 | ||||
| import { AppComponent }        from './app.component'; | ||||
| import { HeroDetailComponent } from './hero-detail.component'; // <-- #1 import component | ||||
| import { HeroListComponent }   from './hero-list.component'; | ||||
| 
 | ||||
| import { HeroService }         from './hero.service'; //  <-- #1 import service | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|     ReactiveFormsModule // <-- #2 add to Angular module imports | ||||
|   ], | ||||
|   declarations: [ | ||||
|     AppComponent, | ||||
|     HeroDetailComponent, // <-- #3 declare app component | ||||
|     HeroListComponent | ||||
|   ], | ||||
|   providers: [ HeroService ], // <-- #4 provide HeroService | ||||
|   bootstrap: [ AppComponent ] | ||||
| }) | ||||
| export class AppModule { } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/app.module.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/data-model.ts][content]" value="export class Hero { | ||||
|   id = 0; | ||||
|   name = ''; | ||||
|   addresses: Address[]; | ||||
| } | ||||
| 
 | ||||
| export class Address { | ||||
|   street = ''; | ||||
|   city   = ''; | ||||
|   state  = ''; | ||||
|   zip    = ''; | ||||
| } | ||||
| 
 | ||||
| export const heroes: Hero[] = [ | ||||
|   { | ||||
|     id: 1, | ||||
|     name: 'Whirlwind', | ||||
|     addresses: [ | ||||
|       {street: '123 Main',  city: 'Anywhere', state: 'CA',  zip: '94801'}, | ||||
|       {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'}, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     id: 2, | ||||
|     name: 'Bombastic', | ||||
|     addresses: [ | ||||
|       {street: '789 Elm',  city: 'Smallville', state: 'OH',  zip: '04501'}, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     id: 3, | ||||
|     name: 'Magneta', | ||||
|     addresses: [ ] | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| export const states = ['CA', 'MD', 'OH', 'VA']; | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/data-model.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/demo.component.ts][content]" value="/* tslint:disable:member-ordering */ | ||||
| import { Component } from '@angular/core'; | ||||
| 
 | ||||
| import { Hero }        from './data-model'; | ||||
| import { HeroService } from './hero.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'my-app', | ||||
|   templateUrl: 'demo.component.html' | ||||
| }) | ||||
| export class DemoComponent { | ||||
| 
 | ||||
|   demos: string[] = [ | ||||
|     'Simple FormControl', | ||||
|     'Simple FormBuilder group', | ||||
|     'Group with multiple controls', | ||||
|     'Nested FormBuilder group', | ||||
|     'PatchValue', | ||||
|     'SetValue', | ||||
|     'FormArray', | ||||
|     'Final'].map(n => n + ' Demo'); | ||||
| 
 | ||||
|   final = this.demos.length; | ||||
|   demo = this.final; // current demo | ||||
| 
 | ||||
|   heroes: Hero[]; | ||||
|   selectedHero: Hero; | ||||
| 
 | ||||
|   constructor(private heroService: HeroService) { } | ||||
| 
 | ||||
|   getHeroes() { | ||||
|     return this.heroService.getHeroes().then(heroes => this.heroes = heroes); | ||||
|   } | ||||
| 
 | ||||
|   select(hero: Hero) { this.selectedHero = hero; } | ||||
| 
 | ||||
|   selectDemo(demo: number) { | ||||
|     this.demo = demo + 1; | ||||
|     this.heroes = undefined; | ||||
|     this.selectedHero = undefined; | ||||
|     this.getHeroes(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/demo.component.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/demo.module.ts][content]" value="import { NgModule }             from '@angular/core'; | ||||
| import { BrowserModule }        from '@angular/platform-browser'; | ||||
| import { ReactiveFormsModule }  from '@angular/forms';  // <-- #1 import the module | ||||
| 
 | ||||
| import { DemoComponent }        from './demo.component'; | ||||
| import { components }           from './hero-detail-versions.component'; | ||||
| import { HeroDetailComponent }  from './hero-detail.component'; | ||||
| import { HeroListComponent }    from './hero-list.component'; | ||||
| 
 | ||||
| import { HeroService }          from './hero.service'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|     ReactiveFormsModule | ||||
|   ], | ||||
|   declarations: [ | ||||
|     DemoComponent, HeroDetailComponent, HeroListComponent, | ||||
|     ...components | ||||
|   ], | ||||
|   providers: [ HeroService ], | ||||
|   bootstrap: [ DemoComponent ] | ||||
| }) | ||||
| export class DemoModule { } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/demo.module.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail-versions.component.ts][content]" value="/* tslint:disable:component-class-suffix */ | ||||
| 
 | ||||
| import { Component, Input, OnChanges } from '@angular/core'; | ||||
| import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; | ||||
| import { Address, Hero, states } from './data-model'; | ||||
| 
 | ||||
| //////// 1 //////////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-1', | ||||
|   templateUrl: './hero-detail-1.component.html' | ||||
| }) | ||||
| 
 | ||||
| export class HeroDetailComponent1 { | ||||
|   heroForm = new FormGroup ({ | ||||
|     name: new FormControl() | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| //////// 2 //////////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-2', | ||||
|   templateUrl: './hero-detail-2.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent2 { | ||||
|   heroForm: FormGroup; // <--- heroForm is of type FormGroup | ||||
|   constructor(private fb: FormBuilder) { // <--- inject FormBuilder | ||||
|     this.heroForm = this.fb.group({ // <--- make this.form a FormBuilder group | ||||
|       name: '', // <--- the FormControl called "name" | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| //////// 3 //////////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-3', | ||||
|   templateUrl: './hero-detail-3.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent3 { | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       street: '', | ||||
|       city: '', | ||||
|       state: '', | ||||
|       zip: '' | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ////////// 4 ///////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-4', | ||||
|   templateUrl: './hero-detail-4.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent4 { | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ // <-- the parent FormGroup | ||||
|       name: '', | ||||
|       address: this.fb.group({ // <-- the child FormGroup | ||||
|         street: '', | ||||
|         city: '', | ||||
|         state: '', | ||||
|         zip: '' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ////////// 5 //////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-5', | ||||
|   templateUrl: './hero-detail-4.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent5 implements OnChanges { // <-- implements OnChanges | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       address: this.fb.group({ | ||||
|         street: '', | ||||
|         city: '', | ||||
|         state: '', | ||||
|         zip: '' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges() { // <-- wrap patchValue in ngOnChanges | ||||
|     this.heroForm.patchValue({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| ///////// 6 //////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-6', | ||||
|   templateUrl: './hero-detail-4.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent6 implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       address: this.fb.group(new Address()) // <-- a FormGroup with a new address | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset(); | ||||
|     this.heroForm.setValue({ | ||||
|       name:    this.hero.name, | ||||
|       address: this.hero.addresses[0] || new Address() | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| ////////// 7 //////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-7', | ||||
|   templateUrl: './hero-detail-5.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent7 implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       secretLairs: this.fb.array([]) // <-- secretLairs as an empty FormArray | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get secretLairs(): FormArray { | ||||
|     return this.heroForm.get('secretLairs') as FormArray; | ||||
|   }; | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset(); | ||||
|     this.heroForm.patchValue({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|     this.setAddresses(this.hero.addresses); | ||||
|   } | ||||
|   setAddresses(addresses: Address[]) { | ||||
|     const addressFGs = addresses.map(address => this.fb.group(address)); | ||||
|     const addressFormArray = this.fb.array(addressFGs); | ||||
|     this.heroForm.setControl('secretLairs', addressFormArray); | ||||
|   } | ||||
| 
 | ||||
|   addLair() { | ||||
|     this.secretLairs.push(this.fb.group(new Address())); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| //////////////////////////////// | ||||
| 
 | ||||
| export const components = [ | ||||
|   HeroDetailComponent1, | ||||
|   HeroDetailComponent2, | ||||
|   HeroDetailComponent3, | ||||
|   HeroDetailComponent4, | ||||
|   HeroDetailComponent5, | ||||
|   HeroDetailComponent6, | ||||
|   HeroDetailComponent7 | ||||
| ]; | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/hero-detail-versions.component.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail.component.ts][content]" value="// tslint:disable:no-unused-variable | ||||
| import { Component, EventEmitter, Input, Output, OnChanges } from '@angular/core'; | ||||
| import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; | ||||
| import { Address, Hero, states } from './data-model'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail', | ||||
|   templateUrl: './hero-detail.component.html' | ||||
| }) | ||||
| 
 | ||||
| export class HeroDetailComponent implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
|   @Output() save = new EventEmitter<Hero>(); | ||||
| 
 | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       secretLairs: this.fb.array([]) | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get secretLairs(): FormArray { | ||||
|     return this.heroForm.get('secretLairs') as FormArray; | ||||
|   }; | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset(); | ||||
|     this.heroForm.patchValue({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|     this.setAddresses(this.hero.addresses); | ||||
|   } | ||||
| 
 | ||||
|    setAddresses(addresses: Address[]) { | ||||
|     const addressFGs = addresses.map(address => this.fb.group(address)); | ||||
|     const addressFormArray = this.fb.array(addressFGs); | ||||
|     this.heroForm.setControl('secretLairs', addressFormArray); | ||||
|   } | ||||
| 
 | ||||
|   addLair() { | ||||
|     this.secretLairs.push(this.fb.group(new Address())); | ||||
|   } | ||||
| 
 | ||||
|   onSubmit() { | ||||
|     const formModel = this.heroForm.value; | ||||
| 
 | ||||
|     const newHero: Hero = { | ||||
|       id: this.hero.id, | ||||
|       name: formModel.name as string, | ||||
|       addresses: formModel.secretLairs | ||||
|     }; | ||||
| 
 | ||||
|     this.save.emit(newHero); // let parent decide what to do | ||||
| 
 | ||||
|     console.log(newHero); // diagnostic | ||||
|   } | ||||
| 
 | ||||
|   revert() { this.ngOnChanges(); } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/hero-detail.component.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-list.component.ts][content]" value="import { Component, OnInit } from '@angular/core'; | ||||
| 
 | ||||
| import { Hero }        from './data-model'; | ||||
| import { HeroService } from './hero.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-list', | ||||
|   templateUrl: 'hero-list.component.html' | ||||
| }) | ||||
| export class HeroListComponent implements OnInit { | ||||
|   heroes: Hero[]; | ||||
|   selectedHero: Hero; | ||||
| 
 | ||||
|   constructor(private heroService: HeroService) { } | ||||
| 
 | ||||
|   ngOnInit() { this.getHeroes(); } | ||||
| 
 | ||||
|   getHeroes() { | ||||
|     this.heroService.getHeroes() | ||||
|         .then(heroes => this.heroes = heroes); | ||||
|   } | ||||
| 
 | ||||
|   select(hero: Hero) { this.selectedHero = hero; } | ||||
| 
 | ||||
|   onSave(hero: Hero) { | ||||
|     this.heroService.updateHero(hero) | ||||
|         .then(() => { | ||||
|           this.getHeroes(); | ||||
|           const newHero = this.heroes.find(h => h === hero); | ||||
|           this.select(newHero); | ||||
|         }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/hero-list.component.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/hero.service.ts][content]" value="import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { Hero, heroes } from './data-model'; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class HeroService { | ||||
| 
 | ||||
|   getHeroes(): Promise<Hero[]> { | ||||
|     return new Promise<Hero[]>(resolve => { | ||||
|       // simulate server latency with delay | ||||
|       setTimeout(() => resolve(heroes), 500); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   updateHero(hero: Hero): Promise<Hero>  { | ||||
|     // Demo: faking server update; nothing can go wrong ;-) | ||||
|     return new Promise<Hero>(resolve => { | ||||
|       // simulate server latency with delay | ||||
|       const ix = heroes.findIndex(h => h.id === hero.id); | ||||
|       setTimeout(() => { | ||||
|         heroes[ix] = hero; | ||||
|         resolve(hero); | ||||
|       }, 500); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/hero.service.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/main-final.ts][content]" value="// tslint:disable:no-unused-variable | ||||
| import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||||
| import { AppModule } from './app.module'; | ||||
| 
 | ||||
| platformBrowserDynamic().bootstrapModule(AppModule); | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/main-final.ts][encoding]" value="utf8"><input type="hidden" name="entries[app/main.ts][content]" value="// tslint:disable:no-unused-variable | ||||
| import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||||
| import { AppModule }  from './app.module';  // just the final version | ||||
| import { DemoModule } from './demo.module'; // demo picker | ||||
| 
 | ||||
| platformBrowserDynamic().bootstrapModule(DemoModule); // (AppModule); | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[app/main.ts][encoding]" value="utf8"><input type="hidden" name="entries[styles.css][content]" value="/* Master Styles */ | ||||
| h1 { | ||||
|   color: #369; | ||||
|   font-family: Arial, Helvetica, sans-serif; | ||||
|   font-size: 250%; | ||||
| } | ||||
| h2, h3 { | ||||
|   color: #444; | ||||
|   font-family: Arial, Helvetica, sans-serif; | ||||
|   font-weight: lighter; | ||||
| } | ||||
| body { | ||||
|   margin: 2em; | ||||
| } | ||||
| body, input[text], button { | ||||
|   color: #888; | ||||
|   font-family: Cambria, Georgia; | ||||
| } | ||||
| a { | ||||
|   cursor: pointer; | ||||
|   cursor: hand; | ||||
| } | ||||
| button { | ||||
|   font-family: Arial; | ||||
|   background-color: #eee; | ||||
|   border: none; | ||||
|   padding: 5px 10px; | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
|   cursor: hand; | ||||
| } | ||||
| button:hover { | ||||
|   background-color: #cfd8dc; | ||||
| } | ||||
| button:disabled { | ||||
|   background-color: #eee; | ||||
|   color: #aaa; | ||||
|   cursor: auto; | ||||
| } | ||||
| 
 | ||||
| /* Navigation link styles */ | ||||
| nav a { | ||||
|   padding: 5px 10px; | ||||
|   text-decoration: none; | ||||
|   margin-right: 10px; | ||||
|   margin-top: 10px; | ||||
|   display: inline-block; | ||||
|   background-color: #eee; | ||||
|   border-radius: 4px; | ||||
| } | ||||
| nav a:visited, a:link { | ||||
|   color: #607D8B; | ||||
| } | ||||
| nav a:hover { | ||||
|   color: #039be5; | ||||
|   background-color: #CFD8DC; | ||||
| } | ||||
| nav a.active { | ||||
|   color: #039be5; | ||||
| } | ||||
| 
 | ||||
| /* items class */ | ||||
| .items { | ||||
|   margin: 0 0 2em 0; | ||||
|   list-style-type: none; | ||||
|   padding: 0; | ||||
|   width: 24em; | ||||
| } | ||||
| .items li { | ||||
|   cursor: pointer; | ||||
|   position: relative; | ||||
|   left: 0; | ||||
|   background-color: #EEE; | ||||
|   margin: .5em; | ||||
|   padding: .3em 0; | ||||
|   height: 1.6em; | ||||
|   border-radius: 4px; | ||||
| } | ||||
| .items li:hover { | ||||
|   color: #607D8B; | ||||
|   background-color: #DDD; | ||||
|   left: .1em; | ||||
| } | ||||
| .items li.selected { | ||||
|   background-color: #CFD8DC; | ||||
|   color: white; | ||||
| } | ||||
| .items li.selected:hover { | ||||
|   background-color: #BBD8DC; | ||||
| } | ||||
| .items .text { | ||||
|   position: relative; | ||||
|   top: -3px; | ||||
| } | ||||
| .items .badge { | ||||
|   display: inline-block; | ||||
|   font-size: small; | ||||
|   color: white; | ||||
|   padding: 0.8em 0.7em 0 0.7em; | ||||
|   background-color: #607D8B; | ||||
|   line-height: 1em; | ||||
|   position: relative; | ||||
|   left: -1px; | ||||
|   top: -4px; | ||||
|   height: 1.8em; | ||||
|   margin-right: .8em; | ||||
|   border-radius: 4px 0 0 4px; | ||||
| } | ||||
| /* everywhere else */ | ||||
| * { | ||||
|   font-family: Arial, Helvetica, sans-serif; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="entries[styles.css][encoding]" value="utf8"><input type="hidden" name="entries[app/demo.component.html][content]" value="<div class="container"> | ||||
| <h1>Reactive Forms</h1> | ||||
| <h4><i>Pick a demo:</i> | ||||
|   <select [selectedIndex]="demo - 1" (change)="selectDemo($event.target.selectedIndex)"> | ||||
|     <option *ngFor="let demo of demos">{{demo}}</option> | ||||
|   </select> | ||||
| </h4> | ||||
| 
 | ||||
| <hr> | ||||
| 
 | ||||
| <div class="demo"> | ||||
|   <hero-list *ngIf="demo===final"></hero-list> | ||||
| 
 | ||||
|   <hero-detail-1 *ngIf="demo===1"></hero-detail-1> | ||||
|   <hero-detail-2 *ngIf="demo===2"></hero-detail-2> | ||||
|   <hero-detail-3 *ngIf="demo===3"></hero-detail-3> | ||||
|   <hero-detail-4 *ngIf="demo===4"></hero-detail-4> | ||||
| 
 | ||||
|   <div *ngIf="demo >= 5 && demo !== final" > | ||||
| 
 | ||||
|     <p *ngIf="!heroes"><b><i>Loading heroes ... </i></b></p> | ||||
|     <h3 *ngIf="heroes">Select a hero:</h3> | ||||
| 
 | ||||
|     <nav> | ||||
|       <a *ngFor="let hero of heroes" (click)="select(hero)">{{hero.name}}</a> | ||||
|     </nav> | ||||
| 
 | ||||
|     <div *ngIf="selectedHero"> | ||||
|       <hr> | ||||
|       <h2>Hero Detail</h2> | ||||
|       <h3>Editing: {{selectedHero.name}}</h3> | ||||
|       <hero-detail-5 [hero]=selectedHero *ngIf="demo===5"></hero-detail-5> | ||||
|       <hero-detail-6 [hero]=selectedHero *ngIf="demo===6"></hero-detail-6> | ||||
|       <hero-detail-7 [hero]=selectedHero *ngIf="demo===7"></hero-detail-7> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/demo.component.html][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail-1.component.html][content]" value="<h2>Hero Detail</h2> | ||||
| <h3><i>A simple form with a single FormControl</i></h3> | ||||
| <form novalidate [formGroup]="heroForm"> | ||||
|   <div class="form-group"> | ||||
|     <label>Name:</label> | ||||
|     <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/hero-detail-1.component.html][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail-2.component.html][content]" value="<h2>Hero Detail</h2> | ||||
| <h3><i>A simple form with a single FormControl using FormBuilder</i></h3> | ||||
| <form novalidate [formGroup]="heroForm"> | ||||
|   <div class="form-group"> | ||||
|     <label>Name:</label> | ||||
|     <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>Form: {{ heroForm.value | json }}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/hero-detail-2.component.html][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail-3.component.html][content]" value="<h2>Hero Detail</h2> | ||||
| <h3><i>A simple form with multiple FormControls in a single FormBuilder group</i></h3> | ||||
| <form novalidate [formGroup]="heroForm"> | ||||
|   <div class="form-group"> | ||||
|     <label>Name:</label> | ||||
|     <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Street:</label> | ||||
|     <input class="form-control" formControlName="street"> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>City:</label> | ||||
|     <input class="form-control" formControlName="city"> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>State:</label> | ||||
|     <select class="form-control" formControlName="state"> | ||||
|         <option *ngFor="let state of states">{{state}}</option> | ||||
|     </select> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Zip Code:</label> | ||||
|     <input class="form-control" formControlName="zip"> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/hero-detail-3.component.html][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail-4.component.html][content]" value="<form novalidate [formGroup]="heroForm"> | ||||
|     <div class="form-group"> | ||||
|       <label>Name:</label> | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </div> | ||||
|     <div formGroupName="address" class="well well-lg"> | ||||
|       <h4>Secret Lair</h4> | ||||
|       <div class="form-group"> | ||||
|         <label>Street:</label> | ||||
|         <input class="form-control" formControlName="street"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label>City:</label> | ||||
|         <input class="form-control" formControlName="city"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label>State:</label> | ||||
|         <select class="form-control" formControlName="state"> | ||||
|           <option *ngFor="let state of states">{{state}}</option> | ||||
|         </select> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label>Zip Code:</label> | ||||
|         <input class="form-control" formControlName="zip"> | ||||
|       </div> | ||||
|     </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| <h4>Extra info for the curious:</h4> | ||||
| <p>Name value: {{ heroForm.get('name').value }}</p> | ||||
| 
 | ||||
| <p>Street value: {{ heroForm.get('address.street').value}}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/hero-detail-4.component.html][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail-5.component.html][content]" value="<h3><i>Using FormArray to add groups</i></h3> | ||||
| 
 | ||||
| <form novalidate [formGroup]="heroForm" (ngSubmit)="onSubmit(heroForm)"> | ||||
|     <p>Form Changed: {{ heroForm.dirty }}</p> | ||||
| 
 | ||||
|     <div class="form-group"> | ||||
|       <label>Name:</label> | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </div> | ||||
|     <div formArrayName="secretLairs" class="well well-lg"> | ||||
|       <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > | ||||
|         <!-- The repeated address template --> | ||||
|         <h4>Address #{{i + 1}}</h4> | ||||
|         <div style="margin-left: 1em;"> | ||||
|           <div class="form-group"> | ||||
|             <label>Street:</label> | ||||
|             <input class="form-control" formControlName="street"> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label>City:</label> | ||||
|             <input class="form-control" formControlName="city"> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label>State:</label> | ||||
|             <select class="form-control" formControlName="state"> | ||||
|               <option *ngFor="let state of states">{{state}}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label>Zip Code:</label> | ||||
|             <input class="form-control" formControlName="zip"> | ||||
|           </div> | ||||
|         </div> | ||||
|         <br> | ||||
|         <!-- End of the repeated address template --> | ||||
|       </div> | ||||
|       <button (click)="addLair()">Add a Secret Lair</button> <!-- add button --> | ||||
|     </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/hero-detail-5.component.html][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-detail.component.html][content]" value="<form novalidate [formGroup]="heroForm" (ngSubmit)="onSubmit(heroForm)"> | ||||
|   <div style="margin-bottom: 1em"> | ||||
|     <button type="submit" | ||||
|             [disabled]="!heroForm.dirty" class="btn btn-success">Save</button> &nbsp; | ||||
|     <button type="reset" (click)="revert()" | ||||
|             [disabled]="!heroForm.dirty" class="btn btn-danger">Revert</button> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="form-group"> | ||||
|       <label>Name:</label> | ||||
|       <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
| 
 | ||||
|   <div formArrayName="secretLairs" class="well well-lg"> | ||||
|     <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > | ||||
|       <!-- The repeated address template --> | ||||
|       <h4>Address #{{i + 1}}</h4> | ||||
|       <div style="margin-left: 1em;"> | ||||
|         <div class="form-group"> | ||||
|           <label>Street:</label> | ||||
|           <input class="form-control" formControlName="street"> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>City:</label> | ||||
|           <input class="form-control" formControlName="city"> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>State:</label> | ||||
|           <select class="form-control" formControlName="state"> | ||||
|             <option *ngFor="let state of states">{{state}}</option> | ||||
|           </select> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>Zip Code:</label> | ||||
|           <input class="form-control" formControlName="zip"> | ||||
|         </div> | ||||
|       </div> | ||||
|       <br> | ||||
|       <!-- End of the repeated address template --> | ||||
|     </div> | ||||
|     <button (click)="addLair()">Add a Secret Lair</button> <!-- add button --> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/hero-detail.component.html][encoding]" value="utf8"><input type="hidden" name="entries[app/hero-list.component.html][content]" value="<p *ngIf="!heroes"><b><i>Loading heroes ... </i></b></p> | ||||
| <h3 *ngIf="heroes">Select a hero:</h3> | ||||
| 
 | ||||
| <nav> | ||||
|   <a *ngFor="let hero of heroes" (click)="select(hero)">{{hero.name}}</a> | ||||
| </nav> | ||||
| 
 | ||||
| <div *ngIf="selectedHero"> | ||||
|   <hr> | ||||
|   <h2>Hero Detail</h2> | ||||
|   <h3>Editing: {{selectedHero.name}}</h3> | ||||
|   <hero-detail [hero]="selectedHero" (save)="onSave($event)"></hero-detail> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[app/hero-list.component.html][encoding]" value="utf8"><input type="hidden" name="entries[index.html][content]" value="<!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Hero Form</title> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| 
 | ||||
|     <link rel="stylesheet" href="styles.css"> | ||||
|     <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css"> | ||||
| 
 | ||||
|     <!-- Polyfills for older browsers --> | ||||
|     <script src="https://unpkg.com/core-js/client/shim.min.js"></script> | ||||
| 
 | ||||
|     <script src="https://unpkg.com/zone.js@0.7.2?main=browser"></script> | ||||
|     <script src="https://unpkg.com/reflect-metadata@0.1.8"></script> | ||||
|     <script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script> | ||||
| 
 | ||||
|     <script src="https://cdn.rawgit.com/angular/angular.io/b3c65a9/public/docs/_examples/_boilerplate/systemjs.config.web.js"></script> | ||||
|     <script> | ||||
|       System.import('app').catch(function(err){ console.error(err); }); | ||||
|     </script> | ||||
|   </head> | ||||
| 
 | ||||
|   <body> | ||||
|     <my-app>Loading...</my-app> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="entries[index.html][encoding]" value="utf8"><input type="hidden" name="tags[0]" value="angular"><input type="hidden" name="tags[1]" value="example"><input type="hidden" name="tags[2]" value="reactive"><input type="hidden" name="tags[3]" value="forms"><input type="hidden" name="title" value="Angular Example - Reactive Forms (final)"><input type="hidden" name="source[type]" value="Reactive Forms (final)"><input type="hidden" name="source[url]" value="https://angular.io"></form><script>document.getElementById("mainForm").submit();</script></body></html> | ||||
| @ -0,0 +1,948 @@ | ||||
| <html lang="en"><head></head><body><form id="mainForm" method="post" action="http://plnkr.co/edit/?p=preview" target="_self"><input type="hidden" name="files[app/app.component.ts]" value="import { Component } from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'my-app', | ||||
|   template: ` | ||||
|   <div class="container"> | ||||
|     <h1>Reactive Forms</h1> | ||||
|     <hero-list></hero-list> | ||||
|   </div>` | ||||
| }) | ||||
| export class AppComponent { } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/app.module.ts]" value="import { NgModule }            from '@angular/core'; | ||||
| import { BrowserModule }       from '@angular/platform-browser'; | ||||
| import { ReactiveFormsModule } from '@angular/forms';  // <-- #1 import module | ||||
| 
 | ||||
| import { AppComponent }        from './app.component'; | ||||
| import { HeroDetailComponent } from './hero-detail.component'; // <-- #1 import component | ||||
| import { HeroListComponent }   from './hero-list.component'; | ||||
| 
 | ||||
| import { HeroService }         from './hero.service'; //  <-- #1 import service | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|     ReactiveFormsModule // <-- #2 add to Angular module imports | ||||
|   ], | ||||
|   declarations: [ | ||||
|     AppComponent, | ||||
|     HeroDetailComponent, // <-- #3 declare app component | ||||
|     HeroListComponent | ||||
|   ], | ||||
|   providers: [ HeroService ], // <-- #4 provide HeroService | ||||
|   bootstrap: [ AppComponent ] | ||||
| }) | ||||
| export class AppModule { } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/data-model.ts]" value="export class Hero { | ||||
|   id = 0; | ||||
|   name = ''; | ||||
|   addresses: Address[]; | ||||
| } | ||||
| 
 | ||||
| export class Address { | ||||
|   street = ''; | ||||
|   city   = ''; | ||||
|   state  = ''; | ||||
|   zip    = ''; | ||||
| } | ||||
| 
 | ||||
| export const heroes: Hero[] = [ | ||||
|   { | ||||
|     id: 1, | ||||
|     name: 'Whirlwind', | ||||
|     addresses: [ | ||||
|       {street: '123 Main',  city: 'Anywhere', state: 'CA',  zip: '94801'}, | ||||
|       {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'}, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     id: 2, | ||||
|     name: 'Bombastic', | ||||
|     addresses: [ | ||||
|       {street: '789 Elm',  city: 'Smallville', state: 'OH',  zip: '04501'}, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     id: 3, | ||||
|     name: 'Magneta', | ||||
|     addresses: [ ] | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| export const states = ['CA', 'MD', 'OH', 'VA']; | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/demo.component.ts]" value="/* tslint:disable:member-ordering */ | ||||
| import { Component } from '@angular/core'; | ||||
| 
 | ||||
| import { Hero }        from './data-model'; | ||||
| import { HeroService } from './hero.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'my-app', | ||||
|   templateUrl: 'demo.component.html' | ||||
| }) | ||||
| export class DemoComponent { | ||||
| 
 | ||||
|   demos: string[] = [ | ||||
|     'Simple FormControl', | ||||
|     'Simple FormBuilder group', | ||||
|     'Group with multiple controls', | ||||
|     'Nested FormBuilder group', | ||||
|     'PatchValue', | ||||
|     'SetValue', | ||||
|     'FormArray', | ||||
|     'Final'].map(n => n + ' Demo'); | ||||
| 
 | ||||
|   final = this.demos.length; | ||||
|   demo = this.final; // current demo | ||||
| 
 | ||||
|   heroes: Hero[]; | ||||
|   selectedHero: Hero; | ||||
| 
 | ||||
|   constructor(private heroService: HeroService) { } | ||||
| 
 | ||||
|   getHeroes() { | ||||
|     return this.heroService.getHeroes().then(heroes => this.heroes = heroes); | ||||
|   } | ||||
| 
 | ||||
|   select(hero: Hero) { this.selectedHero = hero; } | ||||
| 
 | ||||
|   selectDemo(demo: number) { | ||||
|     this.demo = demo + 1; | ||||
|     this.heroes = undefined; | ||||
|     this.selectedHero = undefined; | ||||
|     this.getHeroes(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/demo.module.ts]" value="import { NgModule }             from '@angular/core'; | ||||
| import { BrowserModule }        from '@angular/platform-browser'; | ||||
| import { ReactiveFormsModule }  from '@angular/forms';  // <-- #1 import the module | ||||
| 
 | ||||
| import { DemoComponent }        from './demo.component'; | ||||
| import { components }           from './hero-detail-versions.component'; | ||||
| import { HeroDetailComponent }  from './hero-detail.component'; | ||||
| import { HeroListComponent }    from './hero-list.component'; | ||||
| 
 | ||||
| import { HeroService }          from './hero.service'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|     ReactiveFormsModule | ||||
|   ], | ||||
|   declarations: [ | ||||
|     DemoComponent, HeroDetailComponent, HeroListComponent, | ||||
|     ...components | ||||
|   ], | ||||
|   providers: [ HeroService ], | ||||
|   bootstrap: [ DemoComponent ] | ||||
| }) | ||||
| export class DemoModule { } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/hero-detail-versions.component.ts]" value="/* tslint:disable:component-class-suffix */ | ||||
| 
 | ||||
| import { Component, Input, OnChanges } from '@angular/core'; | ||||
| import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; | ||||
| import { Address, Hero, states } from './data-model'; | ||||
| 
 | ||||
| //////// 1 //////////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-1', | ||||
|   templateUrl: './hero-detail-1.component.html' | ||||
| }) | ||||
| 
 | ||||
| export class HeroDetailComponent1 { | ||||
|   heroForm = new FormGroup ({ | ||||
|     name: new FormControl() | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| //////// 2 //////////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-2', | ||||
|   templateUrl: './hero-detail-2.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent2 { | ||||
|   heroForm: FormGroup; // <--- heroForm is of type FormGroup | ||||
|   constructor(private fb: FormBuilder) { // <--- inject FormBuilder | ||||
|     this.heroForm = this.fb.group({ // <--- make this.form a FormBuilder group | ||||
|       name: '', // <--- the FormControl called "name" | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| //////// 3 //////////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-3', | ||||
|   templateUrl: './hero-detail-3.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent3 { | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       street: '', | ||||
|       city: '', | ||||
|       state: '', | ||||
|       zip: '' | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ////////// 4 ///////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-4', | ||||
|   templateUrl: './hero-detail-4.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent4 { | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ // <-- the parent FormGroup | ||||
|       name: '', | ||||
|       address: this.fb.group({ // <-- the child FormGroup | ||||
|         street: '', | ||||
|         city: '', | ||||
|         state: '', | ||||
|         zip: '' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ////////// 5 //////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-5', | ||||
|   templateUrl: './hero-detail-4.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent5 implements OnChanges { // <-- implements OnChanges | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       address: this.fb.group({ | ||||
|         street: '', | ||||
|         city: '', | ||||
|         state: '', | ||||
|         zip: '' | ||||
|       }) | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges() { // <-- wrap patchValue in ngOnChanges | ||||
|     this.heroForm.patchValue({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| ///////// 6 //////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-6', | ||||
|   templateUrl: './hero-detail-4.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent6 implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
| 
 | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       address: this.fb.group(new Address()) // <-- a FormGroup with a new address | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset(); | ||||
|     this.heroForm.setValue({ | ||||
|       name:    this.hero.name, | ||||
|       address: this.hero.addresses[0] || new Address() | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| ////////// 7 //////////////////// | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail-7', | ||||
|   templateUrl: './hero-detail-5.component.html' | ||||
| }) | ||||
| export class HeroDetailComponent7 implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       secretLairs: this.fb.array([]) // <-- secretLairs as an empty FormArray | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get secretLairs(): FormArray { | ||||
|     return this.heroForm.get('secretLairs') as FormArray; | ||||
|   }; | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset(); | ||||
|     this.heroForm.patchValue({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|     this.setAddresses(this.hero.addresses); | ||||
|   } | ||||
|   setAddresses(addresses: Address[]) { | ||||
|     const addressFGs = addresses.map(address => this.fb.group(address)); | ||||
|     const addressFormArray = this.fb.array(addressFGs); | ||||
|     this.heroForm.setControl('secretLairs', addressFormArray); | ||||
|   } | ||||
| 
 | ||||
|   addLair() { | ||||
|     this.secretLairs.push(this.fb.group(new Address())); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| //////////////////////////////// | ||||
| 
 | ||||
| export const components = [ | ||||
|   HeroDetailComponent1, | ||||
|   HeroDetailComponent2, | ||||
|   HeroDetailComponent3, | ||||
|   HeroDetailComponent4, | ||||
|   HeroDetailComponent5, | ||||
|   HeroDetailComponent6, | ||||
|   HeroDetailComponent7 | ||||
| ]; | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/hero-detail.component.ts]" value="// tslint:disable:no-unused-variable | ||||
| import { Component, EventEmitter, Input, Output, OnChanges } from '@angular/core'; | ||||
| import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; | ||||
| import { Address, Hero, states } from './data-model'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-detail', | ||||
|   templateUrl: './hero-detail.component.html' | ||||
| }) | ||||
| 
 | ||||
| export class HeroDetailComponent implements OnChanges { | ||||
|   @Input() hero: Hero; | ||||
|   @Output() save = new EventEmitter<Hero>(); | ||||
| 
 | ||||
|   states = states; | ||||
|   heroForm: FormGroup; | ||||
| 
 | ||||
|   constructor(private fb: FormBuilder) { | ||||
|     this.heroForm = this.fb.group({ | ||||
|       name: '', | ||||
|       secretLairs: this.fb.array([]) | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get secretLairs(): FormArray { | ||||
|     return this.heroForm.get('secretLairs') as FormArray; | ||||
|   }; | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.heroForm.reset(); | ||||
|     this.heroForm.patchValue({ | ||||
|       name: this.hero.name | ||||
|     }); | ||||
|     this.setAddresses(this.hero.addresses); | ||||
|   } | ||||
| 
 | ||||
|    setAddresses(addresses: Address[]) { | ||||
|     const addressFGs = addresses.map(address => this.fb.group(address)); | ||||
|     const addressFormArray = this.fb.array(addressFGs); | ||||
|     this.heroForm.setControl('secretLairs', addressFormArray); | ||||
|   } | ||||
| 
 | ||||
|   addLair() { | ||||
|     this.secretLairs.push(this.fb.group(new Address())); | ||||
|   } | ||||
| 
 | ||||
|   onSubmit() { | ||||
|     const formModel = this.heroForm.value; | ||||
| 
 | ||||
|     const newHero: Hero = { | ||||
|       id: this.hero.id, | ||||
|       name: formModel.name as string, | ||||
|       addresses: formModel.secretLairs | ||||
|     }; | ||||
| 
 | ||||
|     this.save.emit(newHero); // let parent decide what to do | ||||
| 
 | ||||
|     console.log(newHero); // diagnostic | ||||
|   } | ||||
| 
 | ||||
|   revert() { this.ngOnChanges(); } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/hero-list.component.ts]" value="import { Component, OnInit } from '@angular/core'; | ||||
| 
 | ||||
| import { Hero }        from './data-model'; | ||||
| import { HeroService } from './hero.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   moduleId: module.id, | ||||
|   selector: 'hero-list', | ||||
|   templateUrl: 'hero-list.component.html' | ||||
| }) | ||||
| export class HeroListComponent implements OnInit { | ||||
|   heroes: Hero[]; | ||||
|   selectedHero: Hero; | ||||
| 
 | ||||
|   constructor(private heroService: HeroService) { } | ||||
| 
 | ||||
|   ngOnInit() { this.getHeroes(); } | ||||
| 
 | ||||
|   getHeroes() { | ||||
|     this.heroService.getHeroes() | ||||
|         .then(heroes => this.heroes = heroes); | ||||
|   } | ||||
| 
 | ||||
|   select(hero: Hero) { this.selectedHero = hero; } | ||||
| 
 | ||||
|   onSave(hero: Hero) { | ||||
|     this.heroService.updateHero(hero) | ||||
|         .then(() => { | ||||
|           this.getHeroes(); | ||||
|           const newHero = this.heroes.find(h => h === hero); | ||||
|           this.select(newHero); | ||||
|         }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/hero.service.ts]" value="import { Injectable } from '@angular/core'; | ||||
| 
 | ||||
| import { Hero, heroes } from './data-model'; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class HeroService { | ||||
| 
 | ||||
|   getHeroes(): Promise<Hero[]> { | ||||
|     return new Promise<Hero[]>(resolve => { | ||||
|       // simulate server latency with delay | ||||
|       setTimeout(() => resolve(heroes), 500); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   updateHero(hero: Hero): Promise<Hero>  { | ||||
|     // Demo: faking server update; nothing can go wrong ;-) | ||||
|     return new Promise<Hero>(resolve => { | ||||
|       // simulate server latency with delay | ||||
|       const ix = heroes.findIndex(h => h.id === hero.id); | ||||
|       setTimeout(() => { | ||||
|         heroes[ix] = hero; | ||||
|         resolve(hero); | ||||
|       }, 500); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/main-final.ts]" value="// tslint:disable:no-unused-variable | ||||
| import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||||
| import { AppModule } from './app.module'; | ||||
| 
 | ||||
| platformBrowserDynamic().bootstrapModule(AppModule); | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/main.ts]" value="// tslint:disable:no-unused-variable | ||||
| import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | ||||
| import { AppModule }  from './app.module';  // just the final version | ||||
| import { DemoModule } from './demo.module'; // demo picker | ||||
| 
 | ||||
| platformBrowserDynamic().bootstrapModule(DemoModule); // (AppModule); | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[styles.css]" value="/* Master Styles */ | ||||
| h1 { | ||||
|   color: #369; | ||||
|   font-family: Arial, Helvetica, sans-serif; | ||||
|   font-size: 250%; | ||||
| } | ||||
| h2, h3 { | ||||
|   color: #444; | ||||
|   font-family: Arial, Helvetica, sans-serif; | ||||
|   font-weight: lighter; | ||||
| } | ||||
| body { | ||||
|   margin: 2em; | ||||
| } | ||||
| body, input[text], button { | ||||
|   color: #888; | ||||
|   font-family: Cambria, Georgia; | ||||
| } | ||||
| a { | ||||
|   cursor: pointer; | ||||
|   cursor: hand; | ||||
| } | ||||
| button { | ||||
|   font-family: Arial; | ||||
|   background-color: #eee; | ||||
|   border: none; | ||||
|   padding: 5px 10px; | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
|   cursor: hand; | ||||
| } | ||||
| button:hover { | ||||
|   background-color: #cfd8dc; | ||||
| } | ||||
| button:disabled { | ||||
|   background-color: #eee; | ||||
|   color: #aaa; | ||||
|   cursor: auto; | ||||
| } | ||||
| 
 | ||||
| /* Navigation link styles */ | ||||
| nav a { | ||||
|   padding: 5px 10px; | ||||
|   text-decoration: none; | ||||
|   margin-right: 10px; | ||||
|   margin-top: 10px; | ||||
|   display: inline-block; | ||||
|   background-color: #eee; | ||||
|   border-radius: 4px; | ||||
| } | ||||
| nav a:visited, a:link { | ||||
|   color: #607D8B; | ||||
| } | ||||
| nav a:hover { | ||||
|   color: #039be5; | ||||
|   background-color: #CFD8DC; | ||||
| } | ||||
| nav a.active { | ||||
|   color: #039be5; | ||||
| } | ||||
| 
 | ||||
| /* items class */ | ||||
| .items { | ||||
|   margin: 0 0 2em 0; | ||||
|   list-style-type: none; | ||||
|   padding: 0; | ||||
|   width: 24em; | ||||
| } | ||||
| .items li { | ||||
|   cursor: pointer; | ||||
|   position: relative; | ||||
|   left: 0; | ||||
|   background-color: #EEE; | ||||
|   margin: .5em; | ||||
|   padding: .3em 0; | ||||
|   height: 1.6em; | ||||
|   border-radius: 4px; | ||||
| } | ||||
| .items li:hover { | ||||
|   color: #607D8B; | ||||
|   background-color: #DDD; | ||||
|   left: .1em; | ||||
| } | ||||
| .items li.selected { | ||||
|   background-color: #CFD8DC; | ||||
|   color: white; | ||||
| } | ||||
| .items li.selected:hover { | ||||
|   background-color: #BBD8DC; | ||||
| } | ||||
| .items .text { | ||||
|   position: relative; | ||||
|   top: -3px; | ||||
| } | ||||
| .items .badge { | ||||
|   display: inline-block; | ||||
|   font-size: small; | ||||
|   color: white; | ||||
|   padding: 0.8em 0.7em 0 0.7em; | ||||
|   background-color: #607D8B; | ||||
|   line-height: 1em; | ||||
|   position: relative; | ||||
|   left: -1px; | ||||
|   top: -4px; | ||||
|   height: 1.8em; | ||||
|   margin-right: .8em; | ||||
|   border-radius: 4px 0 0 4px; | ||||
| } | ||||
| /* everywhere else */ | ||||
| * { | ||||
|   font-family: Arial, Helvetica, sans-serif; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| */"><input type="hidden" name="files[app/demo.component.html]" value="<div class="container"> | ||||
| <h1>Reactive Forms</h1> | ||||
| <h4><i>Pick a demo:</i> | ||||
|   <select [selectedIndex]="demo - 1" (change)="selectDemo($event.target.selectedIndex)"> | ||||
|     <option *ngFor="let demo of demos">{{demo}}</option> | ||||
|   </select> | ||||
| </h4> | ||||
| 
 | ||||
| <hr> | ||||
| 
 | ||||
| <div class="demo"> | ||||
|   <hero-list *ngIf="demo===final"></hero-list> | ||||
| 
 | ||||
|   <hero-detail-1 *ngIf="demo===1"></hero-detail-1> | ||||
|   <hero-detail-2 *ngIf="demo===2"></hero-detail-2> | ||||
|   <hero-detail-3 *ngIf="demo===3"></hero-detail-3> | ||||
|   <hero-detail-4 *ngIf="demo===4"></hero-detail-4> | ||||
| 
 | ||||
|   <div *ngIf="demo >= 5 && demo !== final" > | ||||
| 
 | ||||
|     <p *ngIf="!heroes"><b><i>Loading heroes ... </i></b></p> | ||||
|     <h3 *ngIf="heroes">Select a hero:</h3> | ||||
| 
 | ||||
|     <nav> | ||||
|       <a *ngFor="let hero of heroes" (click)="select(hero)">{{hero.name}}</a> | ||||
|     </nav> | ||||
| 
 | ||||
|     <div *ngIf="selectedHero"> | ||||
|       <hr> | ||||
|       <h2>Hero Detail</h2> | ||||
|       <h3>Editing: {{selectedHero.name}}</h3> | ||||
|       <hero-detail-5 [hero]=selectedHero *ngIf="demo===5"></hero-detail-5> | ||||
|       <hero-detail-6 [hero]=selectedHero *ngIf="demo===6"></hero-detail-6> | ||||
|       <hero-detail-7 [hero]=selectedHero *ngIf="demo===7"></hero-detail-7> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[app/hero-detail-1.component.html]" value="<h2>Hero Detail</h2> | ||||
| <h3><i>A simple form with a single FormControl</i></h3> | ||||
| <form novalidate [formGroup]="heroForm"> | ||||
|   <div class="form-group"> | ||||
|     <label>Name:</label> | ||||
|     <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[app/hero-detail-2.component.html]" value="<h2>Hero Detail</h2> | ||||
| <h3><i>A simple form with a single FormControl using FormBuilder</i></h3> | ||||
| <form novalidate [formGroup]="heroForm"> | ||||
|   <div class="form-group"> | ||||
|     <label>Name:</label> | ||||
|     <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>Form: {{ heroForm.value | json }}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[app/hero-detail-3.component.html]" value="<h2>Hero Detail</h2> | ||||
| <h3><i>A simple form with multiple FormControls in a single FormBuilder group</i></h3> | ||||
| <form novalidate [formGroup]="heroForm"> | ||||
|   <div class="form-group"> | ||||
|     <label>Name:</label> | ||||
|     <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Street:</label> | ||||
|     <input class="form-control" formControlName="street"> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>City:</label> | ||||
|     <input class="form-control" formControlName="city"> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>State:</label> | ||||
|     <select class="form-control" formControlName="state"> | ||||
|         <option *ngFor="let state of states">{{state}}</option> | ||||
|     </select> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Zip Code:</label> | ||||
|     <input class="form-control" formControlName="zip"> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[app/hero-detail-4.component.html]" value="<form novalidate [formGroup]="heroForm"> | ||||
|     <div class="form-group"> | ||||
|       <label>Name:</label> | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </div> | ||||
|     <div formGroupName="address" class="well well-lg"> | ||||
|       <h4>Secret Lair</h4> | ||||
|       <div class="form-group"> | ||||
|         <label>Street:</label> | ||||
|         <input class="form-control" formControlName="street"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label>City:</label> | ||||
|         <input class="form-control" formControlName="city"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label>State:</label> | ||||
|         <select class="form-control" formControlName="state"> | ||||
|           <option *ngFor="let state of states">{{state}}</option> | ||||
|         </select> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label>Zip Code:</label> | ||||
|         <input class="form-control" formControlName="zip"> | ||||
|       </div> | ||||
|     </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| <h4>Extra info for the curious:</h4> | ||||
| <p>Name value: {{ heroForm.get('name').value }}</p> | ||||
| 
 | ||||
| <p>Street value: {{ heroForm.get('address.street').value}}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[app/hero-detail-5.component.html]" value="<h3><i>Using FormArray to add groups</i></h3> | ||||
| 
 | ||||
| <form novalidate [formGroup]="heroForm" (ngSubmit)="onSubmit(heroForm)"> | ||||
|     <p>Form Changed: {{ heroForm.dirty }}</p> | ||||
| 
 | ||||
|     <div class="form-group"> | ||||
|       <label>Name:</label> | ||||
|       <input class="form-control" formControlName="name"> | ||||
|     </div> | ||||
|     <div formArrayName="secretLairs" class="well well-lg"> | ||||
|       <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > | ||||
|         <!-- The repeated address template --> | ||||
|         <h4>Address #{{i + 1}}</h4> | ||||
|         <div style="margin-left: 1em;"> | ||||
|           <div class="form-group"> | ||||
|             <label>Street:</label> | ||||
|             <input class="form-control" formControlName="street"> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label>City:</label> | ||||
|             <input class="form-control" formControlName="city"> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label>State:</label> | ||||
|             <select class="form-control" formControlName="state"> | ||||
|               <option *ngFor="let state of states">{{state}}</option> | ||||
|             </select> | ||||
|           </div> | ||||
|           <div class="form-group"> | ||||
|             <label>Zip Code:</label> | ||||
|             <input class="form-control" formControlName="zip"> | ||||
|           </div> | ||||
|         </div> | ||||
|         <br> | ||||
|         <!-- End of the repeated address template --> | ||||
|       </div> | ||||
|       <button (click)="addLair()">Add a Secret Lair</button> <!-- add button --> | ||||
|     </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[app/hero-detail.component.html]" value="<form novalidate [formGroup]="heroForm" (ngSubmit)="onSubmit(heroForm)"> | ||||
|   <div style="margin-bottom: 1em"> | ||||
|     <button type="submit" | ||||
|             [disabled]="!heroForm.dirty" class="btn btn-success">Save</button> &nbsp; | ||||
|     <button type="reset" (click)="revert()" | ||||
|             [disabled]="!heroForm.dirty" class="btn btn-danger">Revert</button> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="form-group"> | ||||
|       <label>Name:</label> | ||||
|       <input class="form-control" formControlName="name"> | ||||
|   </div> | ||||
| 
 | ||||
|   <div formArrayName="secretLairs" class="well well-lg"> | ||||
|     <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" > | ||||
|       <!-- The repeated address template --> | ||||
|       <h4>Address #{{i + 1}}</h4> | ||||
|       <div style="margin-left: 1em;"> | ||||
|         <div class="form-group"> | ||||
|           <label>Street:</label> | ||||
|           <input class="form-control" formControlName="street"> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>City:</label> | ||||
|           <input class="form-control" formControlName="city"> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>State:</label> | ||||
|           <select class="form-control" formControlName="state"> | ||||
|             <option *ngFor="let state of states">{{state}}</option> | ||||
|           </select> | ||||
|         </div> | ||||
|         <div class="form-group"> | ||||
|           <label>Zip Code:</label> | ||||
|           <input class="form-control" formControlName="zip"> | ||||
|         </div> | ||||
|       </div> | ||||
|       <br> | ||||
|       <!-- End of the repeated address template --> | ||||
|     </div> | ||||
|     <button (click)="addLair()">Add a Secret Lair</button> <!-- add button --> | ||||
|   </div> | ||||
| </form> | ||||
| 
 | ||||
| <p>heroForm value: {{ heroForm.value | json}}</p> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[app/hero-list.component.html]" value="<p *ngIf="!heroes"><b><i>Loading heroes ... </i></b></p> | ||||
| <h3 *ngIf="heroes">Select a hero:</h3> | ||||
| 
 | ||||
| <nav> | ||||
|   <a *ngFor="let hero of heroes" (click)="select(hero)">{{hero.name}}</a> | ||||
| </nav> | ||||
| 
 | ||||
| <div *ngIf="selectedHero"> | ||||
|   <hr> | ||||
|   <h2>Hero Detail</h2> | ||||
|   <h3>Editing: {{selectedHero.name}}</h3> | ||||
|   <hero-detail [hero]="selectedHero" (save)="onSave($event)"></hero-detail> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="files[index.html]" value="<!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Hero Form</title> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| 
 | ||||
|     <link rel="stylesheet" href="styles.css"> | ||||
|     <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css"> | ||||
| 
 | ||||
|     <!-- Polyfills for older browsers --> | ||||
|     <script src="https://unpkg.com/core-js/client/shim.min.js"></script> | ||||
| 
 | ||||
|     <script src="https://unpkg.com/zone.js@0.7.2?main=browser"></script> | ||||
|     <script src="https://unpkg.com/reflect-metadata@0.1.8"></script> | ||||
|     <script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script> | ||||
| 
 | ||||
|     <script src="https://cdn.rawgit.com/angular/angular.io/b3c65a9/public/docs/_examples/_boilerplate/systemjs.config.web.js"></script> | ||||
|     <script> | ||||
|       System.import('app').catch(function(err){ console.error(err); }); | ||||
|     </script> | ||||
|   </head> | ||||
| 
 | ||||
|   <body> | ||||
|     <my-app>Loading...</my-app> | ||||
|   </body> | ||||
| 
 | ||||
| </html> | ||||
| 
 | ||||
| 
 | ||||
| <!--  | ||||
| Copyright 2016 Google Inc. All Rights Reserved. | ||||
| Use of this source code is governed by an MIT-style license that | ||||
| can be found in the LICENSE file at http://angular.io/license | ||||
| -->"><input type="hidden" name="tags[0]" value="angular"><input type="hidden" name="tags[1]" value="example"><input type="hidden" name="tags[2]" value="reactive"><input type="hidden" name="tags[3]" value="forms"><input type="hidden" name="private" value="true"><input type="hidden" name="description" value="Angular Example - Reactive Forms (final)"></form><script>document.getElementById("mainForm").submit();</script></body></html> | ||||