Explorar o código

Rerunning all tests, and adding simpler API to Dynaform Service

Richard Knight %!s(int64=6) %!d(string=hai) anos
pai
achega
064562f968

+ 14 - 17
src/app/_mock/testfields.v1.ts

@@ -1,7 +1,7 @@
 // TESTS: Generation of Modeled MetaData using form field models
 
 import { ValueTransformer } from './../dynaform/interfaces';
-import * as fmd from './../dynaform/models'; // fmd = Form Meta Data
+import * as fmd from '../dynaform/models/field.model'; // fmd = Form Meta Data
 
 // ---------------------------------------------------------------------------------------------------------------------
 // Native
@@ -123,25 +123,19 @@ const datepicker = new fmd.DatepickerField({
 // ---------------------------------------------------------------------------------------------------------------------
 // Container
 
-const container = new fmd.Container({
-	// basicTextField,
-	// styledTextField,
-	// textareaField,
-	// passwordField,
-	// selectField,
-	// radioField,
-	// disabledTextField,
-	// radioFieldHorizontal,
-	checkbutton2,
-	// dropdownModifiedInput,
-	// checkbuttonGroup,
-	// timepicker,
-	// datepicker
+const basicTestContainer = new fmd.Container({
+	name: 'basicTestContainer',
+	meta: {
+		basicTextField,
+		checkbutton2
+	}
 });
 
 // ---------------------------------------------------------------------------------------------------------------------
 
-export const meta = {
+const model = {};
+
+const meta = {
 	basicTextField,
 	styledTextField,
 	textareaField,
@@ -155,5 +149,8 @@ export const meta = {
 	checkbuttonGroup,
 	timepicker,
 	datepicker,
-	// container
+	basicTestContainer
 };
+
+export { model, meta };
+

+ 5 - 19
src/app/_mock/testfields.v2.ts

@@ -1,4 +1,4 @@
-// TESTS: Generation of Modeled MetaData using buildModeledMeta library function
+// TESTS: Generation of Field-Specific Modeled MetaData using buildFieldSpecific library function
 
 import { ValueTransformer } from './../dynaform/interfaces';
 
@@ -6,14 +6,12 @@ import { ValueTransformer } from './../dynaform/interfaces';
 // Native
 
 const basicTextField = {
-	name: 'basicTextField',
 	type: 'Text',
 	label: 'Field One',
 	placeholder: 'Type a value here'
 };
 
 const styledTextField = {
-	name: 'styledTextField',
 	type: 'Text',
 	placeholder: 'With a DOM id and CSS classes applied',
 	class: ['red', 'bgPaleBlue'],
@@ -21,38 +19,32 @@ const styledTextField = {
 };
 
 const textareaField = {
-	name: 'textareaField',
 	type: 'Textarea',
 	placeholder: 'Type your long-winded comments here'
 };
 
 const passwordField = {
-	name: 'passwordField',
 	type: 'Password',
 	placeholder: 'It\'s a secret'
 };
 
 const selectField = {
-	name: 'selectField',
 	type: 'Select',
 	options: ['', 'Apples', 'Oranges', 'Pears', 'Gorgonzola']
 };
 
 const radioField = {
-	name: 'radioField',
 	type: 'radio',
 	options: ['Tea', 'Coffee', 'Cocoa', 'Yerba Maté']
 };
 
 const disabledTextField = {
-	name: 'disabledTextField',
 	type: 'Text',
 	placeholder: 'You can\'t touch this',
 	isDisabled: true
 };
 
 const radioFieldHorizontal = {
-	name: 'radioFieldHorizontal',
 	type: 'radio',
 	options: ['Fish', 'Fowl', 'Neither'],
 	horizontal: true
@@ -63,13 +55,11 @@ const radioFieldHorizontal = {
 // Custom
 
 const checkbutton = {
-	name: 'checkbutton',
 	type: 'checkbutton',
 	value: '456'
 };
 
 const checkbutton2 = {
-	name: 'checkbutton2',
 	type: 'checkbutton',
 	value: 'Yowsa'
 };
@@ -106,7 +96,6 @@ const transformerFunctions: ValueTransformer = {
 	}
 };
 const dropdownModifiedInput = {
-	name: 'dropdownModifiedInput',
 	type: 'dropdownModifiedInput',
 	value: 'lovely',
 	modifiers,
@@ -114,14 +103,12 @@ const dropdownModifiedInput = {
 };
 
 const checkbuttonGroup = {
-	name: 'checkbuttonGroup',
 	type: 'CheckbuttonGroup',
 	firstEnablesRest: true,
 	meta: [{name: 'iMaskTheOthers'}, {name: 'groupMemberTwo'}, {name: 'groupMemberThree'}]
 };
 
 const checkbuttonGroup2 = {
-	name: 'waggaWahWah',
 	type: 'CheckbuttonGroup',
 	firstEnablesRest: true,
 	meta: [{name: 'One'}, {name: 'Two'}, {name: 'Three'}]
@@ -131,12 +118,10 @@ const checkbuttonGroup2 = {
 // Kendo
 
 const timepicker = {
-	name: 'timepicker',
 	type: 'timepicker'
 };
 
 const datepicker = {
-	name: 'datepicker',
 	type: 'datepicker'
 };
 
@@ -144,8 +129,7 @@ const datepicker = {
 // Container
 
 const container = {
-	name: 'container',
-	type: 'container',
+	type: 'Container',
 	meta: {
 		basicTextField,
 		checkbuttonGroup2,
@@ -154,6 +138,8 @@ const container = {
 
 // ---------------------------------------------------------------------------------------------------------------------
 
+const model = {};
+
 const meta = {
 	basicTextField,
 	styledTextField,
@@ -171,4 +157,4 @@ const meta = {
 	container
 };
 
-export { meta };
+export { model, meta };

+ 5 - 8
src/app/_mock/testfields.v3.ts

@@ -1,14 +1,7 @@
 // TESTS: Generation of Automatic MetaData from model using autoMeta library function
 // All fields will default to type 'text'
 
-const model1 = {
-	a: 'Value 1',
-	b: 'Value 2',
-	c: 'Value 3',
-	d: ''
-};
-
-const model2 = {
+const model = {
 	a: 'Value 1',
 	b: 'Value 2',
 	c: 'Value 3',
@@ -22,3 +15,7 @@ const model2 = {
 	},
 	z: 'THE END'
 };
+
+const meta = {};
+
+export { model, meta };

+ 3 - 3
src/app/_mock/testfields.v4.ts

@@ -1,6 +1,6 @@
 // TESTS: Lazy combination of Automatic MetaData with Extra MetaData
 
-const model2 = {
+const model = {
 	a: 'Value 1',
 	b: 'Value 2',
 	c: 'Maybe',
@@ -15,7 +15,7 @@ const model2 = {
 	z: 'THE END'
 };
 
-const extra2 = {
+const meta = {
 	b: { type: 'checkbutton' },
 	c: { label: 'Property Three', type: 'radio', options: ['Yes', 'No', 'Maybe'], horizontal: 1 },
 	d: {
@@ -26,5 +26,5 @@ const extra2 = {
 	}
 };
 
-export { model2, extra2 };
+export { model, meta };
 

+ 25 - 27
src/app/app.component.ts

@@ -2,10 +2,15 @@ import { Component, OnInit, OnChanges, ViewChild, TemplateRef } from '@angular/c
 import { FormGroup } from '@angular/forms';
 import { DynaformService } from './dynaform/services/dynaform.service';
 
+import * as test1 from './_mock/testfields.v1';
 import * as test2 from './_mock/testfields.v2';
+import * as test3 from './_mock/testfields.v3';
+import * as test4 from './_mock/testfields.v4';
 import * as test5 from './_mock/testfields.v5';
 import * as test6 from './_mock/testfields.v6';
 
+const testdata = [ test1, test2, test3, test4, test5, test6 ];
+
 @Component({
 	selector: 'app-root',
 	templateUrl: './app.component.html',
@@ -16,6 +21,9 @@ export class AppComponent implements OnInit, OnChanges {
 	form: FormGroup;
 	meta: StringMap;
 
+	hCssRed = 'color: white; background-color: maroon; font-weight: bold;';
+	hCssGreen = 'color: white; background-color: green; font-weight: bold;';
+
 	@ViewChild('testTemplate', { read: TemplateRef })
 	tref: TemplateRef<any>;
 
@@ -26,34 +34,24 @@ export class AppComponent implements OnInit, OnChanges {
 
 	ngOnInit() {
 
-		const testcase: number = 5;
-
-		console.log('%c *** TEST DATA *** ', 'color: white; background-color: green; font-weight: bold;');
-		switch (testcase) {
-			case 1:
-				break;
-			case 2:
-				// Test 2 - Build a FormGroup form metadata
-				console.log(test2);
-				this.form = this.dynaform.buildFormGroup(test2.meta);
-				this.meta = test2.meta;
-				break;
-			case 5:
-				// Test 5 - Deeply nested FormGroups
-				console.log(test5);
-				({ form: this.form, meta: this.meta } = this.dynaform.build(test5.model, test5.meta));
-				break;
-			case 6:
-				// Test 6 - Validation
-				console.log(test6);
-				({ form: this.form, meta: this.meta } = this.dynaform.build(test6.model, test6.meta));
-				break;
-		}
-		console.log('%c *** FormGroup *** ', 'color: white; background-color: green; font-weight: bold;');
-		console.dir(this.form);
-		console.log('%c *** MetaData *** ', 'color: white; background-color: green; font-weight: bold;');
+		// Supply the test to run in the query string ?test=3
+		const testcase = parseInt(new URLSearchParams(document.location.search).get('test'), 10) || 2;
+		const { model, meta } = testdata[testcase - 1];
+
+		const hRed = this.hCssRed;
+		const hGreen = this.hCssGreen;
+
+		console.log('%c *** TEST DATA *** ', hRed);
+		console.log('Model', model);
+		console.log('Meta', meta);
+
+		const dynaformdata = this.dynaform.build(model, meta);
+
+		({ form: this.form, meta: this.meta } = dynaformdata);
+		console.log('%c *** Modeled MetaData *** ', hGreen);
 		console.dir(this.meta);
-		console.log('*******************************');
+		console.log('%c *** FormGroup *** ', hGreen);
+		console.dir(this.form);
 	}
 
 	ngOnChanges() {

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

@@ -31,7 +31,7 @@ export class CheckbuttonGroupComponent extends GroupInputComponent implements On
 			// Observe value changes on first control
 			this.firstControl.valueChanges.subscribe(val => {
 				for (let i = 1; i < this.childMeta.length; i++) {
-					// NOTE: We rassign 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/
 					this.childMeta[i] = {
 						...this.childMeta[1],

+ 1 - 1
src/app/dynaform/dynaform.component.html

@@ -18,7 +18,7 @@
 	
 	<ng-template #recursiveDynaform>
 		<div class="row pt-3">
-			<h3 class="col-sm-12" [ngClass]="'h-dyna-' + (path.length + 2)">{{ meta.name }}</h3>
+			<h3 class="col-sm-12" [ngClass]="'h-dyna-' + (path.length + 2)">{{ meta.label }}</h3>
 		</div>
 		<app-dynaform [formGroup]="control" [meta]="meta.meta" [template]="template"></app-dynaform>
 	</ng-template>

+ 16 - 11
src/app/dynaform/models/index.ts

@@ -45,14 +45,17 @@ interface TimePickerFieldMetaData extends SimpleFieldMetaData {
 	steps: StringMap;
 }
 
+// Utility to unCamelCase
+const unCamelCase = (str: string): string => str.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
+
 // ---------------------------------------------------------------------------------------------------------------------
 // Form Field MetaData Models
 // ---------------------------------------------------------------------------------------------------------------------
 // Base Implementations
 
 abstract class SimpleField {
-	type = 'text';
-	name = 'missingName';
+	type: string;
+	name: string;
 	origin?: string;
 	label?: string;
 	value;
@@ -60,7 +63,7 @@ abstract class SimpleField {
 	placeholder = '';
 	class?: string | Array<string>;
 	id?: string;
-	isDisabled?: boolean;
+	isDisabled = false;
 	validators: Array<ValidatorFn> = [];
 	asyncValidators: Array<AsyncValidatorFn> = [];
 	valFailureMsgs: StringMap = {};
@@ -74,7 +77,7 @@ abstract class SimpleField {
 		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());
+			this.label = unCamelCase(this.name);
 		}
 	}
 }
@@ -162,12 +165,10 @@ class CheckbuttonGroup {
 		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());
-		}
-		this.meta = [];
-		while (groupmeta.meta.length) {
-			this.meta.push( new CheckbuttonField(groupmeta.meta.shift()) );
+			this.label = unCamelCase(this.name);
 		}
+		const groupMembers = Array.isArray(groupmeta.meta) ? groupmeta.meta : Object.values(groupmeta.meta);
+		this.meta = groupMembers.map(cb => new CheckbuttonField(cb));
 	}
 }
 
@@ -191,11 +192,15 @@ class DatepickerField extends SimpleField {
 
 class Container {
 	type = 'Container';
+	name: string;
 	label = '';
 	template?: TemplateRef<any>;
 	meta: StringMap; // TODO: Tighten up on type with union type
-	constructor(children: StringMap) {
-		this.meta = children;
+	constructor(meta: StringMap) {
+		meta.meta ? Object.assign(this, meta) : this.meta = meta;
+		if (!this.label) {
+			this.label = unCamelCase(this.name);
+		}
 	}
 }
 

+ 16 - 13
src/app/dynaform/services/dynaform.service.ts

@@ -55,24 +55,27 @@ export class DynaformService {
 		this.buildFormGroup = buildFormGroupFunctionFactory(fb);
 	}
 
-	// -----------------------------------------------------------------------------------------------------------------
-	// Convenience methods combining several steps
-
-	build(model, meta = {}): FormAndMeta {
+	build(model: StringMap, meta = {}, createFromMeta = false): FormAndMeta {
 		// Short name for autoBuildFormGroupAndMeta
-		return this.autoBuildFormGroupAndMeta(model, meta);
+		return this.autoBuildFormGroupAndMeta(model, meta, createFromMeta);
 	}
 
-	autoBuildFormGroupAndMeta(model, meta = {}): FormAndMeta {
-		const modelWithMeta = this.autoBuildModeledMeta(model, meta);
+	// -----------------------------------------------------------------------------------------------------------------
+	// Convenience methods combining several steps
+
+	autoBuildFormGroupAndMeta(model: StringMap, meta = {}, createFromMeta = false): FormAndMeta {
+		if (Object.keys(model).length === 0) {
+			createFromMeta = true;
+		}
+		const modelWithMeta = this.autoBuildModeledMeta(model, meta, createFromMeta);
 		return {
 			form: this.buildFormGroup(modelWithMeta),
 			meta: modelWithMeta
 		};
 	}
 
-	autoBuildModeledMeta(model, meta = {}) {
-		const modelWithMeta = this.combineModelWithMeta(model, meta);
+	autoBuildModeledMeta(model: StringMap, meta = {}, createFromMeta = false) {
+		const modelWithMeta = this.combineModelWithMeta(model, meta, createFromMeta);
 		return this.buildFieldSpecificMeta(modelWithMeta);
 	}
 
@@ -86,12 +89,12 @@ export class DynaformService {
 	// -----------------------------------------------------------------------------------------------------------------
 	// Lower-level methods
 
-	combineModelWithMeta(model, meta) {
-		return combineModelWithMeta(model, meta);
+	combineModelWithMeta(model: StringMap, meta, createFromMeta = false) {
+		return combineModelWithMeta(model, meta, createFromMeta);
 	}
 
-	combineExtraMeta(meta, extraMeta) {
-		return combineExtraMeta(meta, extraMeta);
+	combineExtraMeta(meta, extraMeta, createFromExtra = false) {
+		return combineExtraMeta(meta, extraMeta, createFromExtra);
 	}
 
 	autoMeta(model) {

+ 35 - 18
src/app/dynaform/services/meta-utils.ts

@@ -19,14 +19,14 @@
 
 import { FormBuilder, FormGroup, FormControl, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
 import { reduce } from 'lodash/fp';
-import * as fmdModels from './../models';
+import * as fmdModels from '../models/field.model';
 
 // REMINDER: Import this directly from @angular/forms when we upgrade to Angular 6
 // While we're still on Angular 5 just declare it
 interface AbstractControlOptions {
-	validators?: ValidatorFn | ValidatorFn[] | null
-	asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null
-	updateOn?: 'change' | 'blur' | 'submit'
+	validators?: ValidatorFn | ValidatorFn[] | null;
+	asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null;
+	updateOn?: 'change' | 'blur' | 'submit';
 }
 
 
@@ -52,20 +52,20 @@ const autoMeta = model => Object.entries(model)
 // ---------------------------------------------------------------------------------------------------------------------
 
 const combineMetaForField = (metaF, extraMetaF) => Object.assign(metaF, extraMetaF);
-const combineExtraMeta = (metaG, extraMeta) => {
+const combineExtraMeta = (metaG, extraMeta, createFromExtra = false) => {
 	const combinedMeta = {};
 	Object.entries(extraMeta).forEach(([key, val]) => {
-		if (typeof metaG[key] === 'object') {
-			combinedMeta[key] = (<any>val).meta ?
-				combineMetaForField(metaG[key], { meta: combineExtraMeta(metaG[key].meta, (<any>val).meta) }) :
-				combineMetaForField(metaG[key], val);
+		if (typeof metaG[key] === 'object' || createFromExtra) {
+			combinedMeta[key] = metaG[key] && (<any>val).meta ?
+				combineMetaForField(metaG[key], { meta: combineExtraMeta(metaG[key].meta, (<any>val).meta, createFromExtra) }) :
+				combineMetaForField(metaG[key] || {}, val);
 		}
 	});
 	return { ...metaG, ...combinedMeta };
 };
 
 // Combine model with overrides (after automatically generating metadata from the model)
-const combineModelWithMeta = (model, extraMeta) => combineExtraMeta(autoMeta(model), extraMeta);
+const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => combineExtraMeta(autoMeta(model), extraMeta, createFromExtra);
 
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -74,7 +74,6 @@ const combineModelWithMeta = (model, extraMeta) => combineExtraMeta(autoMeta(mod
 
 const buildFieldClassName = (t = 'text') => {
 	const start = t[0].toUpperCase() + t.slice(1);
-	console.log(start);
 	if (start === 'Container' || t.slice(-5) === 'Group') {
 		return start;
 	}
@@ -94,15 +93,15 @@ const buildModeledField = metaFoG => {
 const buildModeledFieldGroupMember = metaFoG => {
 	const modeledGroupMember = buildModeledField(metaFoG);
 	if (isContainer(metaFoG)) {
-		modeledGroupMember.meta = _buildModeledFieldGroup(metaFoG.meta);
+		modeledGroupMember.meta = _buildFieldSpecificMeta(metaFoG.meta);
 	}
 	return modeledGroupMember;
 };
 
 // Build Form Group
 const buildModeledFieldGroupReducerIteree = (res, metaFoG) => Object.assign(res, { [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
-const _buildModeledFieldGroup = metaG => reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
-const buildFieldSpecificMeta = metaG => _buildModeledFieldGroup(metaG);
+const _buildFieldSpecificMeta = metaG => reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
+const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
 
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -131,8 +130,13 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 	const _buildFormGroup = _metaG => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG));
 
 	const buildFormGroup = metaG => {
-		const metaWithNames = addMissingNames(metaG);
-		return _buildFormGroup(metaWithNames);
+		// Ensure that we have Field-Specific Metadata, not raw Objects
+		const metaWithNameKeys = addMissingNames(metaG);
+		console.log(metaWithNameKeys);
+		// MAYBE only run this if first entry isn't right, for reasons of efficiency
+		const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
+		console.log(fieldModeledMeta);
+		return _buildFormGroup(fieldModeledMeta);
 	};
 	return buildFormGroup;
 };
@@ -151,7 +155,7 @@ const isGroup = (metaFoG): boolean => !!metaFoG.meta;
 
 // Is Container
 // Helper function to distinguish container group (a group of child fields)
-const isContainer = (metaFoG): boolean => isGroup(metaFoG) && (metaFoG.type === 'container' || typeof metaFoG.type === 'undefined');
+const isContainer = (metaFoG): boolean => isGroup(metaFoG) && (!metaFoG.type || metaFoG.type.toLowerCase() === 'container');
 
 // Add Missing Names
 // Helper function to add any missing 'name' properties to Fields and Groups using property's key, recursively
@@ -159,7 +163,7 @@ const addNameIfMissing = (metaFoG, key) => metaFoG.name ? metaFoG : addProp(meta
 const addNameToSelfAndChildren = ( [key, metaFoG] ) => {
 	metaFoG = addNameIfMissing(metaFoG, key);
 	if (isGroup(metaFoG)) {
-		metaFoG.meta = addMissingNames(metaFoG.meta); // Recursion
+		metaFoG.meta = Array.isArray(metaFoG.meta) ? Object.values(addMissingNames(metaFoG.meta)) : addMissingNames(metaFoG.meta); // Recursion
 	}
 	return [key, metaFoG];
 };
@@ -168,6 +172,19 @@ const addMissingNames = metaG => Object.entries(metaG)
 	.reduce((res, [key, val]) => addProp(res, key, val), {});
 
 
+// Add Missing Field-Specific Meta
+// Helper function to add any missing Field-Specific Metadata  (using models in dynaform/models), recursively
+// Checks the constrctor, which should NOT be a plain Object, but rather TextField, TextareaField, SelectField, etc.
+const add_FSM_IfMissing = metaFoG => metaFoG.constructor.name === 'Object' ? buildModeledFieldGroupMember(metaFoG) : metaFoG;
+const add_FSM_ToSelfAndChildren = ( [key, metaFoG] ) => {
+	metaFoG = add_FSM_IfMissing(metaFoG);
+	return [key, metaFoG];
+};
+const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
+	.map(add_FSM_ToSelfAndChildren)
+	.reduce((res, [key, val]) => addProp(res, key, val), {});
+
+
 // ---------------------------------------------------------------------------------------------------------------------
 // Exports
 // ---------------------------------------------------------------------------------------------------------------------