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