Add model driven form form 'forms' app

merge-requests/1/head
Florian Hartwich 2017-04-05 02:43:17 +02:00
parent 17c89875dc
commit 1d9c108bfb
13 changed files with 384 additions and 13 deletions

View File

@ -5,9 +5,9 @@
"title": "Ersten Prototyp mit Angular 2.0 entwickeln", "title": "Ersten Prototyp mit Angular 2.0 entwickeln",
"description": "Der Prototyp soll zeigen, wie Routing und HTTP-Anbindung umgesetzt werden können.", "description": "Der Prototyp soll zeigen, wie Routing und HTTP-Anbindung umgesetzt werden können.",
"tags": [], "tags": [],
"state": "IN_PROGRESS", "state": "BACKLOG",
"assignee": { "assignee": {
"name": "Christoph Höller", "name": "Christoph Höllers",
"email": "" "email": ""
} }
}, },
@ -40,18 +40,18 @@
"change_settings": true "change_settings": true
}, },
{ {
"id" : 302, "id": 302,
"name": "user_edit", "name": "user_edit",
"password": "ea847988ba59727dbf4e34ee75726dc3", "password": "ea847988ba59727dbf4e34ee75726dc3",
"edit_tasks": true, "edit_tasks": true,
"change_settings": false "change_settings": false
}, },
{ {
"id" : 303, "id": 303,
"name": "user", "name": "user",
"password": "5ebe2294ecd0e0f08eab7690d2a6ee69", "password": "5ebe2294ecd0e0f08eab7690d2a6ee69",
"edit_tasks": false, "edit_tasks": false,
"change_settings": false "change_settings": false
} }
] ]
} }

View File

@ -31,6 +31,9 @@
<li routerLinkActive="active"> <li routerLinkActive="active">
<a routerLink='/blog' class="link">Blog</a> <a routerLink='/blog' class="link">Blog</a>
</li> </li>
<li routerLinkActive="active">
<a routerLink='/model-form' class="link">Model Form</a>
</li>
<!-- <!--
<li routerLinkActive="active"> <li routerLinkActive="active">
<a routerLink='/tasks/new' class="link">Neue Aufgabe anlegen</a> <a routerLink='/tasks/new' class="link">Neue Aufgabe anlegen</a>

View File

@ -16,6 +16,8 @@ import {SOCKET_IO, AUTH_ENABLED} from './app.tokens';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
import {mockIO} from './mocks/mock-socket'; import {mockIO} from './mocks/mock-socket';
import {TaskItemComponent} from './tasks/task-list/task-item.component'; 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() { export function socketIoFactory() {
if (environment.e2eMode) { if (environment.e2eMode) {
@ -29,6 +31,7 @@ const enableAuthentication = !environment.e2eMode;
@NgModule({ @NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule, appRouting, HttpModule], imports: [BrowserModule, FormsModule, ReactiveFormsModule, appRouting, HttpModule],
providers: [LoginService, providers: [LoginService,
UserService,
UserStore, UserStore,
TaskService, TaskService,
TaskStore, TaskStore,
@ -41,6 +44,7 @@ const enableAuthentication = !environment.e2eMode;
routingComponents, routingComponents,
TaskItemComponent, TaskItemComponent,
ShowErrorComponent, ShowErrorComponent,
ShowErrorComponentModelDriven,
APPLICATION_VALIDATORS], APPLICATION_VALIDATORS],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -10,15 +10,16 @@ import {tasksRoutes, tasksRoutingComponents, tasksRoutingProviders} from './task
import {RxDemoComponent} from './rxdemo/rxdemo.component'; import {RxDemoComponent} from './rxdemo/rxdemo.component';
import {LoginGuard} from './login/login.guard'; import {LoginGuard} from './login/login.guard';
import {blogRoutingComponents} from "./blog/blog.routing"; import {blogRoutingComponents} from "./blog/blog.routing";
import {ModelDrivenFormComponent} from "./model-driven-form/model-driven-form.component";
export const appRoutes: Routes = [ export const appRoutes: Routes = [
{path: 'dashboard', component: DashboardComponent, data: {title: 'Startseite'}}, {path: 'dashboard', component: DashboardComponent, data: {title: 'Startseite'}},
{path: '', redirectTo: '/dashboard', pathMatch: 'full'}, {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: 'about', component: AboutComponent, data: {title: 'Über uns'}},
{path: 'rxdemo', component: RxDemoComponent, data: {title: 'RxJS Demo'}}, {path: 'rxdemo', component: RxDemoComponent, data: {title: 'RxJS Demo'}},
{path: 'blog', component: BlogComponent, data: {title: 'Blog'}}, {path: 'blog', component: BlogComponent, data: {title: 'Blog'}},
{path: 'model-form', component: ModelDrivenFormComponent, data: { title: 'Model Driven Task Form' }},
{path: 'login', component: LoginComponent}, {path: 'login', component: LoginComponent},
@ -34,7 +35,7 @@ export const appRoutes: Routes = [
export const appRouting = RouterModule.forRoot(appRoutes); export const appRouting = RouterModule.forRoot(appRoutes);
export const routingComponents = [DashboardComponent, SettingsComponent, AboutComponent, LoginComponent, NotFoundComponent, export const routingComponents = [DashboardComponent, SettingsComponent, AboutComponent, LoginComponent, NotFoundComponent,
RxDemoComponent, ...blogRoutingComponents, ...tasksRoutingComponents]; RxDemoComponent, ModelDrivenFormComponent, ...blogRoutingComponents, ...tasksRoutingComponents];
export const routingProviders = [LoginGuard, export const routingProviders = [LoginGuard,
...tasksRoutingProviders]; ...tasksRoutingProviders];

View File

@ -2,8 +2,8 @@ import {TestBed} from "@angular/core/testing";
import {LoginComponent} from "./login.component"; import {LoginComponent} from "./login.component";
import {RouterTestingModule} from "@angular/router/testing"; import {RouterTestingModule} from "@angular/router/testing";
import {LoginService} from "../services/login-service/login-service"; import {LoginService} from "../services/login-service/login-service";
import {MockLoginService} from "../mocks/mock-login-service.spec";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {MockLoginService} from "../mocks/mock-login-service.spec";
import Spy = jasmine.Spy; import Spy = jasmine.Spy;
@ -32,8 +32,7 @@ describe('Login Component Template Driven Form Test with Spy', () => {
// Login ausfuellen und absenden // Login ausfuellen und absenden
loginFixture.whenStable().then(() => { loginFixture.whenStable().then(() => {
// Spy anmelden // Spy anmelden, die eigentliche Methode wird nicht mehr aufgerufen
// sobald Spy angemeldet ist, wird der eigentliche Methodenrumpf nicht mehr aufgerufen
const spy = spyOn(loginInstance, 'login'); const spy = spyOn(loginInstance, 'login');
// Formular Eingabe // Formular Eingabe

View File

@ -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>

View File

@ -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();
}));
*/
);

View File

@ -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);
};
}

View File

@ -0,0 +1,3 @@
import {ModelDrivenFormComponent} from "./model-driven-form.component";
export const modelDrivenFormRoutingComponent = [ModelDrivenFormComponent];

View File

@ -2,8 +2,10 @@ import {Directive, forwardRef} from '@angular/core';
import { import {
FormControl, FormControl,
AbstractControl, AbstractControl,
NG_VALIDATORS NG_VALIDATORS, NG_ASYNC_VALIDATORS
} from '@angular/forms'; } from '@angular/forms';
import {UserService} from "../services/user-service/user.service";
import {Observable} from "rxjs";
export function asyncIfNotBacklogThenAssignee(control): Promise<any> { export function asyncIfNotBacklogThenAssignee(control): Promise<any> {
const promise = new Promise((resolve, reject) => { 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({ @Directive({
selector: '[emailValidator]', selector: '[emailValidator]',
providers: [ providers: [

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -4,7 +4,6 @@ import {EditTaskComponent} from './edit-task/edit-task.component';
import {EditTaskGuard} from './edit-task/edit-task.guard'; import {EditTaskGuard} from './edit-task/edit-task.guard';
import {TaskOverviewComponent} from './task-overview/task-overview.component'; import {TaskOverviewComponent} from './task-overview/task-overview.component';
import {TasksComponent} from './tasks.component'; import {TasksComponent} from './tasks.component';
import {LoginGuard} from '../login/login.guard';
export const tasksRoutes: Routes = [{ export const tasksRoutes: Routes = [{
path: '', component: TasksComponent, path: '', component: TasksComponent,