Forráskód Böngészése

Early support for component type mutation

Richard Knight 4 éve
szülő
commit
f02a4e8088

+ 1 - 1
README.md

@@ -234,7 +234,7 @@ e.g. radio, horizontal.
           address1: {},
           address2: {},
           city: {},
-          postcode: { class: 'short-field' }
+          postcode: { class: 'multicolor' }
         }
       }
 		}

+ 2 - 2
src/app/_mock/testfields.v11.ts

@@ -7,11 +7,11 @@ const model = {};
 const meta = {
 	container: {
 		label: 'Fields should inherit seeded meta from the container',
-		seed: { class: 'short-field', value: 5 },
+		seed: { class: 'multicolor', value: 5 },
 		meta: {
 			a: {},
 			b: {},
-			c: { value: 6, class: 'short-field' }, 
+			c: { value: 6, class: 'multicolor' }, 
 			d: {},
 			nestedContiner: {
 				label: 'More deeply nested container',

+ 1 - 1
src/app/_mock/testfields.v12.ts

@@ -23,7 +23,7 @@ const model = {
 const meta = {
 	repeating: {
 		label: 'Repeating Group',
-		seed: { class: 'short-field' },
+		seed: { class: 'multicolor' },
 		minRepeat: 1,
 		maxRepeat: 5,
 		initialRepeat: 3,

+ 1 - 1
src/app/_mock/testfields.v13.ts

@@ -19,7 +19,7 @@ const model = {
 const meta = {
 	repeating: {
 		label: 'Repeating Group',
-		seed: { class: 'short-field' },
+		seed: { class: 'multicolor' },
 		minRepeat: 1,
 		maxRepeat: 5,
 		initialRepeat: 3,

+ 1 - 1
src/app/_mock/testfields.v14.ts

@@ -19,7 +19,7 @@ const model = {
 const meta = {
 	repeating: {
 		label: 'Repeating Group',
-		seed: { class: 'short-field' },
+		seed: { class: 'multicolor' },
 		minRepeat: 1,
 		maxRepeat: 5,
 		initialRepeat: 3,

+ 27 - 52
src/app/_mock/testfields.v17.ts

@@ -1,5 +1,5 @@
 // ---------------------------------------------------------------------------------------------------------------------
-// TESTS: container groups? again
+// TESTS: Modification of Meta
 // ---------------------------------------------------------------------------------------------------------------------
 
 import { Validators as V } from '@angular/forms';
@@ -7,57 +7,32 @@ import { Validators as V } from '@angular/forms';
 const model = {};
 
 const meta = {
-	general: {
-		label: 'Genaral Details',
-		source: '/',
-		meta: {
-			name: { validators: V.required },
-			description: { type: 'textarea' },
-			duration: { },
-			unit: { label: 'Time Unit' }
-		}
-	},
-	email: {
-		label: 'Email Configuration',
-		source: '/',
-		meta: {
-			emailTo: { label: 'To', validators: [ V.required, V.email ] },
-			emailCc: { 
-				minRepeat: 1,
-				maxRepeat: 5,
-				initialRepeat: 1,
-				showAddControl: true,
-				showDeleteControl: true,
-				validators: V.email
-			},
-			emailBcc: { 
-				minRepeat: 1,
-				maxRepeat: 5,
-				initialRepeat: 1,
-				showAddControl: true,
-				showDeleteControl: true,
-				validators: V.email
-			},
-			subject: { validators: V.required },
-			message: { type: 'textarea' }
-		}
-	},
-	report: {
-		label: 'Report Configuration',
-		source: '/',
-		meta: {
-			table: {
-				type: 'select',
-				options: [ 'AAA', 'BBB' ],
-				validators: V.required
-			},
-			columns: {
-				source: '/',
-				type: 'checkboxGroup',
-				meta: ['Ab', 'Cd', 'Ef', 'Ghi']
-			}
-		}
+	testField: {
+		type: 'text',
+		placeholder: 'Does it work?'
+		// meta: ['Ab', 'Cd', 'Ef', 'Ghi']
 	}
 };
 
-export { model, meta };
+/*
+const meta2 = {
+	testField: {
+		type: 'text',
+		placeholder: 'WOO HOO!'
+		// meta: ['Ab', 'Cd', 'Ef', 'Ghi']
+	}
+}
+*/
+
+const meta2 = {
+	testField: {
+		type: 'select',
+		placeholder: 'WOO HOO!',
+		class: 'multicolor',
+		options: ['WOO HOO!', 'Yabba Dabba Doo!']
+		// meta: ['Ab', 'Cd', 'Ef', 'Ghi']
+	}
+}
+
+export { model, meta, meta2 };
+

+ 17 - 0
src/app/_mock/testfields.v18.ts

@@ -0,0 +1,17 @@
+// ---------------------------------------------------------------------------------------------------------------------
+// TESTS: Modification of CheckBoxGroups
+// ---------------------------------------------------------------------------------------------------------------------
+
+import { Validators as V } from '@angular/forms';
+
+const model = {};
+
+const meta = {
+	columns: {
+		source: '/',
+		type: 'checkboxGroup',
+		meta: ['Ab', 'Cd', 'Ef', 'Ghi']
+	}
+};
+
+export { model, meta };

+ 13 - 1
src/app/app.component.ts

@@ -49,7 +49,8 @@ export class AppComponent implements OnInit, OnChanges {
 
 		// Optionally supply the test to run in the query string e.g. ?test=3
 		const testcase = parseInt(new URLSearchParams(document.location.search).get('test'), 10) || defatltTest;
-		const { model, meta } = testdata[testcase - 1];
+		// @ts-ignore: meta2 does not always exist
+		const { model, meta, meta2 } = testdata[testcase - 1];
 
 		console.log('%c *** TEST DATA *** ', this.hCssRed);
 		console.log('Model', model);
@@ -93,6 +94,10 @@ export class AppComponent implements OnInit, OnChanges {
 			'SAYHELLO': this.sayHello,
 			'SAYCHEESE': this.sayCheese
 		}, this);
+
+		if (testcase >= 17) {
+			setTimeout(() => this.changeMeta(meta2), 2000);
+		}
 	}
 
 	ngOnChanges() {
@@ -110,5 +115,12 @@ export class AppComponent implements OnInit, OnChanges {
 	sayCheese() {
 		alert('CHEESE');
 	}
+	
+		
+	changeMeta(newMeta) {
+		const dynaformdata = this.dynaform.build({}, newMeta, true);
+		const { form, meta } = dynaformdata;
+		this.meta = meta;
+	}
 }
 

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

@@ -7,4 +7,4 @@
 		(blur)="updateValue()"
 	>
 </ng-container>
-<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
+<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>

+ 32 - 3
src/app/dynaform/directives/dynafield.directive.ts

@@ -68,6 +68,7 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 	call: EventEmitter<string> = new EventEmitter<string>();
 
 	component: ComponentRef<IFFC|FFCCustom>;
+	type: string;
 	_control;
 
 	constructor(
@@ -82,6 +83,7 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 
 	ngOnInit() {
 		const type = componentType(this.meta.type);
+		this.type = type;
 		if (!formFieldComponents[type]) {
 			const validComponentTypes = Object.keys(formFieldComponents).join(', ');
 			throw new Error(
@@ -145,14 +147,37 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 	}
 
 	ngOnChanges() {
-		// We won't support mutating components (e.g. Text --> Select) at this stage,
+		// NOW UNDERWAY (see below): We won't support mutating components (e.g. Text --> Select) at this stage,
 		// but will support mutating an instantiated components metadata
 		// As the component is created in ngOnInt this does nothing in the run before ngOnInit, but responds to later input changes
 		if (this.component) {
-			const { type, class: cssClass, id: cssId } = this.meta;
+			let { control, meta } = this;
+			const type = componentType(meta.type);
+			const { class: cssClass, id: cssId, disabled } = meta;
+			
+			// EARLY SUPPORT FOR COMPONENT TYPE MUTATION! REFACTOR into shared functions to avoid repitition with code in ngOnInit
+			if (type !== this.type) {
+				console.log('TYPE CHANGED', type);
+				this.type = type;
+				this.container.remove(0);
+				const componentFactory = this.resolver.resolveComponentFactory<IFFC>(formFieldComponents[type]);
+				this.component = this.container.createComponent(componentFactory);
+				const instance = this.component.instance;
+
+				// Check whether it's disabled, then set its FormControl and metadata
+				if (disabled) {
+					this.control.reset({ value: this.control.value, disabled: true });
+				}
+
+				// TODO: Change Update Strategy if necessary
+
+				instance.control = control;
+				instance.meta = meta;
+			}
+
 			this.setCssId(cssId);
 			this.setCssClasses(type, cssClass);
-			this.component.instance.meta = this.meta;
+			this.component.instance.meta = meta;
 		}
 	}
 
@@ -165,6 +190,10 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 		}
 	}
 
+	insertComponent() {
+		
+	}
+
 	// ---------------------------------------
 	// Override methods / getters in NgControl
 

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

@@ -17,13 +17,13 @@
 		<ng-template #recursiveDynaform>
 			<ng-container [ngSwitch]="meta.type">
 
-				<div *ngSwitchCase="'RepeatingField'" class="dyna-rf-container">
+				<div *ngSwitchCase="'RepeatingField'" class="dyna-rf-container" [ngClass]="getEmbeddedDynaformClasses(meta)">
 					<div *ngFor="let field of meta.meta; let i = index" class="dyna-rf-field">
 						<button *ngIf="meta.showDeleteControl"
 							class="dyna-rep-btn-delete"
 							[disabled]="!deleteAllowed(meta.name)"
 							(click)="deleteRepeatingMember(meta.name, i)">
-							<clr-icon shape="trash"></clr-icon>
+							<clr-icon shape="trash"></clr-icon> {{ meta.deleteControlLabel }}
 						</button>
 						<ng-container *ngTemplateOutlet="dynafield; context: getRepeatingTemplateContext(meta.name, i)"></ng-container>
 					</div>
@@ -32,12 +32,12 @@
 							class="btn btn-sm btn-icon btn-outline-success dyna-rep-btn-add"
 							[disabled]="!addAllowed(meta.name)"
 							(click)="addRepeatingFieldMember(meta.name)">
-							<clr-icon shape="plus"></clr-icon>
+							<clr-icon shape="plus"></clr-icon> {{ meta.addControlLabel }}
 						</button>
 					</div>
 				</div>
 	
-				<ng-container *ngSwitchCase="'RepeatingContainer'">
+				<div *ngSwitchCase="'RepeatingContainer'" class="dyna-rc-container" [ngClass]="getEmbeddedDynaformClasses(meta)">
 					<div *ngIf="meta.display === 'SINGLE'" class="clr-row dyna-rc-selector">
 						<div class="clr-col-sm-2 text-right dyna-rc-focus-block">
 							<b>FOCUS &gt;</b>
@@ -53,17 +53,17 @@
 								class="btn btn-sm btn-icon btn-outline-success dyna-rep-btn-add"
 								[disabled]="!addAllowed(meta.name)"
 								(click)="addRepeatingContainerMember(meta.name)">
-								<clr-icon shape="plus"></clr-icon>
+								<clr-icon shape="plus"></clr-icon> {{ meta.addControlLabel }}
 							</button>
 						</div>
 					</div>
-					<div *ngFor="let container of meta.meta; let i = index" class="dyna-rc-container" [ngClass]="{ 'dyna-rc-display-all': meta.display === 'ALL' }">
+					<div *ngFor="let container of meta.meta; let i = index" class="dyna-rc-member-container" [ngClass]="{ 'dyna-rc-display-all': meta.display === 'ALL' }">
 						<button *ngIf="meta.showDeleteControl"
 							class="btn btn-sm btn-icon btn-outline-danger dyna-rep-btn-delete"
-							[ngClass]="{ 'dyna-hidden': !meta.meta[i].focussed }"
+							[ngClass]="{ 'dyna-hidden': meta.display === 'SINGLE' && !meta.meta[i].focussed }"
 							[disabled]="!deleteAllowed(meta.name)"
 							(click)="deleteRepeatingMember(meta.name, i)">
-							<clr-icon shape="trash"></clr-icon>
+							<clr-icon shape="trash"></clr-icon> {{ meta.deleteControlLabel }}
 						</button>
 						<ng-container *ngTemplateOutlet="dynaform; context: getRepeatingTemplateContext(meta.name, i)"></ng-container>
 					</div>
@@ -72,10 +72,10 @@
 							class="btn btn-sm btn-icon btn-outline-success dyna-rep-btn-add"
 							[disabled]="!addAllowed(meta.name)"
 							(click)="addRepeatingContainerMember(meta.name)">
-							<clr-icon shape="plus"></clr-icon>
+							<clr-icon shape="plus"></clr-icon> {{ meta.addControlLabel }}
 						</button>
 					</div>
-				</ng-container>
+				</div>
 
 				<ng-container *ngSwitchCase="'Container'">
 					<ng-container *ngTemplateOutlet="dynaform; context: { control: control, meta: meta }"></ng-container>

+ 9 - 7
src/app/dynaform/dynaform.component.ts

@@ -65,6 +65,7 @@ export class DynaformComponent implements OnInit, OnChanges {
 	// Colours for CSS in console
 	conRed = 'color: white; background-color: maroon; font-weight: bold;';
 	conGreen = 'color: white; background-color: green; font-weight: bold;';
+	conOlive = 'color: white; background-color: olive; font-weight: bold;';
 
 	constructor(
 		@Optional() private cc: ControlContainer
@@ -76,13 +77,14 @@ export class DynaformComponent implements OnInit, OnChanges {
 
 	ngOnChanges() {
 		// Triggered when inputs change
-		// console.log('Dynaform ngOnChanges');
+		console.log('%c *** DynaformChange *** ', this.conOlive);
+		console.log(this.formMetaData);
 		// Get the formGroup from the formGroupName if necessary
 		if (!this.formGroup && this.formGroupName) {
 			this.formGroup = this.cc.control as FormGroup; // Get theFormGroup from the injected ControlContainer
 		}
 		if (!this.formGroup) {
-			throw new Error('Dynaform Component initialised without [formGroup] or formGroupName');
+			throw new Error(`Dynaform Component initialised without [formGroup] or cant't find formGroup from formGroupName ${this.formGroupName}`);
 		}
 		if (typeof this.formMetaData !== 'object') {
 			throw new Error('Dynaform: [meta] should be an object');
@@ -132,8 +134,8 @@ export class DynaformComponent implements OnInit, OnChanges {
 		return path;
 	}
 
-	getEmbeddedDynaformClasses(meta): StringMap<boolean> {
-		let ngClassObj = { 'dyna-hidden': !meta.focussed };
+	getEmbeddedDynaformClasses(meta: StringMap<any>): StringMap<boolean> {
+		let ngClassObj = {};
 		if (Array.isArray(meta.class)) {
 			ngClassObj = (meta.class as string[]).reduce((acc, className) => (acc[className] = true, acc), ngClassObj);
 		} else if (typeof meta.class === 'string' && meta.class) {
@@ -205,7 +207,7 @@ export class DynaformComponent implements OnInit, OnChanges {
 		}
 		// (2) Add metadata for new container member
 		const rfMeta = this.formMetaData[name];
-		const fieldTemplate = cloneDeep(rfMeta.__fieldTemplate);
+		const fieldTemplate = cloneDeep(rfMeta.__template);
 		const i = this.formMetaData[name].meta.length;
 		fieldTemplate.name = `group${i+1}`; // CHECK
 		rfMeta.meta.push(fieldTemplate);
@@ -222,7 +224,7 @@ export class DynaformComponent implements OnInit, OnChanges {
 		}
 		// (2) Add metadata for new container member
 		const rcMeta = this.formMetaData[name];
-		const containerTemplate = cloneDeep(rcMeta.__containerTemplate);
+		const containerTemplate = cloneDeep(rcMeta.__template);
 		const i = this.formMetaData[name].meta.length;
 		containerTemplate.name = `group${i+1}`;
 		rcMeta.meta.push(containerTemplate);
@@ -281,7 +283,7 @@ export class DynaformComponent implements OnInit, OnChanges {
 		this.call.emit(fnId);
 	}
 
-	private getContolKeysCSVFromMetadata(metadata): string {
+	private getContolKeysCSVFromMetadata(metadata: StringMap<any>): string {
 		// Return CSV of control keys in current nesting-level's metadata,
 		// excluding metadata points that don't create FormControls, FromGroups or FormArrays
 		// (identified by their 'noFormControl' flag)

+ 5 - 1
src/app/dynaform/models/field.model.ts

@@ -320,6 +320,8 @@ class RepeatingField<T> {
 	initialRepeat: number = 1;
 	showAddControl:  boolean = true;
 	showDeleteControl: boolean = true;
+	addControlLabel: string = '';
+	deleteControlLabel: string = '';
 	constructor(repeatingFieldMeta: StringMap<any>) {
 		Object.assign(this, repeatingFieldMeta);
 		if (typeof this.label === 'undefined') {
@@ -365,8 +367,10 @@ class RepeatingContainer {
 	initialRepeat: number = 1;
 	showAddControl:  boolean = true;
 	showDeleteControl: boolean = true;
+	addControlLabel: string = '';
+	deleteControlLabel: string = '';
 	primaryField: string = '';
-	display: string = 'SINGLE'; // Display strategy to use  - ALL or SINGLE - All at once, or one at a time (with a switcher)
+	display: string = 'ALL'; // Display strategy to use  - ALL or SINGLE - All at once, or one at a time (with a switcher)
 	constructor(containerMeta: StringMap<any>) {
 		Object.assign(this, containerMeta);
 		if (typeof this.label === 'undefined') {

+ 36 - 36
src/app/dynaform/services/_formdata-utils.ts

@@ -26,7 +26,6 @@
 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.v1';
 
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -77,25 +76,25 @@ 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));
-			*/
+			// console.log('******************* BEFORE MODELLING *******************');
+			// console.log(key);
+			// 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] || null));
+			// 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
+				metaFoG.type = 'RepeatingContainer';
+				const extraMetaTemplate = Array.isArray(val.meta) ? val.meta[0] || {} : val.meta;
 				const baseObjWithAllKeys = getRCBaseObjectWithAllKeys(metaFoG, val, createFromExtra);
 				metaFoG.meta = generateRepeatedGroup(metaFoG, val, baseObjWithAllKeys);
 				const extra = {
@@ -103,7 +102,7 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 					meta: metaFoG.meta.map(
 						rgMem => combineExtraMeta(
 							rgMem.meta,
-							val['meta'][0],
+							extraMetaTemplate,
 							createFromExtra,
 							val['seed'] || containerSeed
 						)
@@ -111,11 +110,11 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 				};
 				combinedMeta[key] = combineMetaForField(metaFoG, {}, extra);
 
-				// Stash a 'conbtainer template' for adding extra containers to the repeating container
-				combinedMeta[key].__containerTemplate = combineExtraMeta(
+				// Stash a 'container template' for adding extra containers to the repeating container
+				combinedMeta[key].__template = combineExtraMeta(
 					cloneDeep(baseObjWithAllKeys),
-					val['meta'][0],
-					false,
+					extraMetaTemplate,
+					createFromExtra,
 					val['seed'] || containerSeed
 				);
 			}
@@ -124,11 +123,12 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 				let extra: StringMap<any>;
 				if (isCon) {
 					// We've got a container
+					metaFoG.type = 'Container';
 					extra = {
 						...val,
 						meta: combineExtraMeta( // RECURSION
 							metaFoG.meta || {},
-							val['meta'],
+							val.meta,
 							createFromExtra,
 							val['seed'] || containerSeed // Inherit seeded data if this group's seed isn't set
 						)
@@ -138,6 +138,7 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 				{
 					if (isRepeating(val)) {
 						// We've got a repeating field
+						metaFoG.type = 'RepeatingField';
 						const metaForFieldToRepeat = {
 							...containerSeed,
 							...omit(['seed', 'minRepeat', 'maxRepeat', 'initialRepeat', 'showAddControl', 'showDeleteControl'], val as StringMap<any>),
@@ -147,7 +148,7 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 						extra = {
 							...val,
 							meta: Array.from(Array(val.initialRepeat || 1).keys()).map((f, i) => metaForFieldToRepeat),
-							__fieldTemplate: metaForFieldToRepeat
+							__template: metaForFieldToRepeat
 						}
 					} else {
 						// We've got a standard field
@@ -171,14 +172,15 @@ const generateRepeatedGroup = (metaFoG, extraMeta, baseObjWithAllKeys): StringMa
 	const repeatInAutoMeta = Array.isArray(metaFoG.meta) ? metaFoG.meta.length : 0;
 	const repeatInExtraMeta = extraMeta['initialRepeat'] || extraMeta['minRepeat'];
 	const repeat = Math.max(repeatInAutoMeta, repeatInExtraMeta);
-
-	metaFoG.meta = metaFoG.meta.map( rcMem => ({ ...rcMem, meta: { ...baseObjWithAllKeys, ...rcMem.meta } }) ); // Add extra keys to model meta
+	// console.log('generateRepeatedGroup');
+	// console.log(metaFoG, extraMeta, baseObjWithAllKeys);
+	metaFoG.meta = (metaFoG.meta || []).map( rcMem => ({ ...rcMem, meta: { ...baseObjWithAllKeys, ...rcMem.meta } }) ); // Add extra keys to model meta
 
 	// Extend repeated group from model (if any) to correct length, and add any missing names
 	const repeatedGroup = repeatInAutoMeta ?
 		[ ...metaFoG.meta, ...Array(repeat - repeatInAutoMeta).fill({ meta: baseObjWithAllKeys }) ] :
 		Array(repeat).fill({ meta: baseObjWithAllKeys });
-	const fullyNamedRepeatedGroup = repeatedGroup.map((rgMem, i) => rgMem.name ? rgMem : { name: `group${i + 1}`, ...rgMem });
+	const fullyNamedRepeatedGroup = repeatedGroup.map((rgMem, i) => rgMem.name ? { ...rgMem } : { name: `group${i + 1}`, ...rgMem });
 	return fullyNamedRepeatedGroup;
 }
 
@@ -192,7 +194,6 @@ const getRCBaseObjectWithAllKeys = (metaFoG, extraMeta, createFromExtra = false)
 	return baseObjWithAllKeys;
 }
 
-
 // ---------------------------------------------------------------------------------------------------------------------
 // Build Form-Field-Type-Specific Metadata (using the field models in dynaform/models)
 // ---------------------------------------------------------------------------------------------------------------------
@@ -249,16 +250,15 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 		*/
 		if (isRepeatingField(modeledGroupMember)) {
 			modeledGroupMember.meta = modeledGroupMember.meta.map(metaF => buildModeledField(metaF));
-			modeledGroupMember.__fieldTemplate = buildModeledField(modeledGroupMember.__fieldTemplate);
+			modeledGroupMember.__template = buildModeledField(modeledGroupMember.__template);
 		} else if (isContainer(modeledGroupMember)) {
 			modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
 		} else if (isRepeatingContainer(modeledGroupMember)) {
 			modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
-			modeledGroupMember.__containerTemplate = {
-				...modeledGroupMember.meta[0],
-				meta: _buildFieldSpecificMeta(modeledGroupMember.__containerTemplate),
-				name: '__containerTemplate',
-				button: ''
+			modeledGroupMember.__template = {
+				...modeledGroupMember.meta[0] || {},
+				meta: _buildFieldSpecificMeta(modeledGroupMember.__template),
+				name: '__template'
 			};
 		}
 		return modeledGroupMember;
@@ -317,7 +317,7 @@ const prependParentPathRecursive = (parentPath: string, obj: StringMap<any>) =>
 
 const _extractFieldMapping = ( [key, metaFoG] ) => {
 	let source;
-	if (hasMeta(metaFoG)) {
+	if (hasMeta(metaFoG) && !Array.isArray(metaFoG.meta)) { // If it has non-array meta (note, we patch entire arrays for array meta)
 		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)
@@ -763,7 +763,7 @@ const addNameToSelfAndChildren = ( [key, metaFoG] ) => {
 	metaFoG = addNameIfMissing(metaFoG, key);
 	if (hasMeta(metaFoG) && !isRepeatingContainer(metaFoG)) {
 		if (metaFoG.meta.length && metaFoG.meta.every(isScalar)) {
-			metaFoG.meta = metaFoG.meta.map(val => ({ name: val, value: val }));
+			metaFoG.meta = metaFoG.meta.map(val => ({ name: val, value: val, label: val.replace(/_/g, ' ') }));
 		} else {
 			metaFoG.meta = isArray(metaFoG.meta) ? Object.values(addMissingNames(metaFoG.meta)) : addMissingNames(metaFoG.meta); // Recursion
 		}

+ 108 - 1
src/app/dynaform/services/dynaform.service.ts

@@ -98,7 +98,7 @@
  */
 
 import { Injectable, ComponentRef } from '@angular/core';
-import { FormBuilder, FormGroup } from '@angular/forms';
+import { FormBuilder, FormGroup, FormArray } from '@angular/forms';
 import { SuperForm } from 'angular-super-validator';
 import { ModelMapperService } from './model-mapper.service';
 
@@ -111,8 +111,12 @@ import {
 	generateNewModel, updateMeta
 } from './_formdata-utils';
 
+import { buildFormControl } from './_formdata-utils'; // MAY NOT BE NEEDED AFTER REFACTORING FormArrayLengthening logic to formdata-utils
+
 import { Option } from './../models/field.model';
 
+import { get  as _get, cloneDeep } from 'lodash/fp';
+
 
 export interface IFormAndMeta {
 	form: FormGroup;
@@ -189,6 +193,109 @@ export class DynaformService {
 	updateForm(newModel: StringMap<any>, form?: FormGroup, meta?: StringMap<any>): void {
 		const mapping = extractFieldMappings(meta || this.meta); // Memoize
 		const mappedModel = this.modelMapper.reverseMap(newModel, mapping);
+
+		meta = meta || this.meta;
+		form = form || this.form;
+
+		// When updating with a model that contains RepeatedFields or RepeatedContainers
+		// adjust the lengths of the displayed fields or containers appropiately
+		function findArraysRecursive(model, path = []) {
+			Object.entries(model).forEach(([key, val]) => {
+				if (Array.isArray(val)) {
+					// console.log('---------------------------');
+					// console.log([...path, key].join('.'));
+					const concreteMeta = getConcreteMetaForModelPath([ ...path, key ]);
+					const templateMeta = getTemplateMetaForModelPath([ ...path, key ]);
+					if (!templateMeta) {
+						return;
+					}
+					if (templateMeta.type === 'RepeatingField' || templateMeta.type === 'RepeatingContainer') {
+						// console.log(concreteMeta);
+						// console.log(templateMeta);
+						const delta = val.length - concreteMeta.meta.length;
+						// console.log('delta', delta);
+						const formArray = form.get([ ...path, key]) as FormArray;
+						if (delta > 0) {
+							// Array length in model exceeds length of corresponding metadata array
+							// console.log(templateMeta);
+							const desiredLength = Math.min(val.length, templateMeta.maxRepeat);
+							let lengthenBy = desiredLength - concreteMeta.meta.length;
+							// console.log('desiredLength', desiredLength);
+							// console.log('lengthenBy', lengthenBy);
+							const templateKey = concreteMeta.type === 'RepeatingField' ? '__template' : '__template';
+							const template = concreteMeta[templateKey];
+							concreteMeta.meta = [ ...concreteMeta.meta, ...Array(lengthenBy).fill(null).map(() => cloneDeep(template)) ];
+							while (lengthenBy--) {
+								switch(concreteMeta.type) {
+									case 'RepeatingField':
+										const newFormControl = buildFormControl(concreteMeta.__template);
+										formArray.push(newFormControl);
+										break;
+									case 'RepeatingContainer':
+										const buildFormGroup = buildFormGroupFunctionFactory(new FormBuilder());
+										const newFormGroup = buildFormGroup(concreteMeta.__template.meta);
+										formArray.push(newFormGroup);
+										break;
+								}
+							}
+						} else if (delta < 0) {
+							// Array length in model is shorter that length of corresponding metadata array
+							const desiredLength = Math.max(val.length, templateMeta.minRepeat);
+							let shortenBy = concreteMeta.meta.length - desiredLength;
+							// console.log('desiredLength', desiredLength);
+							// console.log('shortenBy', shortenBy, 'length', concreteMeta.meta.length);
+							concreteMeta.meta.length = desiredLength; // Truncate the array by setting the length
+							while (shortenBy--) {
+								formArray.removeAt(formArray.length - 1);
+							}
+						}
+					}
+					if (templateMeta.type === 'RepeatingContainer') {
+						// Does the value contain nested array values for RepeatingFields or RepeatingContainers?
+						val.map((v, i) => findArraysRecursive(v, [ ...path, key, i]));
+					}
+				} else if (val && typeof val === 'object') {
+					findArraysRecursive(val, [ ...path, key ]);
+				}
+			});
+		}
+		findArraysRecursive(mappedModel);
+
+		function getConcreteMetaForModelPath(path: string | string[]): StringMap<any> {
+			const _path = typeof path === 'string' ? path.split('.') : path;
+			// const deepMetaPath = _path.join('.meta.').replace(/\.\d+$/, '');
+			const deepMetaPath = _path.join('.meta.');
+			// console.log(deepMetaPath);
+			return _get(deepMetaPath, meta);
+		}
+
+		function getTemplateMetaForModelPath(path: string | string[]): StringMap<any> {
+			const _path = typeof path === 'string' ? path.split('.') : path;
+			const deepMetaTemplatePath = _path.join('.meta.').replace(/\.meta\.\d+/g, '.__template');
+			// console.log(deepMetaTemplatePath);
+			return _get(deepMetaTemplatePath, meta);
+		}
+
+		// console.log('*************************************');
+		// console.log(meta.report);
+		// console.log(getConcreteMetaForModelPath('report.clauses.0'));
+		// console.log(getTemplateMetaForModelPath('report.clauses.0'));
+		// console.log(getConcreteMetaForModelPath('report.clauses.0.subClauses'));
+		// console.log(getTemplateMetaForModelPath('report.clauses.0.subClauses'));
+		// console.log('*************************************');
+
+		// console.log(meta.report.meta.clauses.meta[0]);
+		// console.log(meta.report.meta.clauses.meta[0] === meta.report.meta.clauses.meta[0]);
+		// console.log(meta.report.meta.clauses.meta[0] === meta.report.meta.clauses.meta[1]);
+		// console.log(meta.report.meta.clauses.meta[0] === cloneDeep(meta.report.meta.clauses.meta[0]));
+
+		// console.log('HERE');
+		// console.log(meta.report.meta.clauses.meta[0].meta.subClauses.meta.length);
+		// console.log(meta.report.meta.clauses.meta[1].meta.subClauses.meta.length);
+		// console.log(meta.report.meta.clauses.meta[0].meta.subClauses.meta[0] === meta.report.meta.clauses.meta[0].meta.subClauses.meta[0]);
+		// console.log(meta.report.meta.clauses.meta[0].meta.subClauses.meta[0] === meta.report.meta.clauses.meta[0].meta.subClauses.meta[1]);
+		// console.log(meta.report.meta.clauses.meta[0].meta.subClauses.meta[0] === cloneDeep(meta.report.meta.clauses.meta[0].meta.subClauses.meta[0]));
+
 		(form || this.form).patchValue(mappedModel);
 		(form || this.form).updateValueAndValidity();
 	}

+ 49 - 41
src/app/dynaform/services/model-mapper.service.ts

@@ -50,57 +50,65 @@ const mapping = {
 
 (1) forwardMap gives:
 
-{ b: { c: 555, e: 99 },
-  e: { f: { g: 777 } },
-  r: 1000,
-  s: 2000,
-  t: { u: 3000 },
-  d: 44,
-  f: { value: 2, isEven: true },
-  g: { value: 3, isEven: false },
-  y: { hhh: { value: 100000, isEven: true } },
-  i: 'MAPPING ERROR: Unknown mapping type in mapping at i' }
+{
+	b: { c: 555, e: 99 },
+	e: { f: { g: 777 } },
+	r: 1000,
+	s: 2000,
+	t: { u: 3000 },
+	d: 44,
+	f: { value: 2, isEven: true },
+	g: { value: 3, isEven: false },
+	y: { hhh: { value: 100000, isEven: true } },
+	i: 'MAPPING ERROR: Unknown mapping type in mapping at i' }
+}
 
 (2) lazyForwardMap (which also copies properties not explicitly specified in the mapping) gives:
 
-{ b: { c: 555, e: 99 },
-  e: { f: { g: 777 } },
-  r: 1000,
-  s: 2000,
-  t: { u: 3000 },
-  c: { lazy: 'everybody\'s lazy' },
-  d: 44,
-  f: { value: 2, isEven: true },
-  g: { value: 3, isEven: false },
-  y: { hhh: { value: 100000, isEven: true }, name: 'lost in action' },
-  i: 'MAPPING ERROR: Unknown mapping type in mapping at i',
-  z: 'hello' }
+{
+	b: { c: 555, e: 99 },
+	e: { f: { g: 777 } },
+	r: 1000,
+	s: 2000,
+	t: { u: 3000 },
+	c: { lazy: 'everybody\'s lazy' },
+	d: 44,
+	f: { value: 2, isEven: true },
+	g: { value: 3, isEven: false },
+	y: { hhh: { value: 100000, isEven: true }, name: 'lost in action' },
+	i: 'MAPPING ERROR: Unknown mapping type in mapping at i',
+	z: 'hello'
+}
 
 (3) reverseMap (on either of the forwardMap results) regenerates the explicitly mapped parts of the original model:
 
-{ a: 555,
-  b: 777,
-  c: { r: 1000, s: 2000, t: 3000 },
-  d: 22,
-  e: 33,
-  f: 2,
-  g: 3,
-  h: 100000 }
+{
+	a: 555,
+	b: 777,
+	c: { r: 1000, s: 2000, t: 3000 },
+	d: 22,
+	e: 33,
+	f: 2,
+	g: 3,
+	h: 100000
+}
 
 (4) lazyReverseMap also copies additional properties, if it can
 	(it will merge objects without overwriting exissting properties, and won't overwrite scalar properties)
 
-{ a: 555,
-  b: 777,
-  c: { r: 1000, s: 2000, t: 3000, lazy: 'everybody\'s lazy' },
-  d: 22,
-  e: 33,
-  f: 2,
-  g: 3,
-  h: 100000,
-  y: { hhh: { value: 100000, isEven: true }, name: 'lost in action' },
-  i: 'MAPPING ERROR: Unknown mapping type in mapping at i',
-  z: 'hello' }
+{
+	a: 555,
+	b: 777,
+	c: { r: 1000, s: 2000, t: 3000, lazy: 'everybody\'s lazy' },
+	d: 22,
+	e: 33,
+	f: 2,
+	g: 3,
+	h: 100000,
+	y: { hhh: { value: 100000, isEven: true }, name: 'lost in action' },
+	i: 'MAPPING ERROR: Unknown mapping type in mapping at i',
+	z: 'hello'
+}
 
 */
 

+ 39 - 32
src/styles.scss

@@ -10,6 +10,8 @@ $col-blue-primary: #0085bc;
 
 $col-charcoal: #444;
 
+$col-danger: red;
+
 // --------------------------------------------------------------------------------------------------------------------
 
 // Clarity Dependency SCSS - No loner needed as of Clarity 2?
@@ -74,7 +76,7 @@ div.clr-col-sm-8 {
 	}
 }
 
-.short-field {
+.multicolor {
 	input, select {
 		border-width: 1px;
 		border-style: solid;
@@ -83,7 +85,7 @@ div.clr-col-sm-8 {
 		border-right-color: teal;
 		border-bottom-width: 2px;
 		border-bottom-color: magenta;
-		width: 60px;
+		width: 200px;
 		padding-left: 10px;
 	}
 }
@@ -186,27 +188,6 @@ input, textarea, select {
 	.btn { margin-left: 0; margin-right: 4px; }
 }
 
-// ---------------------------------------------------------------------------------------------------------------------
-// Repeating Fields
-
-// dyna-rf = Dynaform Repeating Field
-
-.dyna-rf-container {
-	// outline: 1px lime solid;
-	label {
-		display: none;
-	}
-	> div:first-of-type {
-		label {
-			display: block;
-		}
-	}
-}
-
-// .dyna-rf-field {
-// 	outline: 1px pink solid;
-// }
-
 // ---------------------------------------------------------------------------------------------------------------------
 // Repeating Containers (series of buttons alowwing user to focus a repeating container member)
 
@@ -222,35 +203,61 @@ input, textarea, select {
 	}
 }
 
-.dyna-rc-container.dyna-rc-display-all, .dyna-rc-control.dyna-rc-display-all {
-	border-bottom: 4px #CCC solid;
+.dyna-rc-member-container.dyna-rc-display-all, .dyna-rc-control.dyna-rc-display-all {
+	// border-bottom: 4px #CCC solid;
 	padding-bottom: 1rem;
-	&:first-child {
-		border-top: 4px #CCC solid;
-		> button {
-			margin-top: 1rem;
-		}
-	}
 }
+
 // ---------------------------------------------------------------------------------------------------------------------
 // Repeating Field and Container Buttons
 
+.dyna-rep-add-container {
+	@include clearfix;
+}
+
 .dyna-rep-btn-add {
 	margin-left: 0;
 }
 
 .dyna-rep-btn-delete {
 	float: right;
-	margin-top: 0;
 	margin-right: 0;
+	padding: 0;
+	color: $col-charcoal;
+	cursor: pointer;
+	.dyna-rf-field & {
+		background-color: transparent;
+		border: 0;
+		transform: translateY(16px);
+		&:disabled {
+			color: #AAA !important;
+			cursor: not-allowed;
+		}
+		&:hover {
+			color: $col-danger;
+		}
+	}
+	.dyna-rc-member-container & {
+		&:hover {
+			color: $col-danger;
+		}
+		&:disabled {
+			cursor: not-allowed;
+		}
+	}
 }
 
+
 // ---------------------------------------------------------------------------------------------------------------------
 
 .dyna-hidden {
 	display: none;
 }
 
+div:not(.row-checkbox) + div.row-checkbox {
+	margin-top: 8px;
+}
+
 // ---------------------------------------------------------------------------------------------------------------------
 // Errors