Przeglądaj źródła

Adding skeleton Dynaform

Richard Knight 6 lat temu
rodzic
commit
cbde32bff2
31 zmienionych plików z 12509 dodań i 17 usunięć
  1. 11700 0
      package-lock.json
  2. 3 0
      package.json
  3. 10 0
      src/app/dircetives/component-host.directive.ts
  4. 24 0
      src/app/dircetives/json-formatter.directive.ts
  5. 8 0
      src/app/dynaform/components/dynaform/dynaform.component.html
  6. 0 0
      src/app/dynaform/components/dynaform/dynaform.component.scss
  7. 25 0
      src/app/dynaform/components/dynaform/dynaform.component.spec.ts
  8. 30 0
      src/app/dynaform/components/dynaform/dynaform.component.ts
  9. 1 0
      src/app/dynaform/components/fields/basicinput/basicinput.component.html
  10. 0 0
      src/app/dynaform/components/fields/basicinput/basicinput.component.scss
  11. 25 0
      src/app/dynaform/components/fields/basicinput/basicinput.component.spec.ts
  12. 52 0
      src/app/dynaform/components/fields/basicinput/basicinput.component.ts
  13. 8 0
      src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.html
  14. 0 0
      src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.scss
  15. 25 0
      src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.spec.ts
  16. 51 0
      src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.ts
  17. 12 0
      src/app/dynaform/components/fields/checkbutton/checkbutton.component.html
  18. 17 0
      src/app/dynaform/components/fields/checkbutton/checkbutton.component.scss
  19. 25 0
      src/app/dynaform/components/fields/checkbutton/checkbutton.component.spec.ts
  20. 60 0
      src/app/dynaform/components/fields/checkbutton/checkbutton.component.ts
  21. 9 0
      src/app/dynaform/components/fields/dropdown-modified-input/dropdown-modified-input.component.html
  22. 25 0
      src/app/dynaform/components/fields/dropdown-modified-input/dropdown-modified-input.component.spec.ts
  23. 86 0
      src/app/dynaform/components/fields/dropdown-modified-input/dropdown-modified-input.component.ts
  24. 6 0
      src/app/dynaform/components/layout/dynarow/dynarow.component.html
  25. 0 0
      src/app/dynaform/components/layout/dynarow/dynarow.component.scss
  26. 25 0
      src/app/dynaform/components/layout/dynarow/dynarow.component.spec.ts
  27. 82 0
      src/app/dynaform/components/layout/dynarow/dynarow.component.ts
  28. 10 0
      src/app/dynaform/directives/component-host.directive.ts
  29. 10 0
      src/app/dynaform/dynaform.module.ts
  30. 162 0
      src/app/dynaform/models/index.ts
  31. 18 17
      tsconfig.json

Plik diff jest za duży
+ 11700 - 0
package-lock.json


+ 3 - 0
package.json

@@ -22,6 +22,8 @@
     "@angular/platform-browser-dynamic": "^5.2.0",
     "@angular/router": "^5.2.0",
     "core-js": "^2.4.1",
+    "json-formatter-js": "^2.2.0",
+    "lodash": "^4.17.10",
     "rxjs": "^5.5.6",
     "zone.js": "^0.8.19"
   },
@@ -31,6 +33,7 @@
     "@angular/language-service": "^5.2.0",
     "@types/jasmine": "~2.8.3",
     "@types/jasminewd2": "~2.0.2",
+    "@types/lodash": "^4.14.109",
     "@types/node": "~6.0.60",
     "codelyzer": "^4.0.1",
     "jasmine-core": "~2.8.0",

+ 10 - 0
src/app/dircetives/component-host.directive.ts

@@ -0,0 +1,10 @@
+import { Directive, ViewContainerRef } from '@angular/core';
+
+@Directive({
+	selector: '[component-host]'
+})
+export class ComponentHostDirective {
+
+	constructor(public viewContainerRef: ViewContainerRef) { }
+
+}

+ 24 - 0
src/app/dircetives/json-formatter.directive.ts

@@ -0,0 +1,24 @@
+import { Directive, Input, OnChanges, ElementRef } from '@angular/core';
+import JSONFormatter from 'json-formatter-js';
+
+@Directive({
+	selector: 'json-formatter'
+})
+export class JsonFormatterDirective implements OnChanges {
+
+	@Input()
+	data: Object;
+
+	constructor(private elRef: ElementRef) { }
+
+	ngOnChanges() {
+		if (this.data) {
+			const formatter = new JSONFormatter(this.data);
+			while (this.elRef.nativeElement.firstChild) {
+				this.elRef.nativeElement.removeChild(this.elRef.nativeElement.firstChild);
+			}
+			this.elRef.nativeElement.appendChild(formatter.render());
+		}
+	}
+}
+			

+ 8 - 0
src/app/dynaform/components/dynaform/dynaform.component.html

@@ -0,0 +1,8 @@
+<span [formGroup]="cc.control">
+	<app-dynarow *ngFor="let field of controlNames"
+		[formControlName]="field"
+		[meta]="formMetaData[field]"
+		>
+	</app-dynarow>
+	<!-- <input *ngFor="let field of controlNames" type="text" [formControlName]="field"> -->
+</span>

+ 0 - 0
src/app/dynaform/components/dynaform/dynaform.component.scss


+ 25 - 0
src/app/dynaform/components/dynaform/dynaform.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DynaformComponent } from './dynaform.component';
+
+describe('DynaformComponent', () => {
+  let component: DynaformComponent;
+  let fixture: ComponentFixture<DynaformComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ DynaformComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(DynaformComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 30 - 0
src/app/dynaform/components/dynaform/dynaform.component.ts

@@ -0,0 +1,30 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { ControlContainer, FormGroup, FormControl } from '@angular/forms';
+import { KeysPipe } from '@pipes/keys.pipe';
+
+@Component({
+	selector: 'app-dynaform',
+	templateUrl: './dynaform.component.html',
+	styleUrls: ['./dynaform.component.scss'],
+	providers: [ KeysPipe ]
+})
+export class DynaformComponent implements OnInit {
+
+	@Input()
+	set meta(val) {
+		this.formMetaData = val;
+	}
+
+	formMetaData: any; // TODO: Tighten up type
+	controlNames: Array<string>;
+
+	constructor(private cc: ControlContainer) { 
+	}
+
+	ngOnInit() {
+		if (this.cc) {
+			this.controlNames = Object.keys(this.cc.control.value);
+		}
+	}
+
+}

+ 1 - 0
src/app/dynaform/components/fields/basicinput/basicinput.component.html

@@ -0,0 +1 @@
+<input type="text" (change)="onChange($event.target.value)" class="form-control form-control-sm">

+ 0 - 0
src/app/dynaform/components/fields/basicinput/basicinput.component.scss


+ 25 - 0
src/app/dynaform/components/fields/basicinput/basicinput.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BasicinputComponent } from './basicinput.component';
+
+describe('BasicinputComponent', () => {
+  let component: BasicinputComponent;
+  let fixture: ComponentFixture<BasicinputComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ BasicinputComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(BasicinputComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 52 - 0
src/app/dynaform/components/fields/basicinput/basicinput.component.ts

@@ -0,0 +1,52 @@
+import { Component, Input, OnInit, forwardRef } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+@Component({
+	selector: 'app-basicinput',
+	templateUrl: './basicinput.component.html',
+	styleUrls: ['./basicinput.component.scss'],
+	providers: [
+		{
+			provide: NG_VALUE_ACCESSOR,
+			useExisting: forwardRef(() => BasicinputComponent),
+			multi: true
+		}
+	]
+})
+export class BasicinputComponent implements OnInit {
+
+	@Input()
+	type?: string = 'text';
+
+	@Input()
+	value?: string | boolean = true;
+
+	@Input()
+	isDisabled?: boolean = false;
+	
+	@Input()
+	meta = {};
+
+	currentValue: string;
+	propagateChange = (_: any) => {};
+
+	ngOnInit() {
+		// meta input overrides other inputs if provided
+		['type', 'value', 'isDisabled'].map(p => this[p] = this.meta[p] || this[p]);
+	}
+	
+	onChange(value) {
+		this.currentValue = value;
+		this.propagateChange(value);
+	}
+
+	public writeValue(value: any): void {
+		this.currentValue = value;
+	}
+
+	public registerOnChange(fn: any): void {
+		this.propagateChange = fn;
+	}
+
+	public registerOnTouched(fn: any): void {}
+}

+ 8 - 0
src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.html

@@ -0,0 +1,8 @@
+<div class="lmp-checkbutton-group btn-group btn-group-toggle" [ngClass]="extraClass" [formGroup]="cc.control">
+	<app-checkbutton *ngFor="let control of controlNames; let i = index"
+		[label]="labels[i]"
+		[formControlName]="control"
+		[isDisabled]="isDisabled[i]"
+	>
+	</app-checkbutton>
+</div>

+ 0 - 0
src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.scss


+ 25 - 0
src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CheckbuttonGroupComponent } from './checkbutton-group.component';
+
+describe('CheckbuttonGroupComponent', () => {
+  let component: CheckbuttonGroupComponent;
+  let fixture: ComponentFixture<CheckbuttonGroupComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ CheckbuttonGroupComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CheckbuttonGroupComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 51 - 0
src/app/dynaform/components/fields/checkbutton-group/checkbutton-group.component.ts

@@ -0,0 +1,51 @@
+import { Component, Attribute, OnInit, Input, ElementRef } from '@angular/core';
+import { ControlContainer, FormGroup, FormControl } from '@angular/forms';
+
+@Component({
+	selector: 'app-checkbutton-group',
+	templateUrl: './checkbutton-group.component.html',
+	styleUrls: ['./checkbutton-group.component.scss']
+})
+export class CheckbuttonGroupComponent implements OnInit {
+
+	@Input()
+	labels: Array<string>;
+
+	@Input()
+	meta = {};
+
+	controlNames: Array<string>;
+	isDisabled: Array<boolean>;
+	extraClass: string;
+
+	firstControl: FormControl;
+
+	constructor(
+		private el: ElementRef,
+		private cc: ControlContainer,
+		@Attribute('firstEnablesRest') private firstEnablesRest
+	) { 
+		// Read the classes applied to the host element, to pass through to the template
+		this.extraClass = el.nativeElement.className;
+		this.firstEnablesRest = firstEnablesRest === ''; // True if 'firstEnablesRest' exists as component attribute
+	}
+
+	ngOnInit() {
+		this.controlNames = Object.keys(this.cc.control.value);
+		const labelShortfall = this.controlNames.length - this.labels.length;
+		if (labelShortfall > 0) {
+			this.labels = [ ...this.labels, ...Array(labelShortfall).fill('LABEL MISSING') ];
+		}
+		if (this.firstEnablesRest) {
+			this.firstControl = (<FormGroup>this.cc.control).controls[this.controlNames[0]] as FormControl;
+			this.isDisabled = [false, ...Array(this.controlNames.length - 1).fill(!this.firstControl.value)];
+			// Observe value changes on first control
+			this.firstControl.valueChanges.subscribe(val => {
+				this.isDisabled = [false, ...Array(this.controlNames.length - 1).fill(!val)];
+			});
+		} else {
+			this.isDisabled = Array(this.controlNames.length).fill(false);
+		}
+	}
+
+}

+ 12 - 0
src/app/dynaform/components/fields/checkbutton/checkbutton.component.html

@@ -0,0 +1,12 @@
+<a (click)="toggleChecked($event)"
+	class="btn btn-sm lmp-checkbutton"
+	[ngClass]="{
+		'btn-success': isChecked,
+		'lmp-btn-greyed': !isChecked,
+		'lmp-btn-disabled': isDisabled
+	}"
+	href
+	>
+	<span class="k-icon" [ngClass]="{ 'k-i-check': isChecked, 'k-i-x': !isChecked }"></span>
+	{{ label }}
+</a>

+ 17 - 0
src/app/dynaform/components/fields/checkbutton/checkbutton.component.scss

@@ -0,0 +1,17 @@
+.lmp-checkbutton {
+	padding: 3px 9px 3px 6px;
+	margin: 0 3px 3px 0;
+}
+
+.lmp-btn-greyed {
+	color: white;
+	background-color: #BBB;
+}
+
+.lmp-btn-disabled, .lmp-btn-disabled:active {
+	color: #F3F3F3;
+	background-color: #E3E3E3;
+	border-color: #E3E3E3;
+	box-shadow: none;
+	cursor: not-allowed !important;
+}

+ 25 - 0
src/app/dynaform/components/fields/checkbutton/checkbutton.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CheckbuttonComponent } from './checkbutton.component';
+
+describe('CheckbuttonComponent', () => {
+  let component: CheckbuttonComponent;
+  let fixture: ComponentFixture<CheckbuttonComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ CheckbuttonComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CheckbuttonComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should be created', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 60 - 0
src/app/dynaform/components/fields/checkbutton/checkbutton.component.ts

@@ -0,0 +1,60 @@
+import { Component, OnInit, Input, forwardRef } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+@Component({
+	selector: 'app-checkbutton',
+	templateUrl: './checkbutton.component.html',
+	styleUrls: ['./checkbutton.component.scss'],
+	providers: [
+		{
+			provide: NG_VALUE_ACCESSOR,
+			useExisting: forwardRef(() => CheckbuttonComponent),
+			multi: true
+		}
+	]
+})
+export class CheckbuttonComponent implements OnInit, ControlValueAccessor {
+
+	@Input()
+	label?: string;
+
+	@Input()
+	value?: string | boolean = true;
+
+	@Input()
+	isDisabled?: boolean = false;
+	
+	@Input()
+	meta = {};
+
+	isChecked: boolean;
+	currentValue: string | boolean;
+
+	propagateChange = (_: any) => {};
+
+	ngOnInit() {
+		// meta input overrides other inputs if provided
+		['label', 'value', 'isDisabled'].map(p => this[p] = this.meta[p] || this[p]);
+	}
+	
+	private toggleChecked(e): void {
+		e.target.blur();
+		e.preventDefault();
+		if (this.isDisabled) return;
+		this.isChecked = !this.isChecked;
+		this.currentValue = this.isChecked ? this.value : false;
+		this.propagateChange(this.currentValue);
+	}
+
+	public writeValue(value: any): void {
+		this.isChecked = value && value !== 0 && value !== 'false' ? value : false;
+		this.currentValue = this.isChecked ? value : false;
+	}
+
+	public registerOnChange(fn: any): void {
+		this.propagateChange = fn;
+	}
+
+	public registerOnTouched(fn: any): void {}
+
+}

+ 9 - 0
src/app/dynaform/components/fields/dropdown-modified-input/dropdown-modified-input.component.html

@@ -0,0 +1,9 @@
+<div class="input-group" [ngClass]="extraClass">
+	<div class="input-group-prepend" ngbDropdown>
+		<button class="btn btn-outline-secondary" ngbDropdownToggle>{{ selectedModifier }}</button>
+		<div ngbDropdownMenu>
+			<button *ngFor="let modifier of modifiers" class="dropdown-item" (click)="modifierChange(modifier)">{{ modifier }}</button>
+		</div>
+	</div>
+	<input class="form-control" [(ngModel)]="displayedValue" (change)="inputChange()">
+</div>

+ 25 - 0
src/app/dynaform/components/fields/dropdown-modified-input/dropdown-modified-input.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DropdownModifiedInputComponent } from './dropdown-modified-input.component';
+
+describe('DropdownModifiedInputComponent', () => {
+  let component: DropdownModifiedInputComponent;
+  let fixture: ComponentFixture<DropdownModifiedInputComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ DropdownModifiedInputComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(DropdownModifiedInputComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 86 - 0
src/app/dynaform/components/fields/dropdown-modified-input/dropdown-modified-input.component.ts

@@ -0,0 +1,86 @@
+import { Component, OnInit, Input, ElementRef, forwardRef } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export interface ValueTransformer {
+	inputFn: (value: string) => { modifier: string, value: string }
+	outputFn: (modifier: string, value: string) => string
+}
+
+@Component({
+	selector: 'app-dropdown-modified-input',
+	templateUrl: 'dropdown-modified-input.component.html',
+	styles: [],
+	providers: [
+		{ 
+		  provide: NG_VALUE_ACCESSOR,
+		  useExisting: forwardRef(() => DropdownModifiedInputComponent),
+		  multi: true
+		}
+	]
+})
+export class DropdownModifiedInputComponent implements OnInit, ControlValueAccessor {
+
+	@Input()
+	modifiers?: Array<string>;
+
+	@Input()
+	transform?: ValueTransformer = {
+		inputFn: value => ({ modifier: '', value: value }),
+		outputFn: (modifier, value) => value
+	};
+
+	@Input()
+	meta = {};
+
+	private selectedModifier: string;
+	private displayedValue: string;
+	private _controlValue: string;
+	
+	private extraClass: string = '';
+
+	propagateChange = (_: any) => {};
+
+	constructor(el: ElementRef) {
+		 // Read the classes applied to the host element, to pass through to the template
+		this.extraClass = el.nativeElement.className;
+	}
+
+	ngOnInit() {
+		// meta input overrides other inputs if provided
+		['modifiers', 'transform'].map(p => this[p] = this.meta[p] || this[p]);
+	}
+	
+	get controlValue(): string {
+		return this._controlValue;;
+	}
+
+	set controlValue(inputValue) {
+		const { modifier, value } = this.transform.inputFn(inputValue);
+		this.selectedModifier = modifier;
+		this.displayedValue = value;
+		this._controlValue = inputValue;
+	}
+	
+	writeValue(value: string): void {
+		this.controlValue = value;
+	}
+
+	registerOnChange(fn: any): void {
+		this.propagateChange = fn;
+	}
+
+	registerOnTouched(fn: any): void {
+	}
+
+	modifierChange(modifier) {
+		this.selectedModifier = modifier;
+		this._controlValue = this.transform.outputFn(this.selectedModifier, this.displayedValue);
+		this.propagateChange(this.controlValue);
+	}
+
+	inputChange() {
+		this._controlValue = this.transform.outputFn(this.selectedModifier, this.displayedValue);
+		this.propagateChange(this.controlValue);
+	}
+
+}

+ 6 - 0
src/app/dynaform/components/layout/dynarow/dynarow.component.html

@@ -0,0 +1,6 @@
+<div class="form-group row">
+	<label class="col-sm-4">{{ label }}</label>
+	<div class="col-sm-8">
+		<ng-template component-host></ng-template>
+	</div>
+</div>

+ 0 - 0
src/app/dynaform/components/layout/dynarow/dynarow.component.scss


+ 25 - 0
src/app/dynaform/components/layout/dynarow/dynarow.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DynarowComponent } from './dynarow.component';
+
+describe('DynarowComponent', () => {
+  let component: DynarowComponent;
+  let fixture: ComponentFixture<DynarowComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ DynarowComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(DynarowComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 82 - 0
src/app/dynaform/components/layout/dynarow/dynarow.component.ts

@@ -0,0 +1,82 @@
+// Form field row, capable of hosting any type of form field input compponent
+
+import { Component, ComponentFactoryResolver, Input, ViewChild, OnInit, forwardRef } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { ComponentHostDirective } from '@directives/component-host.directive';
+
+// Group into barrel: https://basarat.gitbooks.io/typescript/docs/tips/barrel.html
+import { BasicinputComponent } from '@components/basicinput/basicinput.component';
+import { DropdownModifiedInputComponent } from '@components/dropdown-modified-input/dropdown-modified-input.component';
+import { CheckbuttonComponent } from '@components/checkbutton/checkbutton.component';
+import { CheckbuttonGroupComponent } from '@components/checkbutton-group/checkbutton-group.component';
+
+const validComponentsArray = [ BasicinputComponent, DropdownModifiedInputComponent, CheckbuttonComponent, CheckbuttonGroupComponent ];
+const validComponents = validComponentsArray.reduce((acc, component) => Object.assign(acc, { [component.name]: component}), {});
+
+type ValidComponentType = BasicinputComponent | DropdownModifiedInputComponent | CheckbuttonComponent | CheckbuttonGroupComponent;
+
+@Component({
+	selector: 'app-dynarow',
+	templateUrl: './dynarow.component.html',
+	styleUrls: ['./dynarow.component.scss'],
+	providers: [
+		{
+			provide: NG_VALUE_ACCESSOR,
+			useExisting: forwardRef(() => DynarowComponent),
+			multi: true
+		}
+	]
+})
+export class DynarowComponent implements OnInit, ControlValueAccessor {
+
+	@Input()
+	meta; // TODO: Add type - will be a union type of all input types available - probably (?)
+	
+	@ViewChild(ComponentHostDirective)
+	host: ComponentHostDirective;
+
+	label: string;
+	currentValue: string | boolean;
+
+	propagateChange = (_: any) => {};
+
+	constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
+
+	ngOnInit() {
+		this.label = this.meta.label;
+		this.loadComponent();
+	}
+
+	loadComponent() {
+		let type = this.meta.type;
+		console.log('Trying to load', type);
+		const viewContainerRef = this.host.viewContainerRef;
+		if (`${type}Component` in validComponents) {
+			type = `${type}Component`;
+		}
+		if (type in validComponents) {
+			// Create and insert the component
+			const componentFactory = this.componentFactoryResolver.resolveComponentFactory<ValidComponentType>(validComponents[type]);
+			const componentRef = viewContainerRef.createComponent(componentFactory);
+			(<ValidComponentType>componentRef.instance).meta = this.meta; // TODO: Impove on <any> by standardising interface
+
+			console.dir(componentRef.instance);
+
+			// Assign the FormControl, FormArry or FormGroup
+			(<any>componentRef.instance).formControlName = this.meta.name;
+			(<any>componentRef.instance).style = "outline: 5px red solid;";
+		} else {
+			console.error('DYNAROW: Unknown Component Type');
+		}
+	}
+
+	public writeValue(value: any): void {
+		// this.currentValue = (<any>this.componentRef.instance).FormControl.value
+	}
+
+	public registerOnChange(fn: any): void {
+		this.propagateChange = fn;
+	}
+
+	public registerOnTouched(fn: any): void {}
+}

+ 10 - 0
src/app/dynaform/directives/component-host.directive.ts

@@ -0,0 +1,10 @@
+import { Directive, ViewContainerRef } from '@angular/core';
+
+@Directive({
+	selector: '[component-host]'
+})
+export class ComponentHostDirective {
+
+	constructor(public viewContainerRef: ViewContainerRef) { }
+
+}

+ 10 - 0
src/app/dynaform/dynaform.module.ts

@@ -0,0 +1,10 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@NgModule({
+  imports: [
+    CommonModule
+  ],
+  declarations: []
+})
+export class DynaformModule { }

+ 162 - 0
src/app/dynaform/models/index.ts

@@ -0,0 +1,162 @@
+/***********************************************************************************************************************
+ * MetaData models for Form Fields
+ * -------------------------------
+ * Keep in one file for now, but maybe split if this grows too large
+ **********************************************************************************************************************/
+
+import { ValidatorFn, AsyncValidatorFn } from '@angular/forms';
+import { ValueTransformer } from '@components/dropdown-modified-input/dropdown-modified-input.component';
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Types & Interfaces
+
+type Option = {
+	label: string,
+	value: string
+}
+
+interface BasicFieldMetaData {
+	name: string 								// The FormControl name
+	origin?: string								// Location in API-returned model - defaults to name
+	type?: string 								// The component type e.g. BasicInput, Checkbutton, Timepicker, etc
+	label?: string								// The field label - defaults to unCamelCased name if not supplied
+	value?: string								// The field value - defaults to empty string if not supplied
+	placeholder?: string						// Optional placeholder text
+	class?: string | Array<string>				// CSS classes to apply								
+	id?: string									// CSS id to apply
+	isDisabled?: boolean						// Whether field is initially disabled
+	validators?: Array<ValidatorFn>				// Array of validator functions - following Angular FormControl API
+	asyncValidators?: Array<AsyncValidatorFn>	// Array of async validator functions - following Angular FormControl API
+	valFailureMsgs?: StringMap					// Validation failure messages - display appropriate message if validation fails
+}
+
+interface OptionsFieldMetaData extends BasicFieldMetaData {
+	options: Option[]							// Array of Options - for select, radio-button-group and other 'multiple-choice' types
+}
+
+interface DropdownModifiedInputFieldMetaData extends OptionsFieldMetaData {
+	modifiers: string[]
+	transform: ValueTransformer
+}
+
+interface TimePickerFieldMetaData extends BasicFieldMetaData {
+	// To add...
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Form Field MetaData Models
+
+class BasicField
+{
+	name: string;
+	origin?: string;
+	type: string = 'Basicinput';
+	label?: string;
+	value: string = '';
+	placeholder: string = '';
+	class?: string | Array<string>;
+	isDisabled?: boolean;
+	validators: Array<ValidatorFn> = [];
+	asyncValidators: Array<AsyncValidatorFn> = [];
+	valFailureMsgs: StringMap = {};
+
+	constructor(meta: BasicFieldMetaData) {
+		Object.assign(this, meta);
+		if (!this.origin) {
+			// If origin is not supplied it's the same as the name
+			this.origin = this.name;
+		}
+		if (!this.label) {
+			// If label is not supplied set it to the unCamelCased'n'Spaced name
+			// e.g. supervisorCardNumber --> Supervisor Card Number
+			this.label = this.name.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
+		}
+	}
+}
+
+class OptionsField extends BasicField
+{
+	options: Option[];
+	constructor(meta: OptionsFieldMetaData) {
+		super(meta);
+	}
+}
+
+class DropdownModifiedInputField extends OptionsField
+{
+	modifiers: string[]
+	transform: ValueTransformer;
+	constructor(meta: DropdownModifiedInputFieldMetaData) {
+		super(meta);
+	}
+}
+
+class TimePickerField extends BasicField
+{
+	// To add...
+	constructor(meta: TimePickerFieldMetaData) {
+		super(meta);
+	}
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Exports
+
+export { BasicField, OptionsField, DropdownModifiedInputField, TimePickerField };
+
+
+
+
+/***********************************************************************************************************************
+ * Usage
+ * Just ideas here for reference at the moment
+ **********************************************************************************************************************/
+
+/*
+ (1) Approach 1: Loop through model and 'lazily generate' the metadata. Something like...
+
+ overrides = {
+	 field3: {
+		 type: Chechkbutton,
+		 label: 'Friendly Name'
+	 }
+ }
+modeledMeta = {};
+for (field in model) {
+	if (field in overrides) {
+		Object.assign(modeledMeta, FieldFactory(model, overrides[field])); // FieldFactory returns a new field model of the specified type
+	} else {
+		Object.assign(modeledMeta, new BasicField(field)); // Defauls to basic text field
+	}
+ }
+ 
+
+ (2) Apprach 2: Meta-data fully specified (not lazily-generated), so loop through metadata genetating field models
+     metaspec may be hard-coded (for now) or loaded from server and cached, as part of app boostraping
+
+ metaspac = {
+	// Meta-data spec
+ }
+ modeledMeta = {}
+ for (field in metaspec) {
+	Object.assign(modeledMeta, FieldFactory(model, metaspec[field]));
+ }
+
+
+
+ ...
+ Then pass modeledMeta to dynamic form generator / layout, etc
+
+ /*
+
+
+
+
+
+
+
+
+
+
+
+

+ 18 - 17
tsconfig.json

@@ -1,19 +1,20 @@
 {
-  "compileOnSave": false,
-  "compilerOptions": {
-    "outDir": "./dist/out-tsc",
-    "sourceMap": true,
-    "declaration": false,
-    "moduleResolution": "node",
-    "emitDecoratorMetadata": true,
-    "experimentalDecorators": true,
-    "target": "es5",
-    "typeRoots": [
-      "node_modules/@types"
-    ],
-    "lib": [
-      "es2017",
-      "dom"
-    ]
-  }
+	"compileOnSave": false,
+	"compilerOptions": {
+		"outDir": "./dist/out-tsc",
+		"sourceMap": true,
+		"declaration": false,
+		"moduleResolution": "node",
+		"emitDecoratorMetadata": true,
+		"experimentalDecorators": true,
+		"target": "es5",
+		"typeRoots": [
+			"node_modules/@types"
+		],
+		"lib": [
+			"esnext",
+			"dom"
+		],
+		"baseUrl": "./src/",
+	}
 }