First custom Dynamic Form Module generation
This commit is contained in:
@@ -15,4 +15,8 @@
|
||||
|
||||
<p>Value:</p>
|
||||
<pre>{{ resultTag | json }}</pre>
|
||||
<pre>{{ resultTag2 | json }}</pre>
|
||||
<pre>{{ resultTag2 | json }}</pre>
|
||||
|
||||
<hr>
|
||||
|
||||
<dyn-form [questions]="formQuestions" (onSubmit)="submit($event)"></dyn-form>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {QuestionInterface} from './modules/dyn-form/types/question.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
@@ -9,4 +10,25 @@ export class AppComponent {
|
||||
public resultCounter = 10;
|
||||
public resultTag = {"alpha": true, "beta": false};
|
||||
public resultTag2 = {"alpha": true, "beta": false};
|
||||
|
||||
public formQuestions: QuestionInterface[] = [
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
submit(value: any) {
|
||||
console.log("send", value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import {CustomInputModule} from "./custom-input.module";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { DynFormModule } from './modules/dyn-form/dyn-form.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -11,6 +12,7 @@ import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CustomInputModule,
|
||||
DynFormModule
|
||||
],
|
||||
declarations: [ AppComponent ],
|
||||
bootstrap: [ AppComponent ]
|
||||
|
||||
34
src/app/modules/dyn-form/dyn-form.component.ts
Normal file
34
src/app/modules/dyn-form/dyn-form.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {QuestionInterface} from './types/question.interface';
|
||||
import {FormControl, FormGroup} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'dyn-form',
|
||||
template: `
|
||||
<form (ngSubmit)="submit()" [formGroup]="form">
|
||||
<dyn-question *ngFor="let question of questions" [question]="question" [form]="form"></dyn-question>
|
||||
<div class="form-row">
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
export class DynFormComponent {
|
||||
@Input() questions: QuestionInterface[];
|
||||
@Input() value: any;
|
||||
@Input() type: "'insert'|'update'|'delete'|'view'";
|
||||
@Output() valueChange: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() onSubmit: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
private form: FormGroup;
|
||||
|
||||
constructor() {
|
||||
this.form = new FormGroup({
|
||||
'flags': new FormControl({'alpha': true})
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.onSubmit.emit(this.form.value);
|
||||
}
|
||||
}
|
||||
27
src/app/modules/dyn-form/dyn-form.module.ts
Normal file
27
src/app/modules/dyn-form/dyn-form.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
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';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
exports: [
|
||||
DynFormComponent
|
||||
],
|
||||
declarations: [
|
||||
DynFormComponent,
|
||||
DynQuestionComponent,
|
||||
CounterInputComponent,
|
||||
TagInputComponent,
|
||||
KeysPipe
|
||||
],
|
||||
providers: [],
|
||||
})
|
||||
export class DynFormModule { }
|
||||
26
src/app/modules/dyn-form/dyn-question.component.ts
Normal file
26
src/app/modules/dyn-form/dyn-question.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {QuestionInterface} from './types/question.interface';
|
||||
import {FormGroup} from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'dyn-question',
|
||||
template: `
|
||||
<div [ngSwitch]="question.type" [formGroup]="form">
|
||||
<label [for]="question.properties.key">{{question.properties.label}}</label>
|
||||
<tag-input *ngSwitchCase="'flag'"
|
||||
[formControlName]="question.properties.key" [id]="question.properties.key"
|
||||
[items]="question.properties.options" [nullable]="question.constraints.optional"
|
||||
(ngModelChange)="change()"></tag-input>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class DynQuestionComponent {
|
||||
@Input() question: QuestionInterface;
|
||||
@Input() type: "'insert'|'update'|'delete'|'view'";
|
||||
@Input() form: FormGroup;
|
||||
@Output() onChange: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
change() {
|
||||
this.onChange.emit();
|
||||
}
|
||||
}
|
||||
55
src/app/modules/dyn-form/inputs/counter-input.component.ts
Normal file
55
src/app/modules/dyn-form/inputs/counter-input.component.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
34
src/app/modules/dyn-form/inputs/custom-input.component.ts
Normal file
34
src/app/modules/dyn-form/inputs/custom-input.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
89
src/app/modules/dyn-form/inputs/tag-input.component.ts
Normal file
89
src/app/modules/dyn-form/inputs/tag-input.component.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
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)">
|
||||
{{lookup[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;
|
||||
|
||||
private lookup: {[_:string]: string} = {};
|
||||
|
||||
// set initial value
|
||||
public writeValue(obj: any): void {
|
||||
let isUpdated = false;
|
||||
this.value = {};
|
||||
this.lookup = {};
|
||||
if (!obj) {
|
||||
obj = {};
|
||||
}
|
||||
for (let i=0; i<this.items.length; i++) {
|
||||
let key = this.items[i].key;
|
||||
this.lookup[key] = this.items[i].value;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
31
src/app/modules/dyn-form/types/question.interface.ts
Normal file
31
src/app/modules/dyn-form/types/question.interface.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface QuestionInterface {
|
||||
type: "flag"|"textbox"|"textarea"|"hidden"|"dropdown",
|
||||
description: string,
|
||||
properties: {
|
||||
key: string,
|
||||
label?: string,
|
||||
order: number,
|
||||
|
||||
methods?: Array<string>,
|
||||
spezialization?: string,
|
||||
|
||||
// textbox
|
||||
type?: string,
|
||||
placeholder?: string,
|
||||
|
||||
// dropdown
|
||||
foreign?: {uri: string, keys: string, values: string, options?: {key: string, value: string}[]},
|
||||
|
||||
// dropdown & flag
|
||||
options?: {key: string, value: string}[],
|
||||
},
|
||||
constraints: {
|
||||
optional: boolean,
|
||||
|
||||
// textbox - string
|
||||
maxLength?: number,
|
||||
|
||||
// textbox - number
|
||||
maxValue?: number
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user