Bläddra i källkod

Updating standalone version with changes from lmp

Richard Knight 6 år sedan
förälder
incheckning
aeb0c650a1
28 ändrade filer med 979 tillägg och 138 borttagningar
  1. 1 1
      src/app/dynaform/components/_abstract/custom-input.component.ts
  2. 4 3
      src/app/dynaform/components/custom/checkbutton/checkbutton.component.ts
  3. 8 0
      src/app/dynaform/components/custom/multiline/multiline.component.html
  4. 0 0
      src/app/dynaform/components/custom/multiline/multiline.component.scss
  5. 25 0
      src/app/dynaform/components/custom/multiline/multiline.component.spec.ts
  6. 50 0
      src/app/dynaform/components/custom/multiline/multiline.component.ts
  7. 1 1
      src/app/dynaform/components/group/checkbutton-group/checkbutton-group.component.ts
  8. 14 13
      src/app/dynaform/components/index.ts
  9. 0 1
      src/app/dynaform/components/kendo/timepicker/timepicker.component.html
  10. 1 2
      src/app/dynaform/components/kendo/timepicker/timepicker.component.ts
  11. 1 2
      src/app/dynaform/components/native/select/select.component.html
  12. 11 0
      src/app/dynaform/components/native/select/select.component.ts
  13. 1 0
      src/app/dynaform/components/nocontrol/heading/heading.component.html
  14. 0 0
      src/app/dynaform/components/nocontrol/heading/heading.component.scss
  15. 25 0
      src/app/dynaform/components/nocontrol/heading/heading.component.spec.ts
  16. 21 0
      src/app/dynaform/components/nocontrol/heading/heading.component.ts
  17. 14 4
      src/app/dynaform/dynaform.component.html
  18. 4 4
      src/app/dynaform/dynaform.component.ts
  19. 5 5
      src/app/dynaform/dynaform.module.ts
  20. 1 1
      src/app/dynaform/index.ts
  21. 61 21
      src/app/dynaform/models/field.model.ts
  22. 248 7
      src/app/dynaform/services/_formdata-utils.ts
  23. 63 10
      src/app/dynaform/services/dynaform.service.ts
  24. 128 59
      src/app/dynaform/services/model-mapper.service.ts
  25. 13 3
      src/app/dynaform/testdata/testset.1.ts
  26. 139 0
      src/app/dynaform/testdata/testset.3.ts
  27. 124 0
      src/app/dynaform/testdata/testset.4.ts
  28. 16 1
      src/app/dynaform/utils.ts

+ 1 - 1
src/app/dynaform/components/_abstract/custom-input.component.ts

@@ -5,7 +5,7 @@ export abstract class CustomInputComponent extends NativeInputComponent implemen
 
 
 	propagateChange = (_: any) => {};
 	propagateChange = (_: any) => {};
 
 
-	public writeValue(value: any): void {}
+	public writeValue(value: any): void {};
 
 
 	public registerOnChange(fn: any): void {
 	public registerOnChange(fn: any): void {
 		this.propagateChange = fn;
 		this.propagateChange = fn;

+ 4 - 3
src/app/dynaform/components/custom/checkbutton/checkbutton.component.ts

@@ -16,12 +16,13 @@ import { CustomInputComponent } from './../../_abstract/custom-input.component';
 })
 })
 export class CheckbuttonComponent extends CustomInputComponent implements OnChanges {
 export class CheckbuttonComponent extends CustomInputComponent implements OnChanges {
 
 
-	exposeMetaInTemplate: string[] = ['label', 'value', 'isDisabled'];
+	exposeMetaInTemplate: string[] = ['label', 'value', 'isDisabled', 'checkedValue'];
 
 
+	value?: string | boolean = true;
 	isChecked: boolean;
 	isChecked: boolean;
 	isDisabled = false;
 	isDisabled = false;
-	value?: string | boolean = true;
 	currentValue: string | boolean;
 	currentValue: string | boolean;
+	checkedValue: string | boolean = true;
 
 
 	ngOnChanges() {
 	ngOnChanges() {
 		this.isDisabled = this.meta.isDisabled;
 		this.isDisabled = this.meta.isDisabled;
@@ -32,7 +33,7 @@ export class CheckbuttonComponent extends CustomInputComponent implements OnChan
 		e.preventDefault();
 		e.preventDefault();
 		if (this.isDisabled) { return; }
 		if (this.isDisabled) { return; }
 		this.isChecked = !this.isChecked;
 		this.isChecked = !this.isChecked;
-		this.currentValue = this.isChecked ? this.value : false;
+		this.currentValue = this.isChecked ? this.value || this.checkedValue : false;
 		this.propagateChange(this.currentValue);
 		this.propagateChange(this.currentValue);
 	}
 	}
 
 

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

@@ -0,0 +1,8 @@
+<ng-container *ngFor="let line of linesArr; let i = index;  trackBy: trackByFn">
+	<input type="text"
+		class="form-control form-control-sm"
+		[(ngModel)]="linesArr[i]"
+		[maxlength]="maxLineLength"
+		(blur)="updateValue()"
+	>
+</ng-container>

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


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

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

+ 50 - 0
src/app/dynaform/components/custom/multiline/multiline.component.ts

@@ -0,0 +1,50 @@
+import { Component, forwardRef } from '@angular/core';
+import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { CustomInputComponent } from '@modules/dynaform/components/_abstract/custom-input.component';
+
+@Component({
+	selector: 'app-multiline',
+	templateUrl: './multiline.component.html',
+	styleUrls: ['./multiline.component.scss'],
+	providers: [
+		{
+			provide: NG_VALUE_ACCESSOR,
+			useExisting: forwardRef(() => MultilineComponent),
+			multi: true
+		}
+	]
+})
+export class MultilineComponent extends CustomInputComponent {
+
+	exposeMetaInTemplate: string[] = ['maxLineLength'];
+	linesArr: string[];
+	value: string;
+
+	public writeValue(value: any): void {
+		this.value = value;
+		this.splitIntoLines(value, this.meta.lines || 5);
+	}
+
+	private updateValue(): void {
+		this.value = this.recombineLines(this.linesArr);
+		this.propagateChange(this.value);
+	}
+	
+	private splitIntoLines(value: string, numLines: number): void {
+		this.linesArr = this.control.value.split(/\n/);
+		if (this.linesArr.length < numLines) {
+			const shortfall = numLines - this.linesArr.length;
+			this.linesArr = [...this.linesArr, ...Array(shortfall).fill('')];
+		} else {
+			this.linesArr = this.linesArr.slice(0, numLines);
+		}
+	}
+	
+	private recombineLines = (linesArr: string[]): string => {
+		return linesArr.join("\n");
+	}
+
+	private trackByFn(index: any, item: any) {
+		return index;
+	}
+}

+ 1 - 1
src/app/dynaform/components/group/checkbutton-group/checkbutton-group.component.ts

@@ -39,7 +39,7 @@ export class CheckbuttonGroupComponent extends GroupInputComponent implements On
 					// NOTE: We reassign the input (array member) to trigger change detection (otherwise it doesn't run)
 					// NOTE: We reassign the input (array member) to trigger change detection (otherwise it doesn't run)
 					// See https://juristr.com/blog/2016/04/angular2-change-detection/
 					// See https://juristr.com/blog/2016/04/angular2-change-detection/
 					this.childMetaArray[i] = {
 					this.childMetaArray[i] = {
-						...this.childMetaArray[1],
+						...this.childMetaArray[i],
 						isDisabled: !val
 						isDisabled: !val
 					};
 					};
 				}
 				}

+ 14 - 13
src/app/dynaform/components/index.ts

@@ -2,16 +2,17 @@
 // See https://basarat.gitbooks.io/typescript/docs/tips/barrel.html
 // See https://basarat.gitbooks.io/typescript/docs/tips/barrel.html
 
 
 // export { DynaformComponent } from './../dynaform.component';
 // export { DynaformComponent } from './../dynaform.component';
-export { TextComponent } from './native/text/text.component';
-export { TextareaComponent } from './native/textarea/textarea.component';
-export { PasswordComponent } from './native/password/password.component';
-export { SelectComponent } from './native/select/select.component';
-export { RadioComponent } from './native/radio/radio.component';
-export { HiddenComponent } from './native/hidden/hidden.component';
-export { DropdownModifiedInputComponent } from './custom/dropdown-modified-input/dropdown-modified-input.component';
-export { CheckbuttonComponent } from './custom/checkbutton/checkbutton.component';
-export { CheckbuttonGroupComponent } from './group/checkbutton-group/checkbutton-group.component';
-export { TimepickerComponent } from './kendo/timepicker/timepicker.component';
-export { DatepickerComponent } from './kendo/datepicker/datepicker.component';
-export { ButtonGroupComponent } from './nocontrol/button-group/button-group.component';;
-
+export { TextComponent } from '@modules/dynaform/components/native/text/text.component';
+export { TextareaComponent } from '@modules/dynaform/components/native/textarea/textarea.component';
+export { PasswordComponent } from '@modules/dynaform/components/native/password/password.component';
+export { SelectComponent } from '@modules/dynaform/components/native/select/select.component';
+export { RadioComponent } from '@modules/dynaform/components/native/radio/radio.component';
+export { HiddenComponent } from '@modules/dynaform/components/native/hidden/hidden.component';
+export { CheckbuttonComponent } from '@modules/dynaform/components/custom/checkbutton/checkbutton.component';
+export { DropdownModifiedInputComponent } from '@modules/dynaform/components/custom/dropdown-modified-input/dropdown-modified-input.component';
+export { MultilineComponent } from '@modules/dynaform/components/custom/multiline/multiline.component';
+export { CheckbuttonGroupComponent } from '@modules/dynaform/components/group/checkbutton-group/checkbutton-group.component';
+export { TimepickerComponent } from '@modules/dynaform/components/kendo/timepicker/timepicker.component';
+export { DatepickerComponent } from '@modules/dynaform/components/kendo/datepicker/datepicker.component';
+export { ButtonGroupComponent } from '@modules/dynaform/components/nocontrol/button-group/button-group.component';
+export { HeadingComponent } from '@modules/dynaform/components/nocontrol/heading/heading.component';

+ 0 - 1
src/app/dynaform/components/kendo/timepicker/timepicker.component.html

@@ -2,7 +2,6 @@
 	[formControl]="control"
 	[formControl]="control"
 	[format]="format"
 	[format]="format"
 	[steps]="steps"
 	[steps]="steps"
-	[value]="value"
 	[placeholder]="placeholder"
 	[placeholder]="placeholder"
 	>
 	>
 </kendo-timepicker>
 </kendo-timepicker>

+ 1 - 2
src/app/dynaform/components/kendo/timepicker/timepicker.component.ts

@@ -8,6 +8,5 @@ import { NativeInputComponent } from '../../_abstract/native-input.component';
 })
 })
 export class TimepickerComponent extends NativeInputComponent {
 export class TimepickerComponent extends NativeInputComponent {
 
 
-	exposeMetaInTemplate: string[] = ['format', 'steps', 'value', 'placeholder'];
-
+	exposeMetaInTemplate: string[] = ['format', 'steps', 'placeholder'];
 }
 }

+ 1 - 2
src/app/dynaform/components/native/select/select.component.html

@@ -8,8 +8,7 @@
 			<option *ngFor="let opt of options" [value]="opt.value">{{ opt.label }}</option>
 			<option *ngFor="let opt of options" [value]="opt.value">{{ opt.label }}</option>
 		</select>
 		</select>
 		<div class="input-group-append">
 		<div class="input-group-append">
-			<button class="btn btn-outline-primary" type="button"
-				[routerLink]="[ link.route, field.value ]">{{ link.label || 'Details' }}</button>
+			<button class="btn btn-outline-primary" type="button" (click)="navigate(field)">{{ link.label || 'Details' }}</button>
 		</div>
 		</div>
 	</div>
 	</div>
 </ng-template>
 </ng-template>

+ 11 - 0
src/app/dynaform/components/native/select/select.component.ts

@@ -1,5 +1,6 @@
 import { Component } from '@angular/core';
 import { Component } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
+import { Router, ActivatedRoute } from '@angular/router';
 
 
 @Component({
 @Component({
 	selector: 'app-select',
 	selector: 'app-select',
@@ -10,4 +11,14 @@ export class SelectComponent extends NativeInputComponent {
 
 
 	exposeMetaInTemplate: string[] = ['options', 'link'];
 	exposeMetaInTemplate: string[] = ['options', 'link'];
 
 
+	constructor(private router: Router, private route: ActivatedRoute) {
+		super();
+	}
+	
+	navigate(field: HTMLSelectElement) {
+		const base = Array.isArray(this.meta.link.route) ? this.meta.link.route : [this.meta.link.route];
+		const destination = [...base, field[field.selectedIndex].value];
+		this.router.navigate(destination, { relativeTo: this.route });
+	}
+
 }
 }

+ 1 - 0
src/app/dynaform/components/nocontrol/heading/heading.component.html

@@ -0,0 +1 @@
+<h3 class="col-sm-12 h-dyna" [ngClass]="'h-dyna-' + (level)">{{ text }}</h3>

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


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

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

+ 21 - 0
src/app/dynaform/components/nocontrol/heading/heading.component.ts

@@ -0,0 +1,21 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+@Component({
+	selector: 'app-heading',
+	templateUrl: './heading.component.html',
+	styleUrls: ['./heading.component.scss']
+})
+export class HeadingComponent implements OnInit {
+
+	@Input()
+	meta: StringMap;
+
+	text: string;
+	level: number;
+
+	ngOnInit() {
+		this.text = this.meta.text;
+		this.level = this.meta.level || 2;
+	}
+
+}

+ 14 - 4
src/app/dynaform/dynaform.component.html

@@ -8,9 +8,11 @@
 <ng-template #default let-control="control" let-meta="meta">
 <ng-template #default let-control="control" let-meta="meta">
 	<ng-container *ngIf="meta.type !== 'Hidden'">
 	<ng-container *ngIf="meta.type !== 'Hidden'">
 
 
-		<div class="form-group row" *ngIf="meta.type !== 'Container'; else recursiveDynaform" [ngClass]="getRowClass(control, meta.type)">
+		<ng-container *ngIf="!meta.noLabel; else fullWidth">
+
+			<div class="form-group row" *ngIf="meta.type !== 'Container'; else recursiveDynaform" [ngClass]="getRowClass(control, meta.type)">
 				<label class="col-sm-4" [for]="meta.name" title="">
 				<label class="col-sm-4" [for]="meta.name" title="">
-					{{ meta.label }}
+					{{ meta.rowLabel !== mull ? meta.rowLabel : meta.label }}
 					<span *ngIf="control && control.touched && control.invalid" class="fas fa-exclamation-triangle"
 					<span *ngIf="control && control.touched && control.invalid" class="fas fa-exclamation-triangle"
 						[ngbTooltip]="getValidationFailureMessage(control, meta)"
 						[ngbTooltip]="getValidationFailureMessage(control, meta)"
 					></span>
 					></span>
@@ -21,11 +23,19 @@
 			</div>
 			</div>
 			
 			
 			<ng-template #recursiveDynaform>
 			<ng-template #recursiveDynaform>
-				<div class="row pt-3">
-					<h3 class="col-sm-12" [ngClass]="'h-dyna-' + (path.length + 2)">{{ meta.label }}</h3>
+				<div *ngIf="meta.label" class="row">
+					<h3 class="col-sm-12 h-dyna" [ngClass]="'h-dyna-' + (path.length + 2)">{{ meta.label }}</h3>
 				</div>
 				</div>
 				<app-dynaform [formGroup]="control" [meta]="meta.meta" [template]="template" (call)="handleCallback($event)"></app-dynaform>
 				<app-dynaform [formGroup]="control" [meta]="meta.meta" [template]="template" (call)="handleCallback($event)"></app-dynaform>
 			</ng-template>
 			</ng-template>
+
+		</ng-container>
+
+		<ng-template #fullWidth>
+			<div class="row" [ngClass]="getRowClass(control, meta.type)">
+				<ng-container dynafield [control]="control" [meta]="meta" (call)="handleCallback($event)"></ng-container>
+			</div>
+		</ng-template>
 		
 		
 	</ng-container>
 	</ng-container>
 </ng-template>
 </ng-template>

+ 4 - 4
src/app/dynaform/dynaform.component.ts

@@ -48,7 +48,7 @@ export class DynaformComponent implements OnInit {
 	template?: TemplateRef<any>;
 	template?: TemplateRef<any>;
 
 
 	@Input()
 	@Input()
-	debug = true;
+	debug = false;
 
 
 	@Output()
 	@Output()
 	call: EventEmitter<string> = new EventEmitter<string>();
 	call: EventEmitter<string> = new EventEmitter<string>();
@@ -69,7 +69,7 @@ export class DynaformComponent implements OnInit {
 	ngOnInit() {
 	ngOnInit() {
 		// Get the formGroup from the formGroupName if necessary
 		// Get the formGroup from the formGroupName if necessary
 		if (!this.formGroup && this.formGroupName) {
 		if (!this.formGroup && this.formGroupName) {
-			this.formGroup = <FormGroup>this.cc.control; // Get theFormGroup from the inhected ControlContainer
+			this.formGroup = <FormGroup>this.cc.control; // Get theFormGroup from the injected ControlContainer
 		}
 		}
 		if (!this.formGroup) {
 		if (!this.formGroup) {
 			throw new Error('Dynaform Component initialised without [formGroup] or formGroupName');
 			throw new Error('Dynaform Component initialised without [formGroup] or formGroupName');
@@ -122,7 +122,7 @@ export class DynaformComponent implements OnInit {
 		}
 		}
 		return path;
 		return path;
 	}
 	}
-	
+
 	getTemplateContext(controlName: string): DynarowContext {
 	getTemplateContext(controlName: string): DynarowContext {
 		return {
 		return {
 			control: this.formGroup.get(controlName),
 			control: this.formGroup.get(controlName),
@@ -146,7 +146,7 @@ export class DynaformComponent implements OnInit {
 	getValidationErrors() {
 	getValidationErrors() {
 		if (!this.formGroup.valid) {
 		if (!this.formGroup.valid) {
 			const errorsFlat = SuperForm.getAllErrorsFlat(this.formGroup);
 			const errorsFlat = SuperForm.getAllErrorsFlat(this.formGroup);
-			return errorsFlat;		
+			return errorsFlat;
 		}
 		}
 		return 'No Errors';
 		return 'No Errors';
 	}
 	}

+ 5 - 5
src/app/dynaform/dynaform.module.ts

@@ -5,12 +5,12 @@ import { RouterModule } from '@angular/router';
 
 
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 
 
-import { DynaformComponent } from './dynaform.component';
-import { DynafieldDirective } from './directives/dynafield.directive';
-import { DynaformService } from './services/dynaform.service';
-import { ModelMapperService } from './services/model-mapper.service';
+import { DynaformComponent } from '@modules/dynaform/dynaform.component';
+import { DynafieldDirective } from '@modules/dynaform/directives/dynafield.directive';
+import { DynaformService } from '@modules/dynaform/services/dynaform.service';
+import { ModelMapperService } from '@modules/dynaform/services/model-mapper.service';
 
 
-import * as formFieldComponents from './components';
+import * as formFieldComponents from '@modules/dynaform/components';
 const ffcArr = Object.values(formFieldComponents); // Array of all the Form Field Components
 const ffcArr = Object.values(formFieldComponents); // Array of all the Form Field Components
 
 
 import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
 import { DateInputsModule } from '@progress/kendo-angular-dateinputs';

+ 1 - 1
src/app/dynaform/index.ts

@@ -2,5 +2,5 @@ export { DynaformComponent } from './dynaform.component';
 // export { DynafieldDirective } from './directives/dynafield.directive';
 // export { DynafieldDirective } from './directives/dynafield.directive';
 export { DynaformService } from './services/dynaform.service';
 export { DynaformService } from './services/dynaform.service';
 export { ModelMapperService } from './services/model-mapper.service';
 export { ModelMapperService } from './services/model-mapper.service';
-export { standardModifiers, standardTransformer, arrayToMeta } from './utils';
+export { standardModifiers, standardTransformer, toArrTag, arrayPad, arrayToMeta, excludeFields } from './utils';
 
 

+ 61 - 21
src/app/dynaform/models/field.model.ts

@@ -6,12 +6,12 @@
 
 
 import { TemplateRef } from '@angular/core'; 
 import { TemplateRef } from '@angular/core'; 
 import { ValidatorFn, AsyncValidatorFn } from '@angular/forms';
 import { ValidatorFn, AsyncValidatorFn } from '@angular/forms';
-import { ValueTransformer } from './../interfaces';
-import { standardModifiers, standardTransformer } from './../utils';
+import { ValueTransformer } from '@modules/dynaform/interfaces';
+import { standardModifiers, standardTransformer } from '@modules/dynaform/utils';
 
 
 interface SimpleFieldMetaData {
 interface SimpleFieldMetaData {
 	name: string; 								// The FormControl name
 	name: string; 								// The FormControl name
-	origin?: string;							// Location in API-returned model - defaults to name
+	source?: string;							// Location in API-returned model - defaults to name
 	type?: string; 								// The component type e.g. BasicInput, Checkbutton, Timepicker, etc
 	type?: string; 								// The component type e.g. BasicInput, Checkbutton, Timepicker, etc
 	label?: string;								// The field label - defaults to unCamelCased name if not supplied
 	label?: string;								// The field label - defaults to unCamelCased name if not supplied
 	value?: any;								// The field value - defaults to empty string if not supplied
 	value?: any;								// The field value - defaults to empty string if not supplied
@@ -19,6 +19,8 @@ interface SimpleFieldMetaData {
 	placeholder?: string;						// Optional placeholder text
 	placeholder?: string;						// Optional placeholder text
 	class?: string | Array<string>;				// CSS classes to apply
 	class?: string | Array<string>;				// CSS classes to apply
 	id?: string;								// CSS id to apply
 	id?: string;								// CSS id to apply
+	before?: string;							// Ordering instruction - move before <name of another key in group>
+	after?: string;								// Ordering instruction - move after <name of another key in group>
 	isDisabled?: boolean;						// Whether field is initially disabled
 	isDisabled?: boolean;						// Whether field is initially disabled
 	validators?: Array<ValidatorFn>;			// Array of validator functions - following Angular FormControl API
 	validators?: Array<ValidatorFn>;			// Array of validator functions - following Angular FormControl API
 	asyncValidators?: Array<AsyncValidatorFn>;	// Array of async validator functions - following Angular FormControl API
 	asyncValidators?: Array<AsyncValidatorFn>;	// Array of async validator functions - following Angular FormControl API
@@ -47,8 +49,7 @@ interface DropdownModifiedInputFieldMetaData extends SimpleFieldMetaData {
 }
 }
 
 
 interface TimePickerFieldMetaData extends SimpleFieldMetaData {
 interface TimePickerFieldMetaData extends SimpleFieldMetaData {
-	// TODO: Tighhten up on types
-	value: Date;
+	value: Date | string;
 	format: string;
 	format: string;
 	steps: StringMap;
 	steps: StringMap;
 }
 }
@@ -56,7 +57,10 @@ interface TimePickerFieldMetaData extends SimpleFieldMetaData {
 // Utility to unCamelCase
 // Utility to unCamelCase
 const unCamelCase = (str: string): string => str.replace(/([A-Z])/g, ' $1')
 const unCamelCase = (str: string): string => str.replace(/([A-Z])/g, ' $1')
 												.replace(/(\w)(\d)/g, '$1 $2')
 												.replace(/(\w)(\d)/g, '$1 $2')
-												.replace(/^./, s => s.toUpperCase());
+												.replace(/^./, s => s.toUpperCase())
+												.replace(/([A-Z]) /g, '$1')
+												// .replace(/ ([A-Z][a-z])/g, s => s.toLowerCase()) lowercase all but first word and acronyms
+												;
 
 
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // Form Field MetaData Models
 // Form Field MetaData Models
@@ -66,7 +70,7 @@ const unCamelCase = (str: string): string => str.replace(/([A-Z])/g, ' $1')
 abstract class SimpleField {
 abstract class SimpleField {
 	type: string;
 	type: string;
 	name: string;
 	name: string;
-	origin?: string;
+	source?: string;
 	label?: string;
 	label?: string;
 	value;
 	value;
 	default = '';
 	default = '';
@@ -79,12 +83,15 @@ abstract class SimpleField {
 	valFailureMsgs: StringMap = {};
 	valFailureMsgs: StringMap = {};
 
 
 	constructor(meta: SimpleFieldMetaData) {
 	constructor(meta: SimpleFieldMetaData) {
+		if (meta.type === 'Multiline') {
+			console.log(meta);
+		}
 		Object.assign(this, meta);
 		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.source) {
+			// If source is not supplied it's the same as the name
+			this.source = this.name;
 		}
 		}
-		if (!this.label) {
+		if (typeof this.label === 'undefined') {
 			// If label is not supplied set it to the unCamelCased'n'Spaced name
 			// If label is not supplied set it to the unCamelCased'n'Spaced name
 			// e.g. supervisorCardNumber --> Supervisor Card Number
 			// e.g. supervisorCardNumber --> Supervisor Card Number
 			this.label = unCamelCase(this.name);
 			this.label = unCamelCase(this.name);
@@ -162,6 +169,9 @@ class HiddenField extends SimpleField {
 class CheckbuttonField extends SimpleField {
 class CheckbuttonField extends SimpleField {
 	type = 'Checkbutton';
 	type = 'Checkbutton';
 	default: any = false;
 	default: any = false;
+	isChecked: boolean;
+	checkedValue: boolean | string = true;
+	rowLabel: null;
 }
 }
 
 
 class DropdownModifiedInputField extends SimpleField {
 class DropdownModifiedInputField extends SimpleField {
@@ -173,6 +183,12 @@ class DropdownModifiedInputField extends SimpleField {
 	}
 	}
 }
 }
 
 
+class MultilineField extends SimpleField {
+	type = 'Multiline';
+	lines: number;
+	maxLineLength: number;
+}
+
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls)
 // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls)
 
 
@@ -181,12 +197,12 @@ class CheckbuttonGroup {
 	name: string;
 	name: string;
 	label?: string;
 	label?: string;
 	groupName: string;
 	groupName: string;
-	firstEnablesRest?;
-	showAllOrNone?;
+	firstEnablesRest?: boolean;
+	showAllOrNone?: boolean;
 	meta: CheckbuttonField[] | { [key: string]: CheckbuttonField };
 	meta: CheckbuttonField[] | { [key: string]: CheckbuttonField };
 	constructor(groupmeta: any) {
 	constructor(groupmeta: any) {
 		Object.assign(this, groupmeta);
 		Object.assign(this, groupmeta);
-		if (!this.label) {
+		if (typeof this.label === 'undefined') {
 			// If label is not supplied set it to the unCamelCased'n'Spaced name
 			// If label is not supplied set it to the unCamelCased'n'Spaced name
 			// e.g. supervisorCardNumber --> Supervisor Card Number
 			// e.g. supervisorCardNumber --> Supervisor Card Number
 			this.label = unCamelCase(this.name);
 			this.label = unCamelCase(this.name);
@@ -209,9 +225,20 @@ class CheckbuttonGroup {
 
 
 class TimepickerField<TimePickerFieldMetaData> extends SimpleField {
 class TimepickerField<TimePickerFieldMetaData> extends SimpleField {
 	type = 'Timepicker';
 	type = 'Timepicker';
-	value: Date = new Date();
+	value: Date | string;
 	format = 'hh:mm a';
 	format = 'hh:mm a';
 	steps = { hour: 1, minute: 15 };
 	steps = { hour: 1, minute: 15 };
+	constructor(meta) {
+		super(meta);
+		if (typeof this.value === 'string') {
+			const [hh, mm, ss] = this.value.split(':');
+			this.value = new Date(2000, 6, 1, +hh | 0, +mm | 0, +ss | 0);
+		}
+		if (!(this.value instanceof Date)) {
+			this.value = new Date();
+		}
+	}
+
 }
 }
 
 
 class DatepickerField extends SimpleField {
 class DatepickerField extends SimpleField {
@@ -228,10 +255,9 @@ class Container {
 	label = '';
 	label = '';
 	template?: TemplateRef<any>;
 	template?: TemplateRef<any>;
 	meta: StringMap; // TODO: Tighten up on type with union type
 	meta: StringMap; // TODO: Tighten up on type with union type
-	constructor(meta: StringMap) {
-		console.log(meta);
-		meta.meta ? Object.assign(this, meta) : this.meta = meta;
-		if (!this.label) {
+	constructor(containerMeta: StringMap) {
+		Object.assign(this, containerMeta);
+		if (typeof this.label === 'undefined') {
 			this.label = unCamelCase(this.name);
 			this.label = unCamelCase(this.name);
 		}
 		}
 	}
 	}
@@ -268,6 +294,20 @@ class ButtonGroup {
 	}
 	}
 }
 }
 
 
+// ---------------------------------------------------------------------------------------------------------------------
+// Heading
+
+class Heading {
+	type = 'Heading';
+	text: string = 'Missing Heading Text';
+	level: number = 3;
+	readonly noFormControls = true; // Indicates this has no FormControls associated with it
+	readonly noLabel = true; // Indicates this has no label, so don't create normal form row
+	constructor(meta) {
+		Object.assign(this, meta);
+	}
+}
+
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
@@ -276,9 +316,9 @@ class ButtonGroup {
 export {
 export {
 	SimpleField,
 	SimpleField,
 	TextField, TextareaField, PasswordField, SelectField, RadioField, HiddenField,
 	TextField, TextareaField, PasswordField, SelectField, RadioField, HiddenField,
-	CheckbuttonField, DropdownModifiedInputField,
+	CheckbuttonField, DropdownModifiedInputField, MultilineField,
 	CheckbuttonGroup,
 	CheckbuttonGroup,
 	TimepickerField, DatepickerField,
 	TimepickerField, DatepickerField,
-	Container, ButtonGroup
+	Container, ButtonGroup, Heading
 };
 };
 
 

+ 248 - 7
src/app/dynaform/services/_formdata-utils.ts

@@ -6,19 +6,22 @@
  * autoMeta(model) - generate basic metadata from a raw or mapped model, recursively if necessary
  * autoMeta(model) - generate basic metadata from a raw or mapped model, recursively if necessary
  * combineExtraMeta(metadata, extraMeta) - combine extra metadata into metatdata, lazyly and recursively
  * combineExtraMeta(metadata, extraMeta) - combine extra metadata into metatdata, lazyly and recursively
  * combineModelWithMeta(model, extraMeta) - automatically generate metadata from model then combine extra metadata
  * combineModelWithMeta(model, extraMeta) - automatically generate metadata from model then combine extra metadata
+ * execMetaReorderingInstructions(meta) - reorder metadata id ant 'before' or 'after' instructions are found
  * buildFieldSpecificMeta(metadata) - use field-type-specific metadata models to expand metadata
  * buildFieldSpecificMeta(metadata) - use field-type-specific metadata models to expand metadata
+ * extractFieldMappings(metadata) - extrtact mappings from metadata 'source' attributes
  * buildFormGroupFunctionFactory(fb) - return a function to buildFormGroups using the supplied FormBuilder singleton
  * buildFormGroupFunctionFactory(fb) - return a function to buildFormGroups using the supplied FormBuilder singleton
+ * generateNewModel(originalModel, updates) - returns an updated copy of the model
  *
  *
  * Variable names
  * Variable names
  * --------------
  * --------------
  * metaF   = metadata for Field
  * metaF   = metadata for Field
  * metaG   = metadata for Group (possibly nested)
  * metaG   = metadata for Group (possibly nested)
- * metaFoG = metadata for Field Or Group
+ * metaFoG = metadata for Field or Group
  *
  *
  */
  */
 
 
 import { FormBuilder, FormGroup, FormArray, FormControl, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
 import { FormBuilder, FormGroup, FormArray, FormControl, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
-import { reduce } from 'lodash/fp';
+import { reduce, cloneDeep } from 'lodash/fp';
 import * as fmdModels from '../models/field.model';
 import * as fmdModels from '../models/field.model';
 
 
 // REMINDER: Import this directly from @angular/forms when we upgrade to Angular 6
 // REMINDER: Import this directly from @angular/forms when we upgrade to Angular 6
@@ -49,7 +52,8 @@ const keyValPairToMetaRecursive = ( [key, val] ) => {
 const arrayMemberAutoName = val => isScalar(val) ? val : val.__typename || val.constructor.name;
 const arrayMemberAutoName = val => isScalar(val) ? val : val.__typename || val.constructor.name;
 const arrayToMeta = array => array.map(val => ({ name: arrayMemberAutoName(val), 'value' : val }));
 const arrayToMeta = array => array.map(val => ({ name: arrayMemberAutoName(val), 'value' : val }));
 
 
-const autoMeta = model =>  Object.entries(model)
+const autoMeta = model => Object.entries(model)
+	.filter(kvPair => kvPair[0] !== '__')
 	.map(keyValPairToMetaRecursive)
 	.map(keyValPairToMetaRecursive)
 	.reduce((res, [key, val]) => addProp(res, key, val), {});
 	.reduce((res, [key, val]) => addProp(res, key, val), {});
 
 
@@ -64,7 +68,7 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false) => {
 	Object.entries(extraMeta).forEach(([key, val]) => {
 	Object.entries(extraMeta).forEach(([key, val]) => {
 		if (typeof metaG[key] === 'object' || createFromExtra) {
 		if (typeof metaG[key] === 'object' || createFromExtra) {
 			combinedMeta[key] = metaG[key] && (<any>val).meta ?
 			combinedMeta[key] = metaG[key] && (<any>val).meta ?
-				combineMetaForField(metaG[key], { meta: combineExtraMeta(metaG[key].meta, (<any>val).meta, createFromExtra) }) :
+				combineMetaForField(metaG[key], { ...val, meta: combineExtraMeta(metaG[key].meta, (<any>val).meta, createFromExtra) }) :
 				combineMetaForField(metaG[key] || {}, val);
 				combineMetaForField(metaG[key] || {}, val);
 		}
 		}
 	});
 	});
@@ -81,7 +85,7 @@ const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => comb
 
 
 const buildFieldClassName = (t = 'text') => {
 const buildFieldClassName = (t = 'text') => {
 	const start = t[0].toUpperCase() + t.slice(1);
 	const start = t[0].toUpperCase() + t.slice(1);
-	if (start === 'Container' || t.slice(-5) === 'Group') {
+	if (start === 'Container' || start === 'Heading' || t.slice(-5) === 'Group') {
 		return start;
 		return start;
 	}
 	}
 	return start + 'Field';
 	return start + 'Field';
@@ -111,6 +115,131 @@ const _buildFieldSpecificMeta = metaG => reduce(buildModeledFieldGroupReducerIte
 const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
 const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
 
 
 
 
+// ---------------------------------------------------------------------------------------------------------------------
+// Generate mapping from source attributes
+// (used to grab data from model when using METAFIRST form generation)
+// ---------------------------------------------------------------------------------------------------------------------
+
+// Each container CAN have a datasource instead of the original model
+// SO ... If container source is a functional mapping, store the mapping to get datasource object later
+
+const isAbsPath = path => typeof path === 'string' && path[0] === '/';
+const isRootPath = path => path === '/';
+const processPath = (parentPath, path) => isAbsPath(path) ? path : `${parentPath}.${path}`;
+
+const prependParentPathRecursive = (parentPath: string, obj: StringMap) => {
+	return Object.entries(obj)
+		.map( ([key, mapping] ) => {
+			let mappingRes;
+			switch(typeof mapping) {
+				case 'string':
+					mappingRes = processPath(parentPath, mapping);
+					break;
+				case 'object':
+					if (Array.isArray(mapping)) {
+						// A functional mapping of the form [fn, fn] or [source, fn, fn]
+						if (typeof mapping[0] === 'string') {
+							const source = processPath(parentPath, mapping[0]);
+							mappingRes = [source, ...mapping.slice(1)];
+						} else {
+							const source = processPath(parentPath, mapping);
+							mappingRes = [source, ...mapping];
+						}
+					} else {
+						mappingRes = prependParentPathRecursive(parentPath, mapping)
+					}
+					break;
+			}
+			return [key, mappingRes];
+		})
+		.reduce((acc, [key, val]) => addProp(acc, key, val), {});
+}
+
+const _extractFieldMapping = ( [key, metaFoG] ) => {
+	let source;
+	if (isGroup(metaFoG)) {
+		if (Array.isArray(metaFoG.source)) {
+			source = extractFieldMappings(metaFoG.meta);
+			source.__ = metaFoG.source; // Store the functional mapping (including function executed later to provide container's data)
+		} else {
+			source = extractFieldMappings(metaFoG.meta, metaFoG.source || key);
+		}
+	} else {
+		source = metaFoG.source || key;
+	}
+	return [key, source];
+};
+
+const extractFieldMappings = (metaG, parentPath = '') => Object.entries(metaG)
+	.map(_extractFieldMapping)
+	.reduce((res, [key, mapping]) => {
+		
+		// Work out the path prefix
+		let prefix;
+		if (parentPath) {
+			if (isRootPath(parentPath) || isAbsPath(metaG[key].source) || Array.isArray(parentPath)) {
+				 // If the parentPath is the root of the data structure, or the source is an absolute path or functional datasource,
+				 // then there's no path prefix
+				prefix = ''
+			} else {
+				// Otherwise create a prefix from the parentPath
+				prefix = parentPath ? (parentPath[0] === '/' ? parentPath.slice(1) : parentPath) : '';
+			}
+		}
+
+		// Work out the mapping result
+		let	mappingRes;
+		if (typeof mapping === 'string') {
+
+			// DIRECT MAPPING
+			if (mapping[0] === '/') {
+				mappingRes = mapping.slice(1);
+			} else if (prefix) {
+				mappingRes = `${prefix}.${mapping}`;
+			} else {
+				mappingRes = mapping;
+			}
+
+		} else if (Array.isArray(mapping)) {
+
+			// FUNCTIONAL MAPPING
+			// of the form [fn, fn] or [source, fn, fn]
+			if (prefix) {
+				if (typeof mapping[0] === 'function') {
+					// Mapping of form [fn, fn] with existing parent path
+					mappingRes = [`${prefix}.${key}`, ...mapping];
+				} else if (typeof mapping[0] === 'string') {
+					// Mapping of form [source, fn, fn] with existing parent path
+					const source = mapping[0][0] === '/' ? mapping[0].slice(1) : `${prefix}.${mapping[0]}`;
+					mappingRes = [source, ...mapping.slice(1)];
+				}
+			} else {
+				if (typeof mapping[0] === 'function') {
+					// Mapping of form [fn, fn] with NO parent path
+					mappingRes = [key, ...mapping];
+				} else if (typeof mapping[0] === 'string') {
+					// Mapping of form [source, fn, fn] with NO parent path
+					const source = mapping[0][0] === '/' ? mapping[0].slice(1) : mapping[0];
+					mappingRes = [source, ...mapping.slice(1)];
+				}
+			}
+
+		} else if (typeof mapping === 'object' && prefix && !mapping.__) {
+
+			// CONTAINER with parentPath, and WITHOUT its own functional datasource stored in __
+			// For every contained value recursively prepend the parentPath to give an absolute path
+			mappingRes = prependParentPathRecursive(prefix, mapping);
+
+		} else {
+			
+			mappingRes = mapping;
+
+		}
+		
+		return addProp(res, key, mappingRes);
+	}, {});
+
+
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // Build Form Group Function Factory
 // Build Form Group Function Factory
 // returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
 // returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
@@ -125,7 +254,7 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 	const buildValidators = (metaF): AbstractControlOptions => ({
 	const buildValidators = (metaF): AbstractControlOptions => ({
 		validators: metaF.validators,
 		validators: metaF.validators,
 		asyncValidators: metaF.asyncValidators,
 		asyncValidators: metaF.asyncValidators,
-		updateOn: 'change' // blur not working for custom components - maybe use change for custom and blur for text
+		updateOn: metaF.type === 'text' || metaF.type === 'textarea' ? 'blur' : 'change' // blur not working for custom components, so use change for custom and blur for text
 	});
 	});
 	const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
 	const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
 
 
@@ -152,6 +281,114 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 };
 };
 
 
 
 
+// ---------------------------------------------------------------------------------------------------------------------
+// Reordering ( support for before / after instructions in metadata )
+// ---------------------------------------------------------------------------------------------------------------------
+
+// Object slice function
+const slice = (obj, start, end = null) => Object.entries(obj)
+	.slice(start, end !== null ? end : Object.keys(obj).length)
+	.reduce((res, [key, val]) => addProp(res, key, val), {});
+
+const objConcat = (obj, pos, key, val = null) => {
+	const existsAlready = Object.keys(obj).indexOf(key);
+	if (existsAlready) {
+		val = obj[key];
+	}
+	const start  = slice(obj, 0, pos);
+	const finish = slice(obj, pos);
+	delete start[key];
+	delete finish[key];
+	return { ...start, [key]: val, ...finish };
+}
+
+const insertBefore = (obj, beforeKey, key, val = null) => {
+	const targetPosition = Object.keys(obj).indexOf(beforeKey);
+	return objConcat(obj, targetPosition, key, val);
+}
+
+const insertAfter = (obj, afterKey, key, val = null) => {
+	const targetPosition = Object.keys(obj).indexOf(afterKey) + 1;
+	return objConcat(obj, targetPosition, key, val);
+}
+
+// Process reordeing instructions recursively
+const _execMetaReorderingInstructions = (metaG: StringMap) => {
+	let reorderedGroup = { ...metaG };
+	Object.entries(metaG).forEach(([key, metaFoG]) => {
+		if (metaFoG.before) {
+			reorderedGroup = insertBefore(reorderedGroup, metaFoG.before, key);
+		} else if (metaFoG.after) {
+			reorderedGroup = insertAfter(reorderedGroup, metaFoG.after, key);
+		}
+		if (isContainer(metaFoG)) {
+			reorderedGroup[key].meta = execMetaReorderingInstructions(metaFoG.meta);
+		}
+	});
+	return reorderedGroup;
+}
+
+const execMetaReorderingInstructions = (metaG: StringMap) => {
+	return _execMetaReorderingInstructions(cloneDeep(metaG));
+}
+
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Generate new model, without mutating original
+// (used to produce an updated copy of a model when form values are changed - will not create new keys)
+// ---------------------------------------------------------------------------------------------------------------------
+
+const generateNewModel = (originalModel, updates) => {
+	return updateObject(originalModel, updates);
+}
+
+const updateObject = (obj, updates, createAdditionalKeys = false) => {
+	// THIS DOES NOT MUTATE obj, instead returning a new object
+	const shallowClone = { ...obj };
+	Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
+	return shallowClone;
+};
+
+const safeSet = (obj, key, val, createAdditionalKeys = false) => {
+	// THIS MUTATES obj - consider the wisdom of this
+	if (!createAdditionalKeys && !obj.hasOwnProperty(key)) {
+		return;
+	}
+	let currentVal = obj[key];
+	if (val === currentVal) {
+		return;
+	}
+	if (nullOrScaler(currentVal)) {
+		console.log('safeSet nullOrScaler', key, val);
+		obj[key] = val;
+	} else {
+		if (Array.isArray(currentVal)) {
+			if (typeof val === 'object') {
+				// Replace array
+				console.log('safeSet array', key, val);
+				obj[key] = Array.isArray(val) ? val : Object.values(val);
+			} else {
+				// Append to end of existing array? Create a single element array?
+				throw new Error(`safeSet Error: Expected array or object @key ${key} but got scalar
+				Rejected update was ${val}`);
+			}
+		} else if (typeof val === 'object') {
+			// Deep merge
+			obj[key] = updateObject(obj[key], val, createAdditionalKeys);
+		} else {
+			throw new Error(`safeSet Error: Can't deep merge object into scalar @key ${key}
+							Rejected update was ${val}`);
+		}
+	}
+}
+
+const nullOrScaler = val => {
+	if (val === null) return true;
+	const t = typeof val;
+	return t === 'number' || t === 'string' || t === 'boolean';
+}
+
+
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // Helper Functions
 // Helper Functions
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
@@ -199,4 +436,8 @@ const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
 // Exports
 // Exports
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 
 
-export { autoMeta, combineModelWithMeta, combineExtraMeta, buildFieldSpecificMeta, buildFormGroupFunctionFactory };
+export { 
+	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
+	buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory,
+	generateNewModel
+};

+ 63 - 10
src/app/dynaform/services/dynaform.service.ts

@@ -1,5 +1,5 @@
 /* *********************************************************************************************************************
 /* *********************************************************************************************************************
- * Dynaform Service, exposing 10 public methods
+ * Dynaform Service
  * *********************************************************************************************************************
  * *********************************************************************************************************************
  * 
  * 
  * BUILD
  * BUILD
@@ -31,6 +31,20 @@
  * build(model, meta, true)	- build by combining model with metadata, creating new fields from metadata points that don't occur in the model
  * build(model, meta, true)	- build by combining model with metadata, creating new fields from metadata points that don't occur in the model
  *
  *
  *
  *
+ * BUILD STRATEGYS
+ * ---------------
+ * 
+ * MODELFIRST - The model determinies the shape of the form
+ * METAFIRST  - The metadata determines the shape of the form
+ * 
+ * Set the build strategy using the setBuildStrategy method e.g.
+ * 
+ * setBuildStrategy('MODELFIRST') - the default
+ * setBuildStrategy('NETAFIRST')
+ * 
+ * then use the build function in the normal way
+ *
+ *
  * REGISTER
  * REGISTER
  * --------
  * --------
  * Registers callbacks attached to the form (e.g. to buttons), identified by strings.
  * Registers callbacks attached to the form (e.g. to buttons), identified by strings.
@@ -73,10 +87,12 @@
 
 
 import { Injectable, ComponentRef } from '@angular/core';
 import { Injectable, ComponentRef } from '@angular/core';
 import { FormBuilder, FormGroup } from '@angular/forms';
 import { FormBuilder, FormGroup } from '@angular/forms';
+import { ModelMapperService } from './model-mapper.service';
+
 import {
 import {
-	autoMeta, combineModelWithMeta, combineExtraMeta,
-	buildFieldSpecificMeta, buildFormGroupFunctionFactory
-} from './_formdata-utils';
+	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
+	buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory, generateNewModel
+} from '@modules/dynaform/services/_formdata-utils';
 
 
 export interface FormAndMeta {
 export interface FormAndMeta {
 	form: FormGroup;
 	form: FormGroup;
@@ -91,12 +107,26 @@ export interface Callbacks {
 export class DynaformService {
 export class DynaformService {
 
 
 	public buildFormGroup: (meta) => FormGroup;
 	public buildFormGroup: (meta) => FormGroup;
+	private buildStrategy: 'MODELFIRST' | 'METAFIRST' = 'MODELFIRST'; // Make ENUM type
 	private callbacks: Callbacks = {};
 	private callbacks: Callbacks = {};
 
 
-	constructor(private fb: FormBuilder) {
+	private conGreen = 'color: white; background-color: green; font-weight: bold;';
+
+	constructor(private fb: FormBuilder, private modelMapper: ModelMapperService) {
 		this.buildFormGroup = buildFormGroupFunctionFactory(fb);
 		this.buildFormGroup = buildFormGroupFunctionFactory(fb);
 	}
 	}
 
 
+	setBuildStrategy(str): void {
+		switch(str.toUpperCase()) {
+			case 'MODELFIRST':	// Build from model, combining optional extra metadata
+			case 'METAFIRST':	// Build from metadata, sourcing values from the model (usindg meta's 'source' attribute)
+				this.buildStrategy = str.toUpperCase();
+				break;
+			default:
+				throw new Error(`DYNAFORM: Unknown Build Strategy ${str} - should be MODELFIRST or METAFIRST`);
+		}
+	}
+	
 	build(model: StringMap, meta = {}, createFromMeta = false): FormAndMeta {
 	build(model: StringMap, meta = {}, createFromMeta = false): FormAndMeta {
 		// Short name for autoBuildFormGroupAndMeta
 		// Short name for autoBuildFormGroupAndMeta
 		return this.autoBuildFormGroupAndMeta(model, meta, createFromMeta);
 		return this.autoBuildFormGroupAndMeta(model, meta, createFromMeta);
@@ -122,23 +152,46 @@ export class DynaformService {
 		}
 		}
 	}
 	}
 
 
+	buildNewModel(originalModel: StringMap, formVal: StringMap, meta: StringMap): StringMap {
+		console.log('%c *** buildNewModel *** ', this.conGreen);
+		const mapping = extractFieldMappings(meta); // Memoize
+		console.dir(mapping);
+		const updates = this.modelMapper.forwardMap(formVal, mapping);
+		console.log('%c *** Updates *** ', this.conGreen);
+		console.dir(updates);
+		return generateNewModel(originalModel, updates);
+	}
+
 	// -----------------------------------------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------------------------------------
 	// Convenience methods combining several steps
 	// Convenience methods combining several steps
 
 
 	autoBuildFormGroupAndMeta(model: StringMap, meta = {}, createFromMeta = false): FormAndMeta {
 	autoBuildFormGroupAndMeta(model: StringMap, meta = {}, createFromMeta = false): FormAndMeta {
-		if (Object.keys(model).length === 0) {
+		let _model;
+		if (this.buildStrategy === 'MODELFIRST') {
+			_model = model;
+		} else if (this.buildStrategy === 'METAFIRST') {
+			// Generate mapping from the 'source' attributes in the metadata (compatible with the model-mapper service)
+			// Then grab values from the model
+			const mapping = extractFieldMappings(meta);
+			_model = this.modelMapper.reverseMap(model, mapping, false); // <-- false - excludes bits of model not explicitly referenced in meta
+			console.log('%c *** Mapping and Mapped Model *** ', this.conGreen);
+			console.dir(mapping);
+			console.dir(_model);
+		}
+		if (this.buildStrategy === 'METAFIRST' || Object.keys(model).length === 0) {
 			createFromMeta = true;
 			createFromMeta = true;
 		}
 		}
-		const modelWithMeta = this.autoBuildModeledMeta(model, meta, createFromMeta);
+		const fullMeta = this.autoBuildModeledMeta(_model, meta, createFromMeta);
 		return {
 		return {
-			form: this.buildFormGroup(modelWithMeta),
-			meta: modelWithMeta
+			form: this.buildFormGroup(fullMeta),
+			meta: fullMeta
 		};
 		};
 	}
 	}
 
 
 	autoBuildModeledMeta(model: StringMap, meta = {}, createFromMeta = false) {
 	autoBuildModeledMeta(model: StringMap, meta = {}, createFromMeta = false) {
 		const modelWithMeta = this.combineModelWithMeta(model, meta, createFromMeta);
 		const modelWithMeta = this.combineModelWithMeta(model, meta, createFromMeta);
-		return this.buildFieldSpecificMeta(modelWithMeta);
+		const reorderedMeta = execMetaReorderingInstructions(modelWithMeta);
+		return this.buildFieldSpecificMeta(reorderedMeta);
 	}
 	}
 
 
 	// -----------------------------------------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------------------------------------

+ 128 - 59
src/app/dynaform/services/model-mapper.service.ts

@@ -89,7 +89,7 @@ const mapping = {
 
 
 (4) lazyReverseMap also copies additional properties, if it can
 (4) lazyReverseMap also copies additional properties, if it can
 	(it will merge objects without overwriting exissting properties, and won't overwrite scalar properties)
 	(it will merge objects without overwriting exissting properties, and won't overwrite scalar properties)
-	
+
 { a: 555,
 { a: 555,
   b: 777,
   b: 777,
   c: { r: 1000, s: 2000, t: 3000, lazy: 'everybody\'s lazy' },
   c: { r: 1000, s: 2000, t: 3000, lazy: 'everybody\'s lazy' },
@@ -112,93 +112,139 @@ export class ModelMapperService {
 
 
 	mapping: StringMap;
 	mapping: StringMap;
 	errors: string[] = [];
 	errors: string[] = [];
-	
+
+	debug = false;
+	private clog = (...args) => this.debug ? console.log(...args) : {}; // Wrapper for console log - silent if debug = false
+
 	constructor() { }
 	constructor() { }
 
 
 	public setMapping(mapping: StringMap) {
 	public setMapping(mapping: StringMap) {
 		this.mapping = mapping;
 		this.mapping = mapping;
 	}
 	}
-	
-	public forwardMap = (model: StringMap, mapping: StringMap = this.mapping, mapMissing = false, res = {}, stack = []): StringMap => {
+
+	public forwardMap = (
+		model: StringMap,
+		mapping: StringMap = this.mapping,
+		mapMissing = false,
+		res = {},
+		stack = []
+	): StringMap => {
+		// Map the input model onto res using the supplied mapping
 		Object.keys(model).forEach(key => {
 		Object.keys(model).forEach(key => {
-			let mappingPath = _.get(mapping, key, mapMissing ? [...stack, key].join('.') : false);
-			if (mappingPath) {
-				let mappingType = this.resolveMappingType(mappingPath);
+			const absPath = [...stack, key].join('.');
+			const _mapping = _.get(mapping, key, mapMissing ? key : false);
+			if (_mapping) {
+				const mappingType = this.resolveMappingType(_mapping);
 				switch(mappingType) {
 				switch(mappingType) {
 					case 'simple':
 					case 'simple':
-						this.deepSet(res, mappingPath, model[key]);
+						this.deepSet(res, _mapping, model[key]);
 						break;
 						break;
 					case 'nested':
 					case 'nested':
-						this.forwardMap(model[key], mappingPath, mapMissing, res, [...stack, key]);
+						// Container
+						if (_mapping.__) {
+							// Container with functional mapping (stored in __)
+							// of the form [fn,fn] or [source, fn, fn]
+							this.clog('-------------------------------------------');
+							this.clog('CONTAINER WITH STORED FUNCTIONAL MAPPING', absPath);
+							try {
+								const storedContainerMapping = _mapping.__
+								let targetPath = typeof storedContainerMapping[0] === 'string' ? storedContainerMapping[0] : absPath;
+								if (targetPath[0] === '/') {
+									targetPath = targetPath.slice(1);
+								}
+								this.clog('Target Path', targetPath);
+								const func = storedContainerMapping.find(m => typeof m === 'function');
+								const funcRes = func(model[key]);
+								const fullRes = targetPath ? _.set({}, targetPath, funcRes) : funcRes; // Construct an update object from the model's root
+								this.forwardMap(fullRes, {}, true, res);
+							} catch (e) {
+								this.clog(e);
+								this.errors.push('Error in *container* mapping function at', _mapping, ':::', e.message);
+							}
+							this.clog('-------------------------------------------');
+						} else {
+							this.forwardMap(model[key], _mapping, mapMissing, res, [...stack, key]);
+						}
 						break;
 						break;
 					case 'functional':
 					case 'functional':
 						try {
 						try {
-							let result = mappingPath.find(m => typeof m === 'function')(model[key]);
-							key = mappingPath.length === 3 ? mappingPath[0] : key;
+							const result = _mapping.find(m => typeof m === 'function')(model[key]);
+							key = _mapping.length === 3 ? _mapping[0] : key;
 							this.deepSet(res, key, result);
 							this.deepSet(res, key, result);
 						} catch (e) {
 						} catch (e) {
-							this.errors.push('Error in mapping function at', mappingPath, ':::', e.message);
+							this.errors.push('Error in mapping function at', _mapping, ':::', e.message);
 						}
 						}
 						break;
 						break;
 					default:
 					default:
-						this.deepSet(res, key, `MAPPING ERROR: Unknown mapping type in mapping at ${key}`);
+						this.deepSet(res, key, `MAPPING ERROR: Unknown mapping type in mapping at ${key}`); 
 				}
 				}
 			}
 			}
 		});
 		});
 		return res;
 		return res;
 	}
 	}
-	
-	public lazyForwardMap = (model: StringMap, mapping: StringMap = this.mapping): StringMap => this.forwardMap(model, mapping, true);
-	
-	public reverseMap = (model: StringMap, mapping: StringMap = this.mapping, mapMissing = false, pathsToDelete = [], stack = []): StringMap => {
+
+	public lazyForwardMap = (
+		model: StringMap,
+		mapping: StringMap = this.mapping
+	): StringMap => this.forwardMap(model, mapping, true)
+
+	public reverseMap = (
+		model: StringMap,
+		mapping: StringMap = this.mapping,
+		mapMissing = false,
+		pathsToDelete = [],
+		stack = []
+	): StringMap => {
 		// pathToDelete contains a list of source paths to delete from the model, leaving the missing to be straight-mapped
 		// pathToDelete contains a list of source paths to delete from the model, leaving the missing to be straight-mapped
 		if (!mapping) {
 		if (!mapping) {
 			throw new Error('Attempting to use Model Mapper without mapping');
 			throw new Error('Attempting to use Model Mapper without mapping');
 		}
 		}
-		let res = {};
-		let modelClone = stack.length ? model : _.cloneDeep(model); // Clone the model unless inside a recursive call
-		Object.keys(mapping).forEach(key => { 
-			let mappingType = this.resolveMappingType(mapping[key]);
-			let destinationPath = mapping[key];
-			let value;
+		const res = {};
+		const modelClone = stack.length ? model : _.cloneDeep(model); // Clone the model unless inside a recursive call
+		Object.keys(mapping).filter(key => key !== '__').forEach(key => { 
+			const dataMapping = mapping[key];
+			const mappingType = this.resolveMappingType(dataMapping);
 			switch(mappingType) {
 			switch(mappingType) {
 				case 'simple':
 				case 'simple':
-					value = _.get(modelClone, mapping[key]);
-					if (typeof value !== 'undefined') {
-						this.deepSet(res, key, value);
+					{
+						// A simple path
+						const value = _.get(modelClone, dataMapping);
+						if (typeof value !== 'undefined') {
+							this.deepSet(res, key, value);
+						}
 					}
 					}
 					break;
 					break;
 				case 'nested':
 				case 'nested':
-					value = this.reverseMap(modelClone, mapping[key], mapMissing, pathsToDelete, [...stack, key]);
-					if (Object.keys(value).length) {
-						this.deepSet(res, key, value);
+					{
+						let alternativeModel = null;
+						if (dataMapping.__) {
+							// Evaluate the functional mapping stored in __ and use the result as an alternative model for the container
+							const absKey = [...stack, key].join('.');
+							alternativeModel = this.evaluateReverseFunctionalMapping(model, absKey, dataMapping.__);
+							this.clog('Setting alternative model for container', alternativeModel);
+						}
+						const value = this.reverseMap(alternativeModel || modelClone, dataMapping, mapMissing, pathsToDelete, [...stack, key]);
+						if (Object.keys(value).length) {
+							this.deepSet(res, key, value);
+						}
 					}
 					}
 					break;
 					break;
 				case 'functional':
 				case 'functional':
-					if (typeof mapping[key][0] === 'string') {
-						destinationPath = mapping[key][0];
-					} else {
-						destinationPath = key;
-					}
-					let arg = _.get(modelClone, destinationPath);
-					if (typeof arg !== 'undefined') {
-						let func = _.findLast(mapping[key], m => typeof m === 'function');
-						try {
-							value = func(arg);
-							this.deepSet(res, key, value);
-						} catch(e) {
-							this.errors.push('Error in mapping function at', key, ':::', e.message);
-						}
+					{
+						// Evaluate the functional mapping stored in dataMapping
+						const absKey = [...stack, key].join('.');
+						const value = this.evaluateReverseFunctionalMapping(model, absKey, dataMapping);
+						this.deepSet(res, key, value);
 					}
 					}
 					break;
 					break;
 				default:
 				default:
 					this.errors.push(`MAPPING ERROR: Unknown mapping type in mapping at ${key}`);
 					this.errors.push(`MAPPING ERROR: Unknown mapping type in mapping at ${key}`);
 			}
 			}
 			if (mappingType && mappingType !== 'nested') {
 			if (mappingType && mappingType !== 'nested') {
-				pathsToDelete.push(destinationPath);
+				pathsToDelete.push(dataMapping);
 			}
 			}
 		});
 		});
-	
+
 		if (mapMissing && !stack.length) {
 		if (mapMissing && !stack.length) {
 			// Delete the paths we have already reverse mapped (if scalar value - not objects), to leave the rest
 			// Delete the paths we have already reverse mapped (if scalar value - not objects), to leave the rest
 			// Sort pathsToDelete by depth, shallowest to deepest
 			// Sort pathsToDelete by depth, shallowest to deepest
@@ -213,22 +259,25 @@ export class ModelMapperService {
 			const modelRemainder = this.deepCleanse(modelClone);
 			const modelRemainder = this.deepCleanse(modelClone);
 			Object.keys(modelRemainder).forEach(key => {
 			Object.keys(modelRemainder).forEach(key => {
 				this.deepSetIfEmpty(res, key, modelRemainder[key]);
 				this.deepSetIfEmpty(res, key, modelRemainder[key]);
-			})
+			});
 		}
 		}
 		return res;
 		return res;
 	}
 	}
-	
-	public lazyReverseMap = (model: StringMap, mapping: StringMap = this.mapping): StringMap => this.reverseMap(model, mapping, true);
-	
+
+	public lazyReverseMap = (
+		model: StringMap,
+		mapping: StringMap = this.mapping
+	): StringMap => this.reverseMap(model, mapping, true)
+
 	public getErrors() {
 	public getErrors() {
 		return this.errors;
 		return this.errors;
 	}
 	}
-	
+
 	// -----------------------------------------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------------------------------------
 
 
 	private resolveMappingType = mappingPath => {
 	private resolveMappingType = mappingPath => {
 		let mappingType;
 		let mappingType;
-		let t = typeof mappingPath;
+		const t = typeof mappingPath;
 		if (t === 'number' || t === 'string') { // CHECK WHAT HAPPENS with number types
 		if (t === 'number' || t === 'string') { // CHECK WHAT HAPPENS with number types
 			mappingType = 'simple';
 			mappingType = 'simple';
 		} else if (t === 'object') {
 		} else if (t === 'object') {
@@ -249,13 +298,33 @@ export class ModelMapperService {
 		}
 		}
 		return mappingType;
 		return mappingType;
 	}
 	}
-	
+
+	private evaluateReverseFunctionalMapping = (model, absKey, fnMapping) => {
+		this.clog('evaluateReverseFunctionalMapping');
+		this.clog(model, absKey, fnMapping);
+		let arg;
+		let result;
+		const path = typeof fnMapping[0] === 'string' ? fnMapping[0] : absKey;
+		if (path === '/') {
+			arg = model; // '/' indicates use the entire model
+		} else {
+			arg = _.get(model, path.replace(/^\//, ''));
+		}
+		const func = _.findLast(fnMapping, m => typeof m === 'function');
+		try {
+			result = func(arg);
+		} catch(e) {
+			this.errors.push('Error in mapping function at', absKey, ':::', e.message);
+		}
+		return result;
+	}
+
 	private deepSet = (obj, mappingPath, valueToSet, overwrite = true) => {
 	private deepSet = (obj, mappingPath, valueToSet, overwrite = true) => {
 		// NOTE: This mutates the incoming object at the moment, so doesn't reed to return a value
 		// NOTE: This mutates the incoming object at the moment, so doesn't reed to return a value
 		// Maybe rewrite with a more functional approach?
 		// Maybe rewrite with a more functional approach?
 		// Will deep merge where possible
 		// Will deep merge where possible
-		let currentVal = _.get(obj, mappingPath);
-		let t = typeof currentVal;
+		const currentVal = _.get(obj, mappingPath);
+		const t = typeof currentVal;
 		if (t === 'undefined') {
 		if (t === 'undefined') {
 			_.set(obj, mappingPath, valueToSet);
 			_.set(obj, mappingPath, valueToSet);
 		} else if (t === 'number' || t === 'string' || t === 'boolean') {
 		} else if (t === 'number' || t === 'string' || t === 'boolean') {
@@ -264,7 +333,7 @@ export class ModelMapperService {
 				this.errors.push('WARNING: Overwriting scalar value at', mappingPath);
 				this.errors.push('WARNING: Overwriting scalar value at', mappingPath);
 				_.set(obj, mappingPath, valueToSet);
 				_.set(obj, mappingPath, valueToSet);
 			} else {
 			} else {
-				this.errors.push('WARNING: Discarding scalar value at', mappingPath, 'as exisiting vale would be overwritten');
+				this.errors.push('WARNING: Discarding scalar value at', mappingPath, 'as exisiting non-scalar value would be overwritten');
 			}
 			}
 		} else if (t === 'object' && typeof valueToSet === 'object') {
 		} else if (t === 'object' && typeof valueToSet === 'object') {
 			// Deep merge
 			// Deep merge
@@ -277,11 +346,11 @@ export class ModelMapperService {
 			this.errors.push('WARNING: Could not merge', typeof valueToSet, 'with object')
 			this.errors.push('WARNING: Could not merge', typeof valueToSet, 'with object')
 		}
 		}
 	}
 	}
-	
+
 	private deepSetIfEmpty = (obj, mappingPath, valueToSet) => this.deepSet(obj, mappingPath, valueToSet, false);
 	private deepSetIfEmpty = (obj, mappingPath, valueToSet) => this.deepSet(obj, mappingPath, valueToSet, false);
-	
+
 	private deepCleanse = obj => {
 	private deepCleanse = obj => {
-		let cleansedObj = Object.keys(obj)
+		const cleansedObj = Object.keys(obj)
 			.filter(k => obj[k] !== null && obj[k] !== undefined)  // Remove undefined and null
 			.filter(k => obj[k] !== null && obj[k] !== undefined)  // Remove undefined and null
 			.reduce((newObj, k) => {
 			.reduce((newObj, k) => {
 				typeof obj[k] === 'object' ?
 				typeof obj[k] === 'object' ?
@@ -296,7 +365,7 @@ export class ModelMapperService {
 		});
 		});
 		return cleansedObj;
 		return cleansedObj;
 	}
 	}
-	
+
 	private sortByPathDepth = pathArr => _.sortBy(pathArr, p => p.split('.').length);
 	private sortByPathDepth = pathArr => _.sortBy(pathArr, p => p.split('.').length);
 
 
 	private clearErrors() {
 	private clearErrors() {

+ 13 - 3
src/app/dynaform/testdata/testset.1.ts

@@ -108,13 +108,22 @@ const checkbuttonGroup = {
 	meta: { iMaskTheOthers: {}, groupMemberTwo: {}, groupMemberThree: {} }
 	meta: { iMaskTheOthers: {}, groupMemberTwo: {}, groupMemberThree: {} }
 };
 };
 
 
-
+const multiline = {
+	type: 'multiline',
+	class: 'centered',
+	value: `This is
+a multiline message
+for all the testers
+in the house`
+}
 
 
 // ---------------------------------------------------------------------------------------------------------------------
 // ---------------------------------------------------------------------------------------------------------------------
 // Kendo
 // Kendo
 
 
 const timepicker = {
 const timepicker = {
-	type: 'timepicker'
+	type: 'timepicker',
+	// value: new Date(1999, 10, 11, 12, 30, 45)
+	value: "12:45"
 };
 };
 
 
 const datepicker = {
 const datepicker = {
@@ -166,7 +175,8 @@ const meta = {
 	checkbuttonGroup,
 	checkbuttonGroup,
 	timepicker,
 	timepicker,
 	datepicker,
 	datepicker,
-	container
+	container,
+	multiline
 };
 };
 
 
 export { model, meta };
 export { model, meta };

+ 139 - 0
src/app/dynaform/testdata/testset.3.ts

@@ -0,0 +1,139 @@
+// TESTS: Metafirst form generation, with data grabbed from model using 'source'
+
+const model = {
+	fieldOne: 'fieldOne',
+	fieldTwo: 'fieldTwo',
+	fieldThree: 'I am fieldThree',
+	double: '4',
+	nest: {
+		test: 'I am from nest.test',
+		funcMap2: '3'
+	},
+	nestedGroup: {
+		fieldA: 'nestedGroup.fieldA',
+		fieldB: 'nestedGroup.fieldB',
+		fieldC: 'nestedGroup.fieldC',
+		anotherNestedGroup: {
+			fieldE: 'nestedGroup.anotherNestedGroup.fieldE',
+			fieldF: 'nestedGroup.anotherNestedGroup.fieldF',
+			yetAnother: {
+				fieldH: 'nestedGroup.anotherNestedGroup.yesAnother.fieldH',
+				jay: 'I AM DEEPLY NESTED jay'
+			}
+		},
+		z: 'THE END',
+		testforSourcedNest: {
+			soNestA: 'I am soNestA',
+			soNestB: 'And this is soNestB',
+			testC: 'CCC',
+			e: {
+				f: {
+					g: {
+						h: {
+							i: 'DID YOU GO DEEPER?',
+							deeperAgain: {
+								j: 'DEEPEST Jay'
+							}
+						}
+					}
+				}
+			}
+		}
+	},
+	fucntionalSourcedNest: {
+		aaa: '122',
+		bbb: '455',
+		ccc: '788'
+	}
+};
+
+const meta = {
+	a: { label: 'a should be fieldOne', source: 'fieldOne', class: 'medium-field' },
+	b: { label: 'b should end with jay', source: 'nestedGroup.anotherNestedGroup.yetAnother.jay' },
+	functionalMapping: {
+		source: [
+			'double',
+			n => (parseFloat(n) / 2).toString(),
+			n => (parseFloat(n) * 2).toString()
+		]
+	},
+	nest: {
+		label: 'Nested values',
+		meta: {
+			c: { label: 'c should be ...fieldC', source: '/nestedGroup.fieldC' },
+			d: { label: 'd should be THE END', source: '/nestedGroup.z' },
+			e: { label: 'e should be fieldThree', source: '/fieldThree', type: 'checkbutton' },
+			test: { label: 'Test of default source' },
+			funcMap2: {
+				source: [
+					n => (parseFloat(n) / 3).toString(),
+					n => (parseFloat(n) * 3).toString()
+				]
+			}
+		}
+	},
+	head: { type: 'heading', text: 'Default mappings (no \'source\')', before: 'fieldTwo' },
+	fieldTwo: { type: 'textarea' },
+	sourcedNest: {
+		label: 'Nested values from source data tree',
+		source: '/nestedGroup.testforSourcedNest',
+		meta: {
+			testA: { source: 'soNestA' },
+			testB: { source: 'soNestB' },
+			testC: { label: 'should be CCC' },
+			deeper: {
+				source: 'e.f.g.h',
+				meta: {
+					depthRetrivelWithinParentTree: { source: 'i' },
+					deeperAgain: {
+						meta: {
+							veryDeep: { source: 'j', label: 'Should be DEEPEST Jay', class: 'medium-field' }
+						}
+					}
+				}
+			}
+		}
+	},
+	fucntionalSourcedNest: {
+		label: 'Nested values using function as datasource',
+		source: [
+			n => n,
+			n => Object.entries(n)
+				.map( ([key, val]) => [key, parseInt(<string>val) + 1] )
+				.reduce( (acc, [key, val]) => { 
+					acc[<string>key] = val;
+					return acc;
+				}, {})
+		],
+		meta: {
+			aaa: {},
+			bbb: {},
+			ccc: {},
+			ddd: {
+				label: 'Nested functional mapping',
+				source: [
+					'aaa',
+					n => (parseFloat(n) / 2).toString(),
+					n => (parseFloat(n) * 2).toString()
+				]
+			}
+		}
+	},
+	dfns: {
+		label: 'Another functional mapping test, with a nested parent function',
+		meta: {
+			general: {
+				source: [
+					'/',
+					n => n,
+					n => ({ val: 'Zippy Zap', tal: 'Zero' })
+				],
+				meta: {
+					val: { source: 'val' }
+				}
+			}
+		}
+	}
+};
+
+export { model, meta };

+ 124 - 0
src/app/dynaform/testdata/testset.4.ts

@@ -0,0 +1,124 @@
+// TESTS: Metafirst form generation, with data grabbed from model using 'source'
+// test of special source characters
+
+const dumbFlatten = obj => {
+	return Object.assign( {}, ...function _flatten(objectBit) {  
+		return [].concat(                                                       
+			...Object.keys(objectBit).map(                                      
+				key => typeof objectBit[key] === 'object' ? _flatten(objectBit[key]) : { [key]: objectBit[key] }              
+			)
+		)
+	}(obj));
+};
+
+const testReturnImplicitSource = obj => ({
+	bbb: obj.bbb,
+	ccc: obj.ccc
+});
+
+const testReturnRoot = obj => ({
+	a: obj.a,
+	b: obj.b,
+	f: {
+		g: obj.g,
+		h: {
+			i: obj.i
+		}
+	},
+	nest3: {
+		aaa: {
+			bbb: obj.bbb,
+			ccc: obj.ccc
+		}
+	}
+});
+
+const testReturnFH = obj => ({
+	i: obj.i
+});
+  
+const model = {
+	a: 1,
+	b: 2,
+	c: 3,
+	d: 4,
+	e: 5,
+	f: {
+		g: 6,
+		h: {
+			i: 7,
+			j: 8, 
+			k: {
+				l: {
+					m: 'Mmmmm...'
+				}
+			}
+		}
+	},
+	nest3: {
+		aaa: {
+			bbb: 1,
+			ccc: 3
+		}
+	}
+};
+
+const meta = {
+	a: {},
+	nest: {
+		label: 'Nest Test - source /',
+		source: '/',
+		meta: {
+			b: {},
+			c: {},
+			z: { label: 'Z (with no model value)' }
+		}
+	},
+	nest2: {
+		label: 'Nest Test 2 - source f.h',
+		source: 'f.h',
+		meta: {
+			i: {},
+			j: {},
+			g: { source: '/f.g' },
+			nestedNest: {
+				label: 'Nested nest with source reset to /',
+				source: '/',
+				meta: {
+					d: {},
+					fg: { source: 'f.g' }
+				}
+			},
+			nestedNestTwo: {
+				label: 'Nested nest with source reset to /f.h.k.l',
+				source: '/f.h.k.l',
+				meta: {
+					m: {},
+					zzz: {}
+				}
+			}
+		}
+	},
+	nest3: {
+		label: 'Test of nested functional mappings',
+		meta: {
+			aaa: {
+				source: [
+					obj => testReturnImplicitSource(obj),
+					obj => dumbFlatten(obj)
+				],
+				meta: {
+					a: {},
+					b: {},
+					g: {},
+					bbb: {},
+					ccc: {},
+					i: {}
+				}
+			}
+		}
+	}
+}
+
+export { model, meta };
+

+ 16 - 1
src/app/dynaform/utils.ts

@@ -35,8 +35,23 @@ const standardTransformer: ValueTransformer = {
 	}
 	}
 };
 };
 
 
+// Generate array from CSV string
+const toArrTag = (str: TemplateStringsArray): string[] => str[0].split(',').map(key => key.trim());
+
+// Pad array
+const arrayPad = (length: number) => (arr: string[]): any[] => [...arr, ...Array(length - arr.length).fill('')];
 
 
 // Utility function for casting an array to metadata (useful for components that render FormGroups)
 // Utility function for casting an array to metadata (useful for components that render FormGroups)
 const arrayToMeta = array => array.map(val => ({ name: val, 'value' : val }));
 const arrayToMeta = array => array.map(val => ({ name: val, 'value' : val }));
 
 
-export { standardModifiers, standardTransformer, arrayToMeta };
+// Exclude 'fieldsToExclude' from obj, returning a new object
+// fieldsToExclude can be an array of field keys or a CSV of field keys
+const excludeFields = (obj: StringMap, fieldsToExclude: string | string[]) => {
+	const ex = Array.isArray(fieldsToExclude) ? fieldsToExclude : fieldsToExclude.split(',').map(key => key.trim());
+	return Object.entries(obj).reduce(
+		(res, [key, val]) => ex.includes(key) ? res : { ...res, [key]: val },
+		{}
+	);
+}
+
+export { standardModifiers, standardTransformer, toArrTag, arrayPad, arrayToMeta, excludeFields };