First custom Dynamic Form Module generation
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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 { }
|
||||
|
||||
29
src/app/modules/dyn-form/services/question.service.ts
Normal file
29
src/app/modules/dyn-form/services/question.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
152
src/app/services/http-base.service.ts
Normal file
152
src/app/services/http-base.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
65
src/app/services/http-cached.service.ts
Normal file
65
src/app/services/http-cached.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
19
src/app/services/http.service.ts
Normal file
19
src/app/services/http.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
src/app/services/response.interface.ts
Normal file
9
src/app/services/response.interface.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ResponseInterface {
|
||||
meta: {
|
||||
code: number,
|
||||
text: string,
|
||||
uri?: string
|
||||
},
|
||||
data?: any,
|
||||
uri?: string
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user