Add model driven form form 'forms' app
							parent
							
								
									17c89875dc
								
							
						
					
					
						commit
						1d9c108bfb
					
				|  | @ -5,9 +5,9 @@ | |||
|       "title": "Ersten Prototyp mit Angular 2.0 entwickeln", | ||||
|       "description": "Der Prototyp soll zeigen, wie Routing und HTTP-Anbindung umgesetzt werden können.", | ||||
|       "tags": [], | ||||
|       "state": "IN_PROGRESS", | ||||
|       "state": "BACKLOG", | ||||
|       "assignee": { | ||||
|         "name": "Christoph Höller", | ||||
|         "name": "Christoph Höllers", | ||||
|         "email": "" | ||||
|       } | ||||
|     }, | ||||
|  |  | |||
|  | @ -31,6 +31,9 @@ | |||
|           <li routerLinkActive="active"> | ||||
|             <a routerLink='/blog' class="link">Blog</a> | ||||
|           </li> | ||||
|           <li routerLinkActive="active"> | ||||
|             <a routerLink='/model-form' class="link">Model Form</a> | ||||
|           </li> | ||||
|           <!-- | ||||
|           <li routerLinkActive="active"> | ||||
|             <a routerLink='/tasks/new' class="link">Neue Aufgabe anlegen</a> | ||||
|  |  | |||
|  | @ -16,6 +16,8 @@ import {SOCKET_IO, AUTH_ENABLED} from './app.tokens'; | |||
| import {environment} from '../environments/environment'; | ||||
| import {mockIO} from './mocks/mock-socket'; | ||||
| import {TaskItemComponent} from './tasks/task-list/task-item.component'; | ||||
| import {UserService} from "./services/user-service/user.service"; | ||||
| import {ShowErrorComponentModelDriven} from "./show-error/show-error-model-driven.component"; | ||||
| 
 | ||||
| export function socketIoFactory() { | ||||
|   if (environment.e2eMode) { | ||||
|  | @ -29,6 +31,7 @@ const enableAuthentication = !environment.e2eMode; | |||
| @NgModule({ | ||||
|   imports: [BrowserModule, FormsModule, ReactiveFormsModule, appRouting, HttpModule], | ||||
|   providers: [LoginService, | ||||
|     UserService, | ||||
|     UserStore, | ||||
|     TaskService, | ||||
|     TaskStore, | ||||
|  | @ -41,6 +44,7 @@ const enableAuthentication = !environment.e2eMode; | |||
|     routingComponents, | ||||
|     TaskItemComponent, | ||||
|     ShowErrorComponent, | ||||
|     ShowErrorComponentModelDriven, | ||||
|     APPLICATION_VALIDATORS], | ||||
|   bootstrap: [AppComponent] | ||||
| }) | ||||
|  |  | |||
|  | @ -10,15 +10,16 @@ import {tasksRoutes, tasksRoutingComponents, tasksRoutingProviders} from './task | |||
| import {RxDemoComponent} from './rxdemo/rxdemo.component'; | ||||
| import {LoginGuard} from './login/login.guard'; | ||||
| import {blogRoutingComponents} from "./blog/blog.routing"; | ||||
| import {ModelDrivenFormComponent} from "./model-driven-form/model-driven-form.component"; | ||||
| 
 | ||||
| export const appRoutes: Routes = [ | ||||
|   {path: 'dashboard', component: DashboardComponent, data: {title: 'Startseite'}}, | ||||
|   {path: '', redirectTo: '/dashboard', pathMatch: 'full'}, | ||||
|   {path: 'settings', component: SettingsComponent, data: { title: 'Einstellungen' }, | ||||
|   }, | ||||
|   {path: 'settings', component: SettingsComponent, data: { title: 'Einstellungen' }}, | ||||
|   {path: 'about', component: AboutComponent, data: {title: 'Über uns'}}, | ||||
|   {path: 'rxdemo', component: RxDemoComponent, data: {title: 'RxJS Demo'}}, | ||||
|   {path: 'blog', component: BlogComponent, data: {title: 'Blog'}}, | ||||
|   {path: 'model-form', component: ModelDrivenFormComponent, data: { title: 'Model Driven Task Form' }}, | ||||
| 
 | ||||
|   {path: 'login', component: LoginComponent}, | ||||
| 
 | ||||
|  | @ -34,7 +35,7 @@ export const appRoutes: Routes = [ | |||
| export const appRouting = RouterModule.forRoot(appRoutes); | ||||
| 
 | ||||
| export const routingComponents = [DashboardComponent, SettingsComponent, AboutComponent, LoginComponent, NotFoundComponent, | ||||
|   RxDemoComponent, ...blogRoutingComponents, ...tasksRoutingComponents]; | ||||
|   RxDemoComponent, ModelDrivenFormComponent, ...blogRoutingComponents, ...tasksRoutingComponents]; | ||||
| 
 | ||||
| export const routingProviders = [LoginGuard, | ||||
|   ...tasksRoutingProviders]; | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ import {TestBed} from "@angular/core/testing"; | |||
| import {LoginComponent} from "./login.component"; | ||||
| import {RouterTestingModule} from "@angular/router/testing"; | ||||
| import {LoginService} from "../services/login-service/login-service"; | ||||
| import {MockLoginService} from "../mocks/mock-login-service.spec"; | ||||
| import {FormsModule} from "@angular/forms"; | ||||
| import {MockLoginService} from "../mocks/mock-login-service.spec"; | ||||
| import Spy = jasmine.Spy; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -32,8 +32,7 @@ describe('Login Component Template Driven Form Test with Spy', () => { | |||
| 
 | ||||
|     // Login ausfuellen und absenden
 | ||||
|     loginFixture.whenStable().then(() => { | ||||
|       // Spy anmelden
 | ||||
|       // sobald Spy angemeldet ist, wird der eigentliche Methodenrumpf nicht mehr aufgerufen
 | ||||
|       // Spy anmelden, die eigentliche Methode wird nicht mehr aufgerufen
 | ||||
|       const spy = spyOn(loginInstance, 'login'); | ||||
| 
 | ||||
|       // Formular Eingabe
 | ||||
|  |  | |||
|  | @ -0,0 +1,87 @@ | |||
| <h2 *ngIf="!task.id">Neue Aufgabe anlegen</h2> | ||||
| <h2 *ngIf="task.id">Aufgabe bearbeiten</h2> | ||||
| 
 | ||||
| <form novalidate [formGroup]="taskForm" | ||||
|       (ngSubmit)="saveTask(taskForm.value)"> | ||||
|   <div class="form-group"> | ||||
|     <label>Titel</label> | ||||
|     <input class="form-control" formControlName="title"/> | ||||
|   </div> | ||||
|   <pjm-show-error text="Titel" path="title"></pjm-show-error> | ||||
|   <div class="form-group"> | ||||
|     <label>Beschreibung</label> | ||||
|     <textarea class="form-control" formControlName="description"> | ||||
|     </textarea> | ||||
|     <pjm-show-error text="Beschreibung" path="description"></pjm-show-error> | ||||
|   </div> | ||||
| 
 | ||||
|   <label>Tags</label> | ||||
|   <div formArrayName="tags"> | ||||
|     <div *ngFor="let tag of tagsArray.controls; let i = index"> | ||||
|       <div class="tag-controls" [formGroupName]="i"> | ||||
|         <input class="form-control" formControlName="label"> | ||||
|         <button class="btn btn-danger" (click)="removeTag(i)"> | ||||
|           Tag entfernen | ||||
|         </button> | ||||
|         <pjm-show-error text="Ein Tag" path="tags.{{i}}.label"></pjm-show-error> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <button class="btn btn-success" (click)="addTag()"> + </button> | ||||
|   </div> | ||||
|   <div class="form-group"> | ||||
|     <label>Status</label> | ||||
|     <select formControlName="state" class="form-control"> | ||||
|         <option *ngFor="let state of model.states" [value]="state"> | ||||
|           {{model.stateTexts[state]}} | ||||
|         </option> | ||||
|     </select> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="checkbox"> | ||||
|     <label> | ||||
|       <input type="checkbox" formControlName="favorite"> | ||||
|       Zu Favoriten hinzufügen | ||||
|     </label> | ||||
|   </div> | ||||
|   <br> | ||||
|   <h4>Zuständiger</h4> | ||||
|   <div formGroupName="assignee"> | ||||
|     <div class="form-group"> | ||||
|       <label>Name</label> | ||||
|       <input type="text" class="form-control" | ||||
|              formControlName="name"/> | ||||
|       <pjm-show-error path="assignee/name" text="Name"></pjm-show-error> | ||||
|     </div> | ||||
|     <div class="form-group"> | ||||
|       <label>E-Mail</label> | ||||
|       <input type="text" class="form-control" | ||||
|              formControlName="email"/> | ||||
|       <pjm-show-error path="assignee.email"></pjm-show-error> | ||||
|     </div> | ||||
| 
 | ||||
|     <div *ngIf="taskForm.hasError('assigneeRequired')" | ||||
|          class="alert alert-danger"> | ||||
|       Der Task befindet sich nicht mehr im Backlog <br> | ||||
|       Bitte geben Sie einen Zuständigen an. | ||||
|     </div> | ||||
|   </div> | ||||
| 
 | ||||
|   <button type="submit" | ||||
|           class="btn btn-default" | ||||
|           [disabled]="!taskForm.valid"> | ||||
|     Aufgabe speichern | ||||
|   </button> | ||||
| </form> | ||||
| 
 | ||||
| <hr> | ||||
| 
 | ||||
| <button (click)="loadTask(1)" | ||||
|         class="btn btn-default"> | ||||
|   Task 1 laden | ||||
| </button> | ||||
| <button (click)="loadTask(2)" | ||||
|         class="btn btn-default"> | ||||
|   Task 2 laden | ||||
| </button> | ||||
|  | @ -0,0 +1,66 @@ | |||
| import {ReactiveFormsModule} from '@angular/forms'; | ||||
| import {TestBed, ComponentFixture} from '@angular/core/testing'; | ||||
| import {ModelDrivenFormComponent} from './model-driven-form.component'; | ||||
| import {TaskService} from '../services/task-service/task.service'; | ||||
| import {UserService} from '../services/user-service/user.service'; | ||||
| 
 | ||||
| beforeEach(() => { | ||||
|   TestBed.configureTestingModule({ | ||||
|     imports: [ReactiveFormsModule], | ||||
|     providers: [TaskService, UserService], | ||||
|     declarations: [ModelDrivenFormComponent] | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('Model Driven Form', () => { | ||||
|   it('should validate the title directly', () => { | ||||
| 
 | ||||
|     const fixture = TestBed.createComponent(ModelDrivenFormComponent); | ||||
|     const form = fixture.componentInstance.taskForm; | ||||
| 
 | ||||
|     const titleControl = form.get('title'); | ||||
|     expect(titleControl.errors['required']).toBeTruthy(); // Cannot read property 'errors' of undefined
 | ||||
| 
 | ||||
|     titleControl.setValue('Task'); | ||||
|     expect(titleControl.errors['required']).toBeUndefined(); | ||||
|     const minError = {requiredLength: 5, actualLength: 4}; | ||||
| 
 | ||||
|     expect(titleControl.errors['minlength']).toEqual(minError); | ||||
|     titleControl.setValue('Task 1'); | ||||
|     expect(titleControl.errors).toBeNull(); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|   it('should validate the whole form ', fakeAsync(() => { | ||||
| 
 | ||||
|     const fixture = TestBed.createComponent(ModelDrivenFormComponent); | ||||
| 
 | ||||
|     const form = fixture.componentInstance.taskForm; | ||||
| 
 | ||||
|     console.log(form.patchValue({title: 'Task123'})); | ||||
|     fixture.detectChanges(); | ||||
|     tick(5000); | ||||
|     fixture.detectChanges(); | ||||
|     tick(50000); | ||||
|     console.log(form.valid) | ||||
| 
 | ||||
| 
 | ||||
|   })); | ||||
| 
 | ||||
|   it('should be able to work with Observable.delay', fakeAsync(() => { | ||||
|     const actuallyDone=false; | ||||
|     const source = Observable.of(true).delay(10); | ||||
|     source.subscribe( | ||||
|       val => { | ||||
|         actuallyDone = true; | ||||
|       }, | ||||
|       err => fail(err) | ||||
|     ); | ||||
|     tick(100); | ||||
|     expect(actuallyDone).toBeTruthy(); // Expected false to be truthy.
 | ||||
| 
 | ||||
|     discardPeriodicTasks(); | ||||
|   })); | ||||
| */ | ||||
|   ); | ||||
|  | @ -0,0 +1,117 @@ | |||
| import {Component} from '@angular/core'; | ||||
| import {FormGroup, FormArray, FormControl, FormBuilder, Validators} from '@angular/forms'; | ||||
| import {Task, createInitialTask} from '../models/model-interfaces'; | ||||
| import * as model from '../models/model-interfaces'; | ||||
| import {ifNotBacklogThanAssignee,  emailValidator, UserExistsValidatorDirective} from '../models/app-validators'; | ||||
| import {TaskService} from '../services/task-service/task.service'; | ||||
| import {UserService} from '../services/user-service/user.service'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'pjm-model-driven-form', | ||||
|   templateUrl: './model-driven-form.component.html' | ||||
| }) | ||||
| export class ModelDrivenFormComponent { | ||||
| 
 | ||||
|   model = model; | ||||
|   task: Task = createInitialTask(); | ||||
| 
 | ||||
|   taskForm: FormGroup; | ||||
|   tagsArray: FormArray; | ||||
| 
 | ||||
|   constructor(private taskService: TaskService, | ||||
|               private userService: UserService, | ||||
|               fb: FormBuilder) { | ||||
|     this.taskForm = fb.group({ | ||||
|       title: ['', [Validators.required, Validators.minLength(5)]], | ||||
|       description: ['', Validators.maxLength(2000)], | ||||
|       favorite: [false], | ||||
|       state: ['BACKLOG'], | ||||
|       tags: fb.array([ | ||||
|         this.createTagControl() | ||||
|       ]), | ||||
|       assignee: fb.group({ | ||||
|         name: ['', null, this.userExistsValidatorReused], | ||||
|         email: ['', emailValidator], | ||||
|       }) | ||||
|     }, {validator: ifNotBacklogThanAssignee}); | ||||
| 
 | ||||
|     this.taskForm.valueChanges.subscribe((value) => { | ||||
|       Object.assign(this.task, value); | ||||
|     }); | ||||
| 
 | ||||
|     this.tagsArray = <FormArray>this.taskForm.controls['tags']; | ||||
| 
 | ||||
|     /* | ||||
|     this.taskForm = new FormGroup({ | ||||
|       title: new FormControl(''), | ||||
|       description: new FormControl(''), | ||||
|       favorite: new FormControl(false), | ||||
|       state: new FormControl('BACKLOG'), | ||||
|       tags: new FormArray([ | ||||
|         new FormGroup({ | ||||
|           label: new FormControl('') | ||||
|         }) | ||||
|       ]), | ||||
|       assignee: new FormGroup({ | ||||
|         name: new FormControl(''), | ||||
|         email: new FormControl('') | ||||
|       }), | ||||
|     });*/ | ||||
|   } | ||||
| 
 | ||||
|   private createTagControl(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       label: new FormControl('', Validators.minLength(3)) | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   addTag() { | ||||
|     this.tagsArray.push(this.createTagControl()); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   removeTag(i: number) { | ||||
|     this.tagsArray.removeAt(i); | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   saveTask(value: any) { | ||||
|     console.log(value); | ||||
|     Object.assign(this.task, value); | ||||
|     this.taskService.saveTask(this.task); | ||||
|   } | ||||
| 
 | ||||
|   loadTask(id: number) { | ||||
|     const task : Task = this.taskService.getTask(id); | ||||
|     this.adjustTagsArray(task.tags); | ||||
|     this.taskForm.patchValue(task); | ||||
|     this.task = task; | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   private adjustTagsArray(tags: any[]) { | ||||
|     const tagCount = tags ? tags.length : 0; | ||||
|     while (tagCount > this.tagsArray.controls.length) { | ||||
|       this.addTag(); | ||||
|     } | ||||
|     while (tagCount <  this.tagsArray.controls.length) { | ||||
|       this.removeTag(0); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   userExistsValidator = (control) => { | ||||
|     return this.userService.checkUserExists(control.value) | ||||
|       .map(checkResult => { | ||||
|         return (checkResult === false) ? {userNotFound: true} : null; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   userExistsValidatorReused = (control) => { | ||||
|     const validator = new UserExistsValidatorDirective(this.userService); | ||||
|     return validator.validate(control); | ||||
|   }; | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -0,0 +1,3 @@ | |||
| import {ModelDrivenFormComponent} from "./model-driven-form.component"; | ||||
| 
 | ||||
| export const modelDrivenFormRoutingComponent = [ModelDrivenFormComponent]; | ||||
|  | @ -2,8 +2,10 @@ import {Directive, forwardRef} from '@angular/core'; | |||
| import { | ||||
|   FormControl, | ||||
|   AbstractControl, | ||||
|   NG_VALIDATORS | ||||
|   NG_VALIDATORS, NG_ASYNC_VALIDATORS | ||||
| } from '@angular/forms'; | ||||
| import {UserService} from "../services/user-service/user.service"; | ||||
| import {Observable} from "rxjs"; | ||||
| 
 | ||||
| export function asyncIfNotBacklogThenAssignee(control): Promise<any> { | ||||
|   const promise = new Promise((resolve, reject) => { | ||||
|  | @ -82,6 +84,27 @@ export function emailValidator2(control): {[key: string]: any} { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| @Directive({ | ||||
|   selector: '[pjmUserExistsValidator]', | ||||
|   providers: [ | ||||
|     { | ||||
|       provide: NG_ASYNC_VALIDATORS, | ||||
|       useExisting: forwardRef(() => UserExistsValidatorDirective), multi: true | ||||
|     } | ||||
|   ] | ||||
| }) | ||||
| export class UserExistsValidatorDirective { | ||||
|   constructor(private userService: UserService) { | ||||
|   } | ||||
| 
 | ||||
|   validate(control: AbstractControl): Observable<any> { | ||||
|     return this.userService.checkUserExists(control.value) | ||||
|       .map(userExists => { | ||||
|         return (userExists === false) ? {userNotFound: true} :null; | ||||
|       }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Directive({ | ||||
|   selector: '[emailValidator]', | ||||
|   providers: [ | ||||
|  |  | |||
|  | @ -0,0 +1,8 @@ | |||
| 
 | ||||
| import {Observable} from 'rxjs/Observable'; | ||||
| export class UserService { | ||||
|   checkUserExists(name: string): Observable<boolean> { | ||||
|     const result = name == null || name.toLowerCase() !== 'johnny incognito'; | ||||
|     return Observable.of(result).delay(250); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,61 @@ | |||
| import {Component, Input, Optional} from '@angular/core'; | ||||
| import {NgForm, FormGroup, FormGroupDirective} from '@angular/forms'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'pjm-show-error', | ||||
|   template: ` | ||||
|     <div *ngIf="errorMessages" class="alert alert-danger"> | ||||
|         <div *ngFor="let errorMessage of errorMessages"> | ||||
|             {{errorMessage}} | ||||
|         </div> | ||||
|     </div>` })
 | ||||
| export class ShowErrorComponentModelDriven { | ||||
| 
 | ||||
|   @Input('path') path; | ||||
|   @Input('text') displayName = ''; | ||||
| 
 | ||||
|   constructor(@Optional() private ngForm: NgForm, | ||||
|               @Optional() private formGroup: FormGroupDirective) { | ||||
|   } | ||||
| 
 | ||||
|   get errorMessages(): string[] { | ||||
|     let form: FormGroup; | ||||
|     if (this.ngForm) { | ||||
|       form = this.ngForm.form; | ||||
|     } else  { | ||||
|       form = this.formGroup.form; | ||||
|     } | ||||
|     const control = form.get(this.path); | ||||
|     const messages = []; | ||||
|     if (!control || !(control.touched) || !control.errors) { | ||||
|       return null; | ||||
|     } | ||||
|     for (const code in control.errors) { | ||||
|       if (control.errors.hasOwnProperty(code)) { | ||||
|         const error = control.errors[code]; | ||||
|         let message = ''; | ||||
|         switch (code) { | ||||
|           case 'required': | ||||
|             message = `${this.displayName} ist ein Pflichtfeld`; | ||||
|             break; | ||||
|           case 'minlength': | ||||
|             message = `${this.displayName} muss mindestens ${error.requiredLength} Zeichen enthalten`; | ||||
|             break; | ||||
|           case 'maxlength': | ||||
|             message = `${this.displayName} darf maximal ${error.requiredLength} Zeichen enthalten`; | ||||
|             break; | ||||
|           case 'invalidEMail': | ||||
|             message = `Bitte geben Sie eine gültige E-Mail-Adresse an`; | ||||
|             break; | ||||
|           case 'userNotFound': | ||||
|             message = `Der eingetragene Benutzer existiert nicht.`; | ||||
|             break; | ||||
|           default: | ||||
|             message = `${name} ist nicht valide`; | ||||
|         } | ||||
|         messages.push(message); | ||||
|       } | ||||
|     } | ||||
|     return messages; | ||||
|   } | ||||
| } | ||||
|  | @ -4,7 +4,6 @@ import {EditTaskComponent} from './edit-task/edit-task.component'; | |||
| import {EditTaskGuard} from './edit-task/edit-task.guard'; | ||||
| import {TaskOverviewComponent} from './task-overview/task-overview.component'; | ||||
| import {TasksComponent} from './tasks.component'; | ||||
| import {LoginGuard} from '../login/login.guard'; | ||||
| 
 | ||||
| export const tasksRoutes: Routes = [{ | ||||
|   path: '', component: TasksComponent, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue