浏览代码

RepeatingFields appear to be working, and nothing else broken

Richard Knight 4 年之前
父节点
当前提交
d24376d9aa

+ 1 - 0
src/app/_mock/testfields.v15.ts

@@ -6,6 +6,7 @@ const model = {};
 
 const meta = {
 	repeatingField: {
+		label: 'Email Address',
 		type: 'text',
 		minRepeat: 1,
 		maxRepeat: 5,

+ 19 - 0
src/app/_mock/testfields.v8.ts

@@ -14,4 +14,23 @@ const meta = {
 	cbFormArray: { type: 'CheckbuttonGroup', showAllOrNone: 1 }
 };
 
+
+/*
+const model = {};
+
+const meta = {
+	cbFormArray: {
+		type: 'CheckbuttonGroup',
+		showAllOrNone: 1,
+		meta: [
+			{ label: 'A', value: 'A' },
+			{ value: 'B' },
+			{ value: 'C' },
+			{ value: 'D' },
+			{ value: 'E' }
+		]
+	}
+};
+*/
+
 export { model, meta };

+ 2 - 2
src/app/app.component.html

@@ -4,7 +4,7 @@
 			<h1>NgDynaform: Clarity Edition</h1>
 			<p>
 			<b>Dynamic Form Layout Module</b><br>
-			Load different tests by appending a query param to the URL <b>?test=N</b> (where N is a number between 1 and 12).<br>
+			Load different tests by appending a query param to the URL <b>?test=N</b> (where N is a number between 1 and 15).<br>
 			NOTE: Model set to update on change, but this can be set to blur or submit for less re-rendering.
 			</p>
 			<br>
@@ -35,7 +35,7 @@
 <ng-template #testTemplate_NOTWORKING let-control="control" let-meta="meta">
 
 	<div class="form-group clr-row" *ngIf="meta.type !== 'Container'; else recursiveDynaform" [ngClass]="getRowClass(meta.type)">
-		<label class="clr-col-sm-4" [for]="meta.name" style="color: purple'">{{ meta.label }}</label>
+		<label class="clr-col-sm-4" [for]="meta.name" style="color: purple">{{ meta.label }}</label>
 		<div class="clr-col-sm-8">
 			<ng-container dynafield [control]="control" [meta]="meta" (call)="handleCallback($event)"></ng-container>
 		</div>

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

@@ -19,6 +19,11 @@
 		</ng-container>
 
 		<ng-template #recursiveDynaform>
+			<ng-container *ngIf="isRepeatingField(meta)">
+				<ng-container *ngFor="let field of meta.meta; let i = index">
+					<ng-container *ngTemplateOutlet="dynafield; context: getRepeatingFieldTemplateContext(meta.name, i)"></ng-container>
+				</ng-container>
+			</ng-container>
 			<ng-container *ngIf="isRepeatingContainer(meta); else container">
 				<div *ngIf="meta.display === 'SINGLE'" class="clr-row dyna-rc-selector">
 					<div class="clr-col-sm-2 text-right dyna-rc-focus-block">
@@ -47,7 +52,7 @@
 						(click)="deleteRCMember(meta.name, i)">
 						<clr-icon shape="trash"></clr-icon>
 					</button>
-					<ng-container *ngTemplateOutlet="dynaform; context: getRCTemplateContext(meta.name, i)"></ng-container>
+					<ng-container *ngTemplateOutlet="dynaform; context: getRepeatingContainerTemplateContext(meta.name, i)"></ng-container>
 				</div>
 				<div *ngIf="meta.showAddControl && meta.display === 'ALL'" class="clr-row">
 					<div class="clr-col-sm-12">
@@ -82,6 +87,18 @@
 </ng-template>
 
 
+<!-- The DYNAFIELD template -->
+<ng-template let-control="control" let-meta="meta" #dynafield>
+	<div *ngIf="isField(meta); else recursiveDynaform" [ngClass]="getRowClass(control, meta)">
+		<ng-container dynafield
+			[control]="control"
+			[meta]="meta"
+			(call)="handleCallback($event)"
+		></ng-container>
+	</div>
+</ng-template>
+
+
 <!-- Display validation status if debugging and not nested -->
 <div *ngIf="debug && path.length === 0" [ngClass]="{
 		'alert-success' 	: formGroup.valid && formGroup.dirty,

+ 15 - 2
src/app/dynaform/dynaform.component.ts

@@ -133,7 +133,11 @@ export class DynaformComponent implements OnInit, OnChanges {
 	}
 
 	isField(meta: StringMap<any>): boolean {
-		return !meta.type.includes('Container');
+		return !meta.type.includes('Container') && meta.type !== 'RepeatingField';
+	}
+
+	isRepeatingField(meta: StringMap<any>): boolean {
+		return meta.type === 'RepeatingField';
 	}
 
 	isRepeatingContainer(meta: StringMap<any>): boolean {
@@ -147,7 +151,16 @@ export class DynaformComponent implements OnInit, OnChanges {
 		};
 	}
 
-	getRCTemplateContext(repeatingContainerName: string, index: number): DynarowContext {
+	getRepeatingFieldTemplateContext(repeatingFieldrName: string, index: number): DynarowContext {
+		const rfFormArray = this.formGroup.get(repeatingFieldrName) as FormArray;
+		const result = {
+			control: rfFormArray.at(index),
+			meta: this.formMetaData[repeatingFieldrName]['meta'][index]
+		};
+		return result;
+	}
+
+	getRepeatingContainerTemplateContext(repeatingContainerName: string, index: number): DynarowContext {
 		const rcFormArray = this.formGroup.get(repeatingContainerName) as FormArray;
 		const result = {
 			control: rcFormArray.at(index),

+ 28 - 0
src/app/dynaform/models/field.model.ts

@@ -212,6 +212,9 @@ class CheckboxField extends SimpleField {
 		if (typeof meta.value === 'undefined') {
 			this.value = this.default; // Get default from this class, not superclass
 		}
+		if (!meta.label) {
+			meta.label = unCamelCase(this.checkedValue.toString());
+		}
 	}
 }
 
@@ -253,6 +256,7 @@ class CheckboxGroup {
 	showAllOrNone?: boolean;
 	meta: CheckbuttonField[] | { [key: string]: CheckbuttonField };
 	constructor(groupmeta: any) {
+		console.log(groupmeta);
 		Object.assign(this, groupmeta);
 		if (typeof this.label === 'undefined') {
 			// If label is not supplied set it to the unCamelCased'n'Spaced name
@@ -260,6 +264,7 @@ class CheckboxGroup {
 			this.label = unCamelCase(this.name);
 		}
 		// Can render as a FormArray or FormGroup depending on input data
+		console.log('GMM', groupmeta.meta);
 		if (Array.isArray(groupmeta.meta)) {
 			const arrayMembers = groupmeta.meta;
 			this.meta = arrayMembers.map(cb => new CheckbuttonField(cb));
@@ -299,6 +304,28 @@ class DatepickerField extends SimpleField {
 	value: Date = new Date();
 }
 
+// ---------------------------------------------------------------------------------------------------------------------
+// Repeating Fields
+
+class RepeatingField<T> {
+	type = 'RepeatingField';
+	name: string;
+	label: string;
+	seed: StringMap<any>;
+	meta: T[]; // An array of fields of type T
+	minRepeat: number = 1;
+	maxRepeat: number = 10;
+	initialRepeat: number;
+	showAddControl:  boolean = true;
+	showDeleteControl: boolean = true;
+	constructor(repeatingFieldMeta: StringMap<any>) {
+		Object.assign(this, repeatingFieldMeta);
+		if (typeof this.label === 'undefined') {
+			this.label = unCamelCase(this.name);
+		}
+	}
+}
+
 // ---------------------------------------------------------------------------------------------------------------------
 // Containers
 
@@ -429,6 +456,7 @@ export {
 	CheckbuttonField, DropdownModifiedInputField, MultilineField,
 	CheckboxGroup, CheckbuttonGroup,
 	DatetimeField, DatepickerField,
+	RepeatingField,
 	Container, RepeatingContainer,
 	ButtonGroup, Heading, DisplayField
 };

+ 85 - 38
src/app/dynaform/services/_formdata-utils.ts

@@ -15,6 +15,7 @@
  * Variable names
  * --------------
  * metaF       = metadata for Field
+ * metaRF      = metadata for Repeating Field
  * metaG       = metadata for Group (possibly nested)
  * metaFoG     = metadata for Field or Group
  * rcMem       = Repeating Container member
@@ -25,6 +26,7 @@
 import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControl, AbstractControlOptions } from '@angular/forms';
 import { cloneDeep, omit, reduce } from 'lodash/fp';
 import * as fmdModels from '../models/field.model';
+import { meta } from '@mock/testfields.v15';
 
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -75,6 +77,22 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 			const metaFoG = metaG[key] || {};
 			const seed = isCon ? {} : containerSeed; // Container's don't seed themselves, only their children
 
+			/*
+			console.log('******************* BEFORE MODELLING *******************');
+			console.log(val);
+			console.log('IS REPEATING', isRepeating(val));
+			console.log('HAS META', hasMeta(val));
+			console.log('IS ARRAY', Array.isArray(val.meta));
+			console.log('MEMBER 1 DEFINED', !!val.meta[0]);
+			console.log('OBJECT VALUES .........', Object.values(val.meta[0]));
+			console.log('MEMBER 1 1st entry .........', Object.values(val.meta[0])[0]);
+			console.log('MEMBER 1 1st entry is SCALAR', isScalar(Object.values(val.meta[0])));
+			*/
+			/*
+			console.log('IS REP Field', isRepeatingField(val));
+			console.log('IS REP Container', isRepeatingContainer(val));
+			console.log('IS ORD Container', isContainer(val));
+			*/
 			if (isRepeatingContainer(val))
 			{
 				// We've got a Repeating Container
@@ -103,23 +121,9 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 			}
 			else // ----------------------------------------------
 			{
-				// We've got a Container or a Field
-				/*
-				const extra = isCon ?
-					{
-						...val,
-						meta: combineExtraMeta( // RECURSION
-							metaFoG.meta || {},
-							val['meta'],
-							createFromExtra,
-							val['seed'] || containerSeed // Inherit seeded data if this group's seed isn't set
-						)
-					}
-					:
-					isRepeatingField(val) ? [ val, val, val ] : val; // <----------! HERE WE BE WORKING!
-				*/
 				let extra: StringMap<any>;
 				if (isCon) {
+					// We've got a container
 					extra = {
 						...val,
 						meta: combineExtraMeta( // RECURSION
@@ -132,12 +136,19 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 				}
 				else // ----------------------------------------------
 				{
-					if (isRepeatingField(val)) {
+					if (val.minRepeat || val.maxRepeat || val.initialRepeat) {
+						// We've got a repeating field
+						const metaForFieldToRepeat = {
+							...containerSeed,
+							...omit(['seed', 'minRepeat', 'maxRepeat', 'initialRepeat', 'showAddControl', 'showDeleteControl'], val as StringMap<any>)
+						};
+						delete val.type;
 						extra = {
 							...val,
-							meta: [ val, val, val ]  // <----------! HERE WE BE WORKING!
+							meta: Array.from(Array(val.initialRepeat || 1).keys()).map((f, i) => ({ ...metaForFieldToRepeat, label: `${val.label || key} ${i + 1}` }))
 						}
 					} else {
+						// We've got a standard field
 						extra = val;
 					}
 				}
@@ -195,6 +206,9 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 		if (metaFoG.type) {
 			return metaFoG.type;
 		}
+		if (isRepeatingField(metaFoG)) {
+			return 'repeatingField';
+		}
 		if (isContainer(metaFoG)) {
 			return 'container';
 		}
@@ -206,7 +220,7 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 
 	const buildFieldClassName = (t: string): string => {
 		const start = t[0].toUpperCase() + t.slice(1);
-		if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
+		if (['RepeatingField', 'Container', 'RepeatingContainer', 'Heading'].includes(start) || t.slice(-5) === 'Group') {
 			return start;
 		}
 		return start + 'Field';
@@ -224,9 +238,19 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 	// Build Form Group Member
 	const buildModeledFieldGroupMember = (metaFoG) => {
 		const modeledGroupMember = buildModeledField(metaFoG);
-		if (isContainer(metaFoG)) {
+		
+		console.log('------------- DEBUGGING -------------');
+		console.log(modeledGroupMember);
+		console.log('IS REP Field', isRepeatingField(modeledGroupMember));
+		console.log('IS REP Container', isRepeatingContainer(modeledGroupMember));
+		console.log('IS ORD Container', isContainer(modeledGroupMember));
+		console.log(modeledGroupMember);
+		
+		if (isRepeatingField(modeledGroupMember)) {
+			modeledGroupMember.meta = modeledGroupMember.meta.map(metaF => buildModeledField(metaF));
+		} else if (isContainer(modeledGroupMember)) {
 			modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
-		} else if (isRepeatingContainer(metaFoG)) {
+		} else if (isRepeatingContainer(modeledGroupMember)) {
 			modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
 			modeledGroupMember.__containerTemplate = {
 				...modeledGroupMember.meta[0],
@@ -245,7 +269,10 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 		reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
 	const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
 
-	return buildFieldSpecificMeta(metaG);
+	// DEBUGGING
+	console.log('buildFieldSpecificMetaInClosure', metaG, context);
+
+	return buildFieldSpecificMeta(metaG); // RUN CODE
 }
 
 
@@ -291,7 +318,7 @@ const prependParentPathRecursive = (parentPath: string, obj: StringMap<any>) =>
 
 const _extractFieldMapping = ( [key, metaFoG] ) => {
 	let source;
-	if (isGroup(metaFoG)) {
+	if (hasMeta(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)
@@ -379,7 +406,6 @@ const extractFieldMappings = (metaG, parentPath = '') => Object.entries(metaG)
 // returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
 // ---------------------------------------------------------------------------------------------------------------------
 
-
 // TODO: In progress: elegantly adding validators at FormGroup and FormArray levels
 // Working, but code needs a rework
 
@@ -399,9 +425,9 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 		if (!metaFoG || !(metaFoG.validators || metaFoG.asyncValidators)) {
 			return undefined;
 		}
-		// console.log(metaFoG);
+		console.log(metaFoG);
 		const res = buildValidators(metaFoG);
-		// console.log(res);
+		console.log(res);
 		return res;
 	}
 	const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
@@ -416,7 +442,7 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 
 	// Build Form Group Member
 	// Builds a FormControl, FormArray or another FormGroup - which in turn can contain any of these
-	const buildFormGroupMember = metaFoG => isGroup(metaFoG) ?
+	const buildFormGroupMember = metaFoG => hasMeta(metaFoG) ?
 		(isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta, metaFoG) : _buildFormGroup(metaFoG.meta, metaFoG)) : // TODO: STINKY! REWORK with 1 param
 		buildFormControl(metaFoG);
 
@@ -688,25 +714,46 @@ const undefinedNullOrScalar = val => {
 // Add Property to object, returning updated object
 const addProp = (obj, key, val) => { obj[key] = val; return obj; };
 
+// Has Meta
+// Helper function to distinguish a group from a simple field  - where a group is a container os repeating container,  or repeating field * AFTER* modelling
+const hasMeta = (metaFoG): boolean => !!metaFoG.meta;
+
+// Is Repeating
+const isRepeating = (metaFoG): boolean => !!(metaFoG.minRepeat || metaFoG.maxRepeat || metaFoG.initialRepeat);
+
 // Is Repeating Field
 // Helper function to distinguish a repeating field (a field that can be repeated 1...N times)
-const isRepeatingField = (metaF): boolean => metaF.minRepeat || metaF.maxRepeat || metaF.initialRepeat;
-
-// Is Group
-// Helper function to distinguish group from field
-const isGroup = (metaFoG): boolean => !!metaFoG.meta;
+const isRepeatingField = (metaRF): boolean => metaRF.type && metaRF.type.toLowerCase() === 'repeatingfield'
+	|| (
+		!metaRF.type
+		&& isRepeating(metaRF)
+		&& (
+			!hasMeta(metaRF)  	|| hasMeta (metaRF)
+								&& Array.isArray(metaRF.meta)
+								&& metaRF.meta[0]
+								&& isScalar(Object.values(metaRF.meta[0])[0])
+		)  
+	);
 
 // Is Container
 // Helper function to distinguish container group (a group of child fields)
-const isContainer = (metaFoG): boolean => isGroup(metaFoG)
-	&& !Array.isArray(metaFoG.meta)
-	&& (!metaFoG.type || metaFoG.type.toLowerCase() === 'container');
+const isContainer = (metaFoG): boolean => metaFoG.type && metaFoG.type.toLowerCase() === 'container'
+	|| (
+		!metaFoG.type
+		&& hasMeta(metaFoG)
+		&& !Array.isArray(metaFoG.meta)
+	);
 
 // Is Repeating Container
 // Helper function to distinguish a repeating container group (a group of child fields that can be repeated 1...N times)
-const isRepeatingContainer = (metaFoG): boolean => isGroup(metaFoG)
-	&& Array.isArray(metaFoG.meta)
-	&& (!metaFoG.type || metaFoG.type.toLowerCase() === 'repeatingContainer');
+const isRepeatingContainer = (metaFoG): boolean => metaFoG.type && metaFoG.type.toLowerCase() === 'repeatingcontainer'
+	|| (
+		!metaFoG.type
+		&& isRepeating(metaFoG)
+		&& hasMeta(metaFoG)
+		&& Array.isArray(metaFoG.meta) && metaFoG.meta[0]
+		&& !isScalar(Object.values(metaFoG.meta[0])[0])
+	);
 
 // Add Missing Names
 // Helper function to add any missing 'name' properties to Fields and Groups using property's key, recursively
@@ -714,7 +761,7 @@ const isRepeatingContainer = (metaFoG): boolean => isGroup(metaFoG)
 const addNameIfMissing = (metaFoG, key) => metaFoG.name ? metaFoG : addProp(metaFoG, 'name', key);
 const addNameToSelfAndChildren = ( [key, metaFoG] ) => {
 	metaFoG = addNameIfMissing(metaFoG, key);
-	if (isGroup(metaFoG) && !isRepeatingContainer(metaFoG)) {
+	if (hasMeta(metaFoG) && !isRepeatingContainer(metaFoG)) {
 		metaFoG.meta = isArray(metaFoG.meta) ? Object.values(addMissingNames(metaFoG.meta)) : addMissingNames(metaFoG.meta); // Recursion
 	}
 	return [key, metaFoG];