First custom Dynamic Form Module generation

This commit is contained in:
Sebastian Seedorf
2017-05-30 09:10:38 +02:00
parent a5fdfa32d2
commit 48b4f67b06
13 changed files with 293 additions and 248 deletions

View File

@@ -1,22 +1 @@
<form #formCounter="ngForm">
<counter-input [(ngModel)]="resultCounter" name="res"></counter-input>
</form>
<p>Value:</p>
<pre>{{ resultCounter }}</pre>
<hr>
<form #formTag="ngForm">
<tag-input [(ngModel)]="resultTag" name="res" [id]="'a'" [items]="{'alpha': 'A', 'beta': 'B', 'gamma': 'G'}"></tag-input>
<tag-input [(ngModel)]="resultTag2" name="res2" [id]="'b'" [items]="{'alpha': 'A', 'beta': 'B', 'gamma': 'G'}" [nullable]="true"></tag-input>
</form>
<p>Value:</p>
<pre>{{ resultTag | json }}</pre>
<pre>{{ resultTag2 | json }}</pre>
<hr>
<dyn-form [questions]="formQuestions" (onSubmit)="submit($event)"></dyn-form>

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import {Component, Inject} from '@angular/core';
import {QuestionInterface} from './modules/dyn-form/types/question.interface';
import {QuestionService} from './modules/dyn-form/services/question.service';
@Component({
selector: 'my-app',
@@ -7,26 +8,14 @@ import {QuestionInterface} from './modules/dyn-form/types/question.interface';
})
export class AppComponent {
public resultCounter = 10;
public resultTag = {"alpha": true, "beta": false};
public resultTag2 = {"alpha": true, "beta": false};
public formQuestions: QuestionInterface[] = [];
public formQuestions: QuestionInterface[] = [
{
type: "flag",
description: "This is a help tooltip",
properties: {
key: "flags",
label: "Form Type Flags",
order: 1,
constructor(private questionService: QuestionService) {
this.questionService.getQuestions((res, err) => {
this.formQuestions = res;
});
}
// dropdown && flags
options: [{key: "alpha", value: "A"}, {key: "beta", value: "B"}]
}, constraints: {
optional: true
}
}
];
submit(value: any) {
console.log("send", value);

View File

@@ -2,19 +2,20 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import {CustomInputModule} from "./custom-input.module";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { DynFormModule } from './modules/dyn-form/dyn-form.module';
import {HttpCachedService} from './services/http-cached.service';
import {HttpModule} from '@angular/http';
@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
CustomInputModule,
DynFormModule
DynFormModule,
HttpModule
],
declarations: [ AppComponent ],
providers: [
HttpCachedService
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@@ -1,55 +0,0 @@
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl, ValidationErrors } from '@angular/forms';
import {CustomInputComponent} from "./custom-input.component";
@Component({
selector: 'counter-input',
template:
`
<button (click)="onChange(-1)" (blur)="onBlur()">-</button>
{{value}}
<button (click)="onChange(1)" (blur)="onBlur()">+</button>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CounterInputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CounterInputComponent),
multi: true,
}]
})
export class CounterInputComponent extends CustomInputComponent {
// set initial value
public writeValue(obj: any): void {
if (obj) {
this.value = parseInt(obj);
this.parseError = isNaN(this.value);
}
}
// validates the form, returns null when valid else the validation object
public validate(c: FormControl): ValidationErrors {
return (!this.parseError) ? null : {
numberParseError: {
valid: false,
},
};
}
// on button click
protected onChange(number: number): void {
this.value = this.value + number;
this.parseError = isNaN(this.value);
super.change();
}
// on touched
protected onBlur(): void {
super.blur();
}
}

View File

@@ -1,34 +0,0 @@
import {ControlValueAccessor, FormControl, ValidationErrors, Validator} from '@angular/forms';
export abstract class CustomInputComponent implements ControlValueAccessor, Validator {
protected value: any;
protected parseError: boolean;
private propagateChange: (_: any) => void = () => {};
private propagateTouch: () => void = () => {};
// set initial value
public abstract writeValue(obj: any): void;
// register function to notify on change
public registerOnChange(fn: (_: any) => void): void {
this.propagateChange = fn;
}
// register function to notify on touch
public registerOnTouched(fn: () => void): void {
this.propagateTouch = fn;
}
// validates the form, returns null when valid else the validation object
public abstract validate(c: FormControl): ValidationErrors;
// on change
protected change(): void {
this.propagateChange(this.value);
}
// on touch
protected blur(): void {
this.propagateTouch();
}
}

View File

@@ -1,24 +0,0 @@
import {FormsModule} from '@angular/forms';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CounterInputComponent } from './counter-input.component';
import {KeysPipe, TagInputComponent} from './tag-input.component';
@NgModule({
imports: [
CommonModule,
FormsModule,
],
exports: [
CounterInputComponent,
TagInputComponent,
],
declarations: [
CounterInputComponent,
TagInputComponent,
KeysPipe
],
providers: [],
})
export class CustomInputModule { }

View File

@@ -5,6 +5,7 @@ import { DynFormComponent } from './dyn-form.component';
import {CounterInputComponent} from './inputs/counter-input.component';
import {KeysPipe, TagInputComponent} from './inputs/tag-input.component';
import {DynQuestionComponent} from './dyn-question.component';
import {QuestionService} from './services/question.service';
@NgModule({
@@ -22,6 +23,8 @@ import {DynQuestionComponent} from './dyn-question.component';
TagInputComponent,
KeysPipe
],
providers: [],
providers: [
QuestionService
],
})
export class DynFormModule { }

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import {HttpCachedService} from '../../../services/http-cached.service';
import {QuestionInterface} from '../types/question.interface';
@Injectable()
export class QuestionService {
constructor(private httpCachedService: HttpCachedService) {
}
public getQuestions(cb: (res: QuestionInterface[], err: any) => void) {
setTimeout(() => cb([
{
type: "flag",
description: "This is a help tooltip",
properties: {
key: "flags",
label: "Form Type Flags",
order: 1,
// dropdown && flags
options: [{key: "alpha", value: "A"}, {key: "beta", value: "B"}]
},
constraints: {
optional: true
}
}
], null), 4000);
}
}

View File

@@ -0,0 +1,152 @@
import { Http, Response, Headers, RequestOptionsArgs } from '@angular/http';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { Observable } from 'rxjs/Observable';
import { ResponseInterface } from './response.interface';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
export abstract class HttpBaseService {
abstract get httpBase(): string;
abstract get httpSuffix(): string;
errorEmitter: ReplaySubject<any> = new ReplaySubject(1);
constructor(private _http: Http) {
}
getJSON(uri: string, query: {[propName: string]: any}, dataFunc: (data: ResponseInterface, e: Response | any) => void) {
var d: ResponseInterface = null;
var e: any = null;
var that = this;
this._doRequest(
this._http.get(this._getURI(uri, query), this._getOptions),
dataFunc
);
}
postJSON(uri: string, query: {[propName: string]: any}, body: {[propName: string]: any}, dataFunc: (data: ResponseInterface, e: Response | any) => void) {
var d: ResponseInterface;
var e: any;
var that = this;
this._doRequest(
this._http.post(this._getURI(uri, query), this._JSONtoQueryString(body), this._postOptions),
dataFunc
);
}
putJSON(uri: string, query: {[propName: string]: any}, body: {[propName: string]: any}, dataFunc: (data: ResponseInterface, e: Response | any) => void) {
var d: ResponseInterface;
var e: any;
var that = this;
this._doRequest(
this._http.put(this._getURI(uri, query), this._JSONtoQueryString(body), this._putOptions),
dataFunc
);
}
deleteJSON(uri: string, query: {[propName: string]: any}, dataFunc: (data: ResponseInterface, e: Response | any) => void) {
var d: ResponseInterface;
var e: any;
var that = this;
this._doRequest(
this._http.delete(this._getURI(uri, query), this._deleteOptions),
dataFunc
);
}
private _doRequest(observe: Observable<Response>, dataFunc: (data: ResponseInterface, e: Response | any) => void) {
var d: ResponseInterface;
var e: any;
var that = this;
function _handleError(error: Response | any) {
function isJsonParsable(str: any) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
let errObj: {status: number, statusText: string, msg: string};
if (error instanceof Response) {
var err: string;
if (isJsonParsable(error.text())) {
const body = error.json();
err = JSON.stringify(body.meta.text || body.meta || body || '');
} else {
err = error.text();
}
errMsg = `${error.status} - ${error.statusText || ''} - ${err}`;
errObj = {status: error.status, statusText: error.statusText || '', msg: err};
} else {
errMsg = error.message ? error.message : error.toString();
errObj = {status: -1, statusText: "Server Error", msg: errMsg};
}
console.error(errMsg);
console.error(errObj);
that.errorEmitter.next(errObj);
return Observable.throw(errMsg);
}
observe
.map(this._extractData)
.catch(_handleError)
.subscribe(
data => d = data,
error => {e = error; /*_handleError(error);*/ dataFunc(null, e);},
() => dataFunc(d, e)
);
}
private _getURI(uri: string, query: {[propName: string]: any}): string {
var qString: string = this._JSONtoQueryString(query);
if (qString)
return this.httpBase + uri + this.httpSuffix + '?' + this._JSONtoQueryString(query);
else
return this.httpBase + uri + this.httpSuffix;
}
private get _getOptions(): RequestOptionsArgs {
var headers = new Headers();
return {
headers: headers,
withCredentials: true
};
}
private get _postOptions(): RequestOptionsArgs {
var headers = new Headers();
headers.append('Content-Type', 'application/x-www-form-urlencoded');
return {
headers: headers,
withCredentials: true
};
}
private get _putOptions(): RequestOptionsArgs {
return this._postOptions;
}
private get _deleteOptions(): RequestOptionsArgs {
return this._getOptions;
}
private _JSONtoQueryString(json: {[propName: string]: any}) {
return Object.keys(json).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]);
}).join('&');
}
private _extractData(res: Response): ResponseInterface {
var body = res.json();
var resBody: ResponseInterface = body as ResponseInterface || null;
return resBody;
}
}

View File

@@ -0,0 +1,65 @@
import { Injectable, Inject } from '@angular/core';
import { Http, Response } from '@angular/http';
import { HttpService } from './http.service';
import { ResponseInterface } from './response.interface';
@Injectable()
export class HttpCachedService extends HttpService {
private _stored: any = {}; // {data: ResponseInterface, e: Response}
get refreshTime() {
return 3*1000;
}
constructor(@Inject(Http) _http: Http) {
super(_http);
}
getJSON(uri: string, query: {[propName: string]: any}, dataFunc: (data: ResponseInterface, e: Response | any) => void) {
var hash: string = this._hashRequest("GET", uri, query);
if (this._stored.hasOwnProperty(hash)) {
if (this._stored[hash] instanceof Array) {
// pending request, add to event emitter
this._stored[hash].push(dataFunc);
} else {
// request alredy stored, send result
dataFunc(this._stored[hash].data, this._stored[hash].e);
}
} else {
// fulfil request, no matching request stored
this._stored[hash] = [];
super.getJSON(uri, query, (data, e) => {
// request recieved, emit to caller and event subscriber
dataFunc(data, e);
for (var i = this._stored[hash].length - 1; i >= 0; i--) {
this._stored[hash][i](data, e);
}
this._stored[hash] = {
data: data,
e: e
};
var that = this;
setTimeout(function() {
delete that._stored[hash];
}, this.refreshTime);
});
}
}
private _hashRequest(method: string, uri: string, query?: Object): string {
function objectToString(obj: Object = {}) {
var keys: string[] = [];
var result: string = '{';
for (var key in obj)
if (obj.hasOwnProperty(key))
keys.push(key);
keys.sort();
for (var i = keys.length - 1; i >= 0; i--) {
result += keys[i]+':"'+obj[keys[i]]+'",'
}
result += '}';
return result;
}
return method+" "+uri+" "+objectToString(query);
}
}

View File

@@ -0,0 +1,19 @@
import { Injectable, Inject } from '@angular/core';
import { Http } from '@angular/http';
import { HttpBaseService } from './http-base.service';
@Injectable()
export class HttpService extends HttpBaseService {
get httpBase() {
return "http://ccrruby1-1:3085";
//return '/public/mocks';
}
get httpSuffix() {
return "";
//return '.json';
}
constructor(@Inject(Http) _http: Http) {
super(_http);
}
}

View File

@@ -0,0 +1,9 @@
export interface ResponseInterface {
meta: {
code: number,
text: string,
uri?: string
},
data?: any,
uri?: string
}

View File

@@ -1,88 +0,0 @@
import {Component, forwardRef, Input, Pipe, PipeTransform} from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl, ValidationErrors } from '@angular/forms';
import {CustomInputComponent} from "./custom-input.component";
@Pipe({name: 'keys'})
export class KeysPipe implements PipeTransform {
transform(value: Object, args:string[]) : any {
let keys = [];
for (let key in value) {
keys.push({key: key, value: value[key]});
}
return keys;
}
}
@Component({
selector: 'tag-input',
template:
`
<div *ngFor="let entry of value | keys" (click)="onChange(entry.key, true)" (contextmenu)="onChange(entry.key, false)">
{{items[entry.key]}}: {{entry.value}}
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TagInputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TagInputComponent),
multi: true,
}]
})
export class TagInputComponent extends CustomInputComponent {
@Input() items: Array<{key: string, value: string}> = [];
@Input() nullable: boolean = false;
@Input() id: string = "";
// set initial value
public writeValue(obj: any): void {
let isUpdated = false;
this.value = {};
if (!obj) {
obj = {};
}
for (let key in this.items) {
if (this.items.hasOwnProperty(key)) {
if (obj.hasOwnProperty(key) && (typeof(obj[key]) === "boolean" || (this.nullable && null===obj[key])))
this.value[key] = obj[key];
else {
isUpdated = true;
this.value[key] = (this.nullable ? null : false);
}
}
}
console.log(this.id, isUpdated, this.value);
if (isUpdated) {
super.change();
}
}
// validates the form, returns null when valid else the validation object
public validate(c: FormControl): ValidationErrors {
return null;
}
// on button click
protected onChange(key: string, forward: boolean): boolean {
let value = this.value[key];
if (!this.nullable)
value = !value;
else if (value===null)
value = forward;
else if ((value ? !forward : forward))
value = null;
else
value = !value;
this.value = Object.assign({}, this.value);
this.value[key] = value;
super.change();
return false;
}
}