diff --git a/src/app/app.component.html b/src/app/app.component.html index 46e4321..54a1708 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,22 +1 @@ -
- -
- -

Value:

-
{{ resultCounter }}
- - -
- -
- - -
- -

Value:

-
{{ resultTag | json }}
-
{{ resultTag2 | json }}
- -
- \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b016c4f..798c6aa 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b67ccab..f255f86 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 { } diff --git a/src/app/counter-input.component.ts b/src/app/counter-input.component.ts deleted file mode 100644 index 5fd0d80..0000000 --- a/src/app/counter-input.component.ts +++ /dev/null @@ -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: - ` - - {{value}} - - `, - 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(); - } -} \ No newline at end of file diff --git a/src/app/custom-input.component.ts b/src/app/custom-input.component.ts deleted file mode 100644 index 1a2f61b..0000000 --- a/src/app/custom-input.component.ts +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/src/app/custom-input.module.ts b/src/app/custom-input.module.ts deleted file mode 100644 index b31eaf6..0000000 --- a/src/app/custom-input.module.ts +++ /dev/null @@ -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 { } diff --git a/src/app/modules/dyn-form/dyn-form.module.ts b/src/app/modules/dyn-form/dyn-form.module.ts index a34af3e..cc79e60 100644 --- a/src/app/modules/dyn-form/dyn-form.module.ts +++ b/src/app/modules/dyn-form/dyn-form.module.ts @@ -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 { } diff --git a/src/app/modules/dyn-form/services/question.service.ts b/src/app/modules/dyn-form/services/question.service.ts new file mode 100644 index 0000000..3827249 --- /dev/null +++ b/src/app/modules/dyn-form/services/question.service.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/app/services/http-base.service.ts b/src/app/services/http-base.service.ts new file mode 100644 index 0000000..ffb2cac --- /dev/null +++ b/src/app/services/http-base.service.ts @@ -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 = 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, 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; + } +} \ No newline at end of file diff --git a/src/app/services/http-cached.service.ts b/src/app/services/http-cached.service.ts new file mode 100644 index 0000000..503b060 --- /dev/null +++ b/src/app/services/http-cached.service.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/app/services/http.service.ts b/src/app/services/http.service.ts new file mode 100644 index 0000000..c461192 --- /dev/null +++ b/src/app/services/http.service.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/app/services/response.interface.ts b/src/app/services/response.interface.ts new file mode 100644 index 0000000..321bc87 --- /dev/null +++ b/src/app/services/response.interface.ts @@ -0,0 +1,9 @@ +export interface ResponseInterface { + meta: { + code: number, + text: string, + uri?: string + }, + data?: any, + uri?: string +} \ No newline at end of file diff --git a/src/app/tag-input.component.ts b/src/app/tag-input.component.ts deleted file mode 100644 index 5f7ed4d..0000000 --- a/src/app/tag-input.component.ts +++ /dev/null @@ -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: - ` -
- {{items[entry.key]}}: {{entry.value}} -
- `, - 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; - } -} \ No newline at end of file