Переглянути джерело

Copying across latest changes from AMP; using Ivy

Richard Knight 4 роки тому
батько
коміт
cc6a4db683
22 змінених файлів з 223 додано та 93 видалено
  1. 6 6
      package-lock.json
  2. 2 2
      package.json
  3. 2 2
      src/app/dynaform/components/_abstract/group-input.component.ts
  4. 4 2
      src/app/dynaform/components/_abstract/native-input.component.ts
  5. 11 2
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.html
  6. 77 8
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.ts
  7. 4 0
      src/app/dynaform/components/clarity/text/clr-text.component.html
  8. 9 2
      src/app/dynaform/components/clarity/text/clr-text.component.ts
  9. 1 0
      src/app/dynaform/components/custom/checkbutton/checkbutton.component.ts
  10. 4 4
      src/app/dynaform/components/custom/dropdown-modified-input/dropdown-modified-input.component.ts
  11. 7 3
      src/app/dynaform/components/group/checkbox-group/clr-checkbox-group.component.ts
  12. 1 1
      src/app/dynaform/components/group/checkbutton-group/checkbutton-group.component.html
  13. 8 3
      src/app/dynaform/components/group/checkbutton-group/checkbutton-group.component.ts
  14. 6 5
      src/app/dynaform/directives/dynafield.directive.ts
  15. 2 2
      src/app/dynaform/dynaform.component.html
  16. 10 2
      src/app/dynaform/dynaform.component.ts
  17. 0 4
      src/app/dynaform/dynaform.module.ts
  18. 2 2
      src/app/dynaform/interfaces/index.ts
  19. 47 29
      src/app/dynaform/models/field.model.ts
  20. 1 1
      src/app/dynaform/services/_formdata-utils.ts
  21. 17 11
      src/app/dynaform/services/dynaform.service.ts
  22. 2 2
      src/app/dynaform/utils.ts

+ 6 - 6
package-lock.json

@@ -1715,9 +1715,9 @@
       "dev": true
     },
     "@types/lodash": {
-      "version": "4.14.156",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.156.tgz",
-      "integrity": "sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ==",
+      "version": "4.14.157",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.157.tgz",
+      "integrity": "sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==",
       "dev": true
     },
     "@types/minimatch": {
@@ -1727,9 +1727,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "14.0.13",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz",
-      "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==",
+      "version": "14.0.14",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.14.tgz",
+      "integrity": "sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ==",
       "dev": true
     },
     "@types/q": {

+ 2 - 2
package.json

@@ -39,8 +39,8 @@
     "@angular/language-service": "^9.1.11",
     "@types/jasmine": "^3.5.11",
     "@types/jasminewd2": "~2.0.8",
-    "@types/lodash": "^4.14.156",
-    "@types/node": "^14.0.13",
+    "@types/lodash": "^4.14.157",
+    "@types/node": "^14.0.14",
     "codelyzer": "^5.2.2",
     "jasmine-core": "~3.5.0",
     "jasmine-spec-reporter": "~5.0.2",

+ 2 - 2
src/app/dynaform/components/_abstract/group-input.component.ts

@@ -1,9 +1,9 @@
 import { Input, OnInit } from '@angular/core';
 import { FormGroup, FormArray } from '@angular/forms';
-import {buildFormControl } from './../../services/_formdata-utils';
+import { buildFormControl } from './../../services/_formdata-utils';
 
 
-export abstract class GroupInputComponent implements OnInit {
+export abstract class GroupInputComponent<C> implements OnInit {
 
 	@Input()
 	control: FormGroup | FormArray;

+ 4 - 2
src/app/dynaform/components/_abstract/native-input.component.ts

@@ -2,7 +2,7 @@ import { OnInit, OnDestroy, Input, Output, ChangeDetectorRef, EventEmitter } fro
 import { FormControl } from '@angular/forms';
 import { FriendlyValidationErrorsService } from './../../services/friendly-validation-errors.service';
 import { Subject, Subscription } from 'rxjs';
-import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
 
 export abstract class NativeInputComponent implements OnInit, OnDestroy {
 
@@ -13,6 +13,9 @@ export abstract class NativeInputComponent implements OnInit, OnDestroy {
 	set meta(meta: StringMap<any>) {
 		this._meta = meta;
 		this.exposeForTemplate();
+		if (this.control.disabled != meta.disabled) { // Deliberate '!='
+			meta.disabled ? this.control.disable() : this.control.enable();
+		}
 	}
 
 	@Output()
@@ -59,7 +62,6 @@ export abstract class NativeInputComponent implements OnInit, OnDestroy {
 		this.keyUpSBX = this.keyUp$.pipe(
 			debounceTime(this.keyUpValidationDelay),
 			distinctUntilChanged(),
-			tap(val => console.log('CHANGED', val))
 		).subscribe(val => {
 			this.control.markAsTouched();
 			this.control.markAsDirty();

+ 11 - 2
src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.html

@@ -1,6 +1,15 @@
 <clr-checkbox-wrapper>
-	<input type="checkbox" clrCheckbox [formControl]="control" (change)="setValue($event.target)">
-	<label>{{ label }}</label>
+	<input type="checkbox" clrCheckbox [formControl]="control" (change)="checkedChange($event.target)">
+	<label [class.editable]="editable">
+		{{ label }}
+		<span *ngIf="editable"
+		[attr.contenteditable]="editable"
+		spellcheck="false"
+		(click)="focusEditable($event)"
+		(input)="labelEdited()" 
+		(keydown.enter)="defocusEditable()"
+		#labelRef></span>
+	</label>
 	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-checkbox-wrapper>
 

+ 77 - 8
src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.ts

@@ -1,25 +1,31 @@
-import { Component, ChangeDetectorRef } from '@angular/core';
+import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
 import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
 
+import { Subject, Subscription } from 'rxjs';
+import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators';
+
 
 @Component({
 	selector: 'app-checkbox',
 	templateUrl: './clr-checkbox.component.html',
 	styleUrls: ['./clr-checkbox.component.scss']
 })
-export class ClrCheckboxComponent extends NativeInputComponent {
+export class ClrCheckboxComponent extends NativeInputComponent implements OnInit, AfterViewInit, OnDestroy {
+
+	@ViewChild('labelRef', { static: false })
+	labelRef: ElementRef;
 
-	exposeMetaInTemplate: string[] = ['label'];
+	exposeMetaInTemplate: string[] = ['label', 'editable'];
 
 	label: string;
+	editable: boolean;
 
-	readonly componentName = 'CheckboxComponent'; // For AOT compatibility, as class names don't survive minification
+	label$: Subject<string> = new Subject();
+	labelSBX: Subscription;
+	labelTxt: string = '';
 
-	setValue(cb: HTMLInputElement) {
-		this.control.setValue(cb.checked ? this._meta.checkedValue : false);
-		this.handleChange();
-	}
+	readonly componentName = 'CheckboxComponent'; // For AOT compatibility, as class names don't survive minification
 
 	constructor(
 		protected valErrsService: FriendlyValidationErrorsService,
@@ -28,4 +34,67 @@ export class ClrCheckboxComponent extends NativeInputComponent {
 		super(valErrsService, _cdr);
 	}
 
+	ngOnInit() {
+		super.ngOnInit();
+		if (this._meta.editable) {
+			this.labelSBX = this.label$.pipe(
+				debounceTime(500),
+				distinctUntilChanged(),
+				tap(label => this.labelTxt = label)
+			).subscribe(label => {
+				this.setValue(!!this.control.value, label);
+			});			
+		}
+	}
+
+	ngAfterViewInit() {
+		if (this.control.value && this._meta.transformer?.inputFn) {
+			const boundFn = this._meta.transformer.inputFn.bind(this);
+			try {
+				boundFn(this.control.value);
+			} catch(e) {
+				console.error('Bad Transformer InputFn');
+				console.error(e);
+			}
+		}
+	}
+
+	ngOnDestroy() {
+		if (this.labelSBX) {
+			this.labelSBX.unsubscribe();
+		}
+	}
+
+	checkedChange(cb: HTMLInputElement) {
+		this.setValue(cb.checked);
+		this.handleChange();
+	}
+
+	setValue(checked: boolean, labelTxt: string = this.labelTxt) {
+		let valToSet = checked ? this._meta.checkedValue : false;
+		if (checked && typeof this._meta.transformer?.outputFn === 'function') {
+			try {
+				valToSet = this._meta.transformer.outputFn(this._meta.checkedValue, this.labelTxt);
+			} catch(e) {
+				console.error('Bad Transformer OutputFn');
+				console.error(e);
+			}
+		}
+		this.control.setValue(valToSet);
+	}
+
+	focusEditable(e: MouseEvent): void {
+		setTimeout(() => this.labelRef.nativeElement.focus(), 250);
+		e.preventDefault();
+	}
+
+	defocusEditable(): void {
+		this.labelRef.nativeElement.blur();
+	}
+	
+	labelEdited(): void {
+		const label = (this.labelRef.nativeElement) as HTMLSpanElement;
+		this.label$.next(label.textContent.trim());
+	}
+
 }

+ 4 - 0
src/app/dynaform/components/clarity/text/clr-text.component.html

@@ -5,6 +5,8 @@
 		[placeholder]="placeholder"
 		(keyup)="handleKeyup($event.target.value)"
 		spellcheck="false"
+		autocomplete="off"
+		#input
 	>
 	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-input-container>
@@ -19,6 +21,8 @@
 			[placeholder]="placeholder"
 			(keyup)="handleKeyup($event.target.value)"
 			spellcheck="false"
+			autocomplete="off"
+			#input
 		>
 		<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 	</clr-input-container>

+ 9 - 2
src/app/dynaform/components/clarity/text/clr-text.component.ts

@@ -1,4 +1,4 @@
-import { Component, ChangeDetectorRef } from '@angular/core';
+import { Component, ViewChild, AfterViewInit, ElementRef, ChangeDetectorRef } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
 import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
 
@@ -7,7 +7,7 @@ import { FriendlyValidationErrorsService } from './../../../services/friendly-va
 	templateUrl: './clr-text.component.html',
 	styleUrls: ['./clr-text.component.scss']
 })
-export class ClrTextComponent extends NativeInputComponent {
+export class ClrTextComponent extends NativeInputComponent implements AfterViewInit {
 
 	exposeMetaInTemplate: string[] = ['label', 'placeholder', 'link'];
 
@@ -17,6 +17,9 @@ export class ClrTextComponent extends NativeInputComponent {
 
 	readonly componentName = 'TextComponent'; // For AOT compatibility, as class names don't survive minification
 
+	@ViewChild('input')
+	formInput: ElementRef;
+
 	constructor(
 		protected valErrsService: FriendlyValidationErrorsService,
 		protected _cdr: ChangeDetectorRef
@@ -24,4 +27,8 @@ export class ClrTextComponent extends NativeInputComponent {
 		super(valErrsService, _cdr);
 	}
 
+	ngAfterViewInit() {
+		(this.formInput.nativeElement as HTMLInputElement).setAttribute(`data-${this._meta.name.toLowerCase()}`, '');
+	}
+
 }

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

@@ -64,4 +64,5 @@ export class CheckbuttonComponent extends CustomInputComponent implements OnChan
 		this.currentValue = this.isChecked ? this.checkedValue : false;
 		this._cdr.markForCheck(); // We have to manually trigger change detection when using setValue or patchValue from outside this component
 	}
+
 }

+ 4 - 4
src/app/dynaform/components/custom/dropdown-modified-input/dropdown-modified-input.component.ts

@@ -24,8 +24,8 @@ export class DropdownModifiedInputComponent extends CustomInputComponent impleme
 	label: string;
 	modifiers: string[];
 	transform: ValueTransformer = {
-		inputFn: value => ({ modifier: '', value }),
-		outputFn: (modifier, value) => value
+		inputFn: value => ({  value, modifier: '' }),
+		outputFn: (value, modifier) => value
 	};
 	extraClass;
 	selectedModifier: string;
@@ -63,12 +63,12 @@ export class DropdownModifiedInputComponent extends CustomInputComponent impleme
 
 	modifierChange(modifier) {
 		this.selectedModifier = modifier;
-		this._controlValue = this.transform.outputFn(this.selectedModifier, this.displayedValue);
+		this._controlValue = this.transform.outputFn(this.displayedValue, this.selectedModifier);
 		this.propagateChange(this.controlValue);
 	}
 
 	inputChange() {
-		this._controlValue = this.transform.outputFn(this.selectedModifier, this.displayedValue);
+		this._controlValue = this.transform.outputFn(this.displayedValue, this.selectedModifier);
 		this.propagateChange(this.controlValue);
 	}
 

+ 7 - 3
src/app/dynaform/components/group/checkbox-group/clr-checkbox-group.component.ts

@@ -1,13 +1,17 @@
-import { Component, Attribute, OnInit } from '@angular/core';
+import { Component, Attribute, OnInit, ViewChildren, QueryList } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { GroupInputComponent } from '../../_abstract/group-input.component';
+import { ClrCheckboxComponent } from '../../clarity/checkbox/clr-checkbox.component';
 
 @Component({
 	selector: 'app-checkbox-group',
 	templateUrl: './clr-checkbox-group.component.html',
 	styleUrls: ['./clr-checkbox-group.component.scss']
 })
-export class ClrCheckboxGroupComponent extends GroupInputComponent implements OnInit {
+export class ClrCheckboxGroupComponent extends GroupInputComponent<ClrCheckboxComponent> implements OnInit {
+
+	@ViewChildren(ClrCheckboxComponent)
+	groupMembers: QueryList<ClrCheckboxComponent>;
 
 	firstControl: FormControl;
 
@@ -50,7 +54,7 @@ export class ClrCheckboxGroupComponent extends GroupInputComponent implements On
 	}
 
 	selectAll(e: MouseEvent): false {
-		this.controlNames.forEach(c => this.formGoA.get(c).setValue(this._meta.meta[c].value));
+		this.groupMembers.forEach(component => component.setValue(true));
 		(e.target as HTMLLinkElement).blur();
 		return false;
 	}

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

@@ -1,4 +1,4 @@
-<div class="aba-checkbutton-group" [formGroup]="formGroup">
+<div class="aba-checkbutton-group" [formGroup]="formGoA">
 	<app-checkbutton *ngFor="let groupMember of controlNames; let i = index"
 		[formControlName]="groupMember"
 		[meta]="childMetaArray[i]"

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

@@ -1,13 +1,17 @@
-import { Component, Attribute, OnInit } from '@angular/core';
+import { Component, Attribute, OnInit, ViewChildren, QueryList } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { GroupInputComponent } from './../../_abstract/group-input.component';
+import { CheckbuttonComponent } from '../../custom/checkbutton/checkbutton.component';
 
 @Component({
 	selector: 'app-checkbutton-group',
 	templateUrl: './checkbutton-group.component.html',
 	styleUrls: ['./checkbutton-group.component.scss']
 })
-export class CheckbuttonGroupComponent extends GroupInputComponent implements OnInit {
+export class CheckbuttonGroupComponent extends GroupInputComponent<CheckbuttonComponent> implements OnInit {
+
+	@ViewChildren(CheckbuttonComponent)
+	groupMembers: QueryList<CheckbuttonComponent>;
 
 	firstControl: FormControl;
 
@@ -50,7 +54,7 @@ export class CheckbuttonGroupComponent extends GroupInputComponent implements On
 	}
 
 	selectAll(e: MouseEvent): false {
-		this.controlNames.forEach(c => this.formGoA.get(c).setValue(this._meta.meta[c].value));
+		this.groupMembers.forEach(component => component.writeValue(true));
 		(e.target as HTMLLinkElement).blur();
 		return false;
 	}
@@ -60,4 +64,5 @@ export class CheckbuttonGroupComponent extends GroupInputComponent implements On
 		(e.target as HTMLLinkElement).blur();
 		return false;
 	}
+	
 }

+ 6 - 5
src/app/dynaform/directives/dynafield.directive.ts

@@ -110,7 +110,7 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 
 			// Set id and classes
 			this.setCssId(cssId);
-			this.setCssClasses(type, cssClass);
+			this.setCssClasses(name, type, cssClass);
 
 			// Check whether it's disabled, then set its FormControl and metadata
 			if (disabled) {
@@ -153,7 +153,7 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 		if (this.component) {
 			let { control, meta } = this;
 			const type = componentType(meta.type);
-			const { class: cssClass, id: cssId, disabled } = meta;
+			const { name, 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) {
@@ -177,7 +177,7 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 			}
 
 			this.setCssId(cssId);
-			this.setCssClasses(type, cssClass);
+			this.setCssClasses(name, type, cssClass);
 			this.component.instance.meta = meta; // NOTE: Setting an imput like this does *NOT* trigger ngOnChanges
 		}
 	}
@@ -228,10 +228,11 @@ export class DynafieldDirective extends NgControl implements OnInit, OnChanges,
 		cssId ? el.setAttribute('id', cssId) : el.removeAttribute('id');
 	}
 
-	setCssClasses(type: string, cssClass: string|string[]): void {
+	setCssClasses(name: string, type: string, cssClass: string|string[]): void {
 		const classList = this.component.location.nativeElement.classList as DOMTokenList;
+		const fieldNameClass = `dff-${name.toLowerCase()}`;
 		const componentTypeClass = type.toLowerCase().replace('component', '');
-		classList.add(componentTypeClass);
+		classList.add(fieldNameClass, componentTypeClass);
 		if (typeof cssClass === 'string' || Array.isArray(cssClass)) {
 			const classesToAdd = (Array.isArray(cssClass) ? cssClass : [...cssClass.split(/\s+/)]).filter(Boolean);
 			const classesToRemove = [];

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

@@ -18,7 +18,7 @@
 			<ng-container [ngSwitch]="meta.type">
 
 				<div *ngSwitchCase="'RepeatingField'" class="dyna-rf-container" [ngClass]="getEmbeddedDynaformClasses(meta)">
-					<div *ngFor="let field of meta.meta; let i = index" class="dyna-rf-field">
+					<div *ngFor="let field of meta.meta; let i = index" class="dyna-rf-field" @fadeNSlide>
 						<button *ngIf="meta.showDeleteControl"
 							class="dyna-rep-btn-delete"
 							[disabled]="!deleteAllowed(meta.name)"
@@ -57,7 +57,7 @@
 							</button>
 						</div>
 					</div>
-					<div *ngFor="let container of meta.meta; let i = index" class="dyna-rc-member-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' }" @fadeNSlide>
 						<button *ngIf="meta.showDeleteControl"
 							class="btn btn-sm btn-icon btn-outline-danger dyna-rep-btn-delete"
 							[ngClass]="{ 'dyna-hidden': meta.display === 'SINGLE' && !meta.meta[i].focussed }"

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

@@ -1,5 +1,7 @@
 import { Component, Input, Output, EventEmitter, TemplateRef, Optional, OnInit, OnChanges, ChangeDetectionStrategy } from '@angular/core';
 import { FormBuilder, FormControl, FormGroup, FormArray, FormGroupName, AbstractControl, ControlContainer } from '@angular/forms';
+import { trigger, style, animate, transition } from '@angular/animations';
+
 import { SuperForm } from 'angular-super-validator';
 import { buildFormGroupFunctionFactory, buildFormControl } from './services/_formdata-utils';
 import { cloneDeep } from 'lodash/fp';
@@ -13,7 +15,13 @@ export interface DynarowContext {
 	selector: 'app-dynaform',
 	templateUrl: './dynaform.component.html',
 	styleUrls: ['./dynaform.component.scss'],
-	changeDetection: ChangeDetectionStrategy.OnPush // or ChangeDetectionStrategy.OnPush - should be more efficient. Experiment later.
+	changeDetection: ChangeDetectionStrategy.OnPush,
+	animations: [
+		trigger('fadeNSlide', [
+			transition(':enter', [ style({ opacity: 0, maxHeight: 0 }), animate('500ms', style({ opacity: 1, maxHeight: '500px' })) ]),
+			transition(':leave', [ style({ opacity: 1, maxHeight: '500px' }), animate('300ms', style({  opacity: 0, maxHeight: '0px' })) ])
+		])
+	]
 })
 export class DynaformComponent implements OnInit, OnChanges {
 
@@ -77,7 +85,7 @@ export class DynaformComponent implements OnInit, OnChanges {
 
 	ngOnChanges() {
 		// Triggered when inputs change
-		console.log('%c *** DynaformChange *** ', this.conOlive);
+		// console.log('%c *** DynaformChange *** ', this.conOlive);
 		// 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

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

@@ -3,8 +3,6 @@ import { CommonModule } from '@angular/common';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 
-// import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
-// import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
 import { ClarityModule } from '@clr/angular';
 
 import { DynaformComponent } from './dynaform.component';
@@ -22,8 +20,6 @@ import { ffcArr } from './components'; // ffcArr = Form Field Components Array,
 		FormsModule,
 		ReactiveFormsModule,
 		RouterModule.forChild([]),
-		// NgbModule,
-		// DateInputsModule
 		ClarityModule
 	],
 	declarations: [

+ 2 - 2
src/app/dynaform/interfaces/index.ts

@@ -1,6 +1,6 @@
 export interface ValueTransformer {
-	inputFn: (value: string) => { modifier: string, value: string };
-	outputFn: (modifier: string, value: string) => string;
+	inputFn: (value: string) => { value: string, modifier?: string };
+	outputFn: ( value: string, modifier?: string) => string;
 }
 export interface FriendlyValErrors {
 	'required': string;

+ 47 - 29
src/app/dynaform/models/field.model.ts

@@ -8,25 +8,27 @@ import { TemplateRef } from '@angular/core';
 import { ValidatorFn, AsyncValidatorFn } from '@angular/forms';
 import { ValueTransformer } from './../interfaces';
 import { standardModifiers, standardTransformer } from './../utils';
+import { BehaviorSubject } from 'rxjs';
 
 interface ISimpleFieldMetaData {
-	name: string; 							// The FormControl name
-	type?: string; 							// The component type e.g. Text, Checkbutton, Timepicker, etc
-	label?: string;							// The field label - defaults to unCamelCased name if not supplied
-	value?: any;							// The field value - defaults to empty string if not supplied
-	checkedValue?: boolean|number|string;	// Checkboxes and Checkbuttons only
-	default?: any;							// Default value
-	placeholder?: string;					// Optional placeholder text
-	class?: string | string[];				// CSS classes to apply
-	id?: string;							// CSS id to apply
-	disabled?: boolean;						// Whether field is initially disabled
-	change?: string;						// Name of function in host component to call when value changes
-	source?: string;						// Location in API-returned model - defaults to name
-	before?: string;						// Ordering instruction - move before <name of another key in group>
-	after?: string;							// Ordering instruction - move after <name of another key in group>
-	validators?: ValidatorFn[];				// Array of validator functions - following Angular FormControl API
-	asyncValidators?: AsyncValidatorFn[];	// Array of async validator functions - following Angular FormControl API
-	valFailureMsgs?: StringMap<any>;		// Validation failure messages - display appropriate message if validation fails
+	name: string; 								// The FormControl name
+	type?: string; 								// The component type e.g. Text, Checkbutton, Timepicker, etc
+	label?: string;								// The field label - defaults to unCamelCased name if not supplied
+	value?: any;								// The field value - defaults to empty string if not supplied
+	checkedValue?: boolean | number | string;	// Checkboxes and Checkbuttons only
+	default?: any;								// Default value
+	placeholder?: string;						// Optional placeholder text
+	class?: string | string[];					// CSS classes to apply
+	id?: string;								// CSS id to apply
+	disabled?: boolean;							// Whether field is initially disabled
+	change?: string;							// Name of function in host component to call when value changes
+	transform?: ValueTransformer				// 
+	source?: string;							// Location in API-returned model - defaults to name
+	before?: string;							// Ordering instruction - move before <name of another key in group>
+	after?: string;								// Ordering instruction - move after <name of another key in group>
+	validators?: ValidatorFn[];					// Array of validator functions - following Angular FormControl API
+	asyncValidators?: AsyncValidatorFn[];		// Array of async validator functions - following Angular FormControl API
+	valFailureMsgs?: StringMap<any>;			// Validation failure messages - display appropriate message if validation fails
 }
 
 interface IOption {
@@ -35,8 +37,8 @@ interface IOption {
 }
 
 interface IOptionsFieldMetaData extends ISimpleFieldMetaData {
-	options: string[] | IOption[] | (() => IOption[]);	// Array of Options - for select, radio-button-group and other 'multiple-choice' types
-	horizontal?: boolean;								// Whether to arrange radio buttons or checkboxes horizontally (default false)
+	options: string[] | IOption[] | (() => IOption[] | BehaviorSubject<IOptionsFieldMetaData['options']>);	// Array of Options - for select, radio-button-group and other 'multiple-choice' types
+	horizontal?: boolean; // Whether to arrange radio buttons or checkboxes horizontally (default false)
 }
 
 // For components that include links to other pages
@@ -82,6 +84,7 @@ abstract class SimpleField {
 	id?: string;
 	disabled = false;
 	change?: string;
+	transform?: ValueTransformer;
 	validators: ValidatorFn|ValidatorFn[] = [];
 	asyncValidators: AsyncValidatorFn|AsyncValidatorFn[] = [];
 	valFailureMsgs: StringMap<any> = {};
@@ -142,15 +145,28 @@ class Option implements IOption {
 }
 
 abstract class OptionsField extends SimpleField {
+	
 	options: Option[] | (() => Option[]) = [];
-	constructor(meta: IOptionsFieldMetaData, context?: any) {
+	
+	constructor(meta: IOptionsFieldMetaData, private context?: any) {
 		super(meta);
+		if (meta.options instanceof BehaviorSubject) {
+			meta.options.subscribe(opts => this.constructOptions(opts));
+		} else {
+			this.constructOptions(meta.options);
+		}
+	}
+
+	private constructOptions(optProvider: IOptionsFieldMetaData['options']) {
 		let options;
-		if (typeof meta.options === 'function') {
-			const boundFn = meta.options.bind(context);
+		if (typeof optProvider === 'function') {
+			const boundFn = optProvider.bind(this.context);
 			options = boundFn();
+			if (options instanceof BehaviorSubject) {
+				options.subscribe(opts => this.constructOptions(opts));
+			}
 		} else {
-			options = meta.options;
+			options = optProvider;
 		}
 		if (Array.isArray(options)) {
 			this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
@@ -161,6 +177,7 @@ abstract class OptionsField extends SimpleField {
 			];
 		}
 	}
+	
 }
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -201,6 +218,7 @@ class CheckboxField extends SimpleField {
 	default: any = false;
 	checkedValue: boolean|number|string = true;
 	rowLabel: null;
+	editable: boolean;
 	constructor(meta: ISimpleFieldMetaData) {
 		super(meta);
 		if (meta.default) {
@@ -215,9 +233,6 @@ class CheckboxField extends SimpleField {
 		if (typeof meta.value === 'undefined') {
 			this.value = this.default; // Get default from this class, not superclass
 		}
-		if (!meta.label) {
-			this.label = unCamelCase(this.checkedValue.toString());
-		}
 	}
 }
 
@@ -256,8 +271,10 @@ class CheckboxGroup {
 	label?: string;
 	groupName: string;
 	firstEnablesRest?: boolean;
+	editable: boolean = false;
 	showAllOrNone?: boolean;
 	meta: CheckboxField[] | { [key: string]: CheckboxField };
+	transformer?: ValueTransformer;
 	constructor(groupmeta: any) {
 		Object.assign(this, groupmeta);
 		if (typeof this.label === 'undefined') {
@@ -266,13 +283,14 @@ class CheckboxGroup {
 			this.label = unCamelCase(this.name);
 		}
 		// Can render as a FormArray or FormGroup depending on input data
+		const passThrough = { editable: this.editable, transformer: this.transformer };
 		if (Array.isArray(groupmeta.meta)) {
 			const arrayMembers = groupmeta.meta;
-			this.meta = arrayMembers.map(cb => new CheckboxField(cb));
+			this.meta = arrayMembers.map(cb => new CheckboxField({ ...cb, ...passThrough }));
 		} else {
 			const groupMembers = groupmeta.meta;
 			this.meta = Object.entries(groupMembers)
-				.map( ([key, cb]) => [key, new CheckboxField(cb as ISimpleFieldMetaData)] )
+				.map( ([key, cb]: [string, ISimpleFieldMetaData]) => [key, new CheckboxField({ label: unCamelCase(cb.checkedValue || cb.value).toString(), ...cb, ...passThrough })] )
 				.reduce((res, [key, cbf]) => { res[key as string] = cbf; return res; }, {});
 		}
 	}
@@ -291,7 +309,7 @@ class DatetimeField extends SimpleField {
 	value: Date | string;
 	constructor(meta) {
 		super(meta);
-		if (typeof this.value === 'string') {
+		if (typeof this.value === 'string' && this.value) {
 			this.value = new Date(this.value);
 		}
 		if (!(this.value instanceof Date)) {

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

@@ -682,7 +682,7 @@ const safeSet = (obj, key, val, createAdditionalKeys = false) => {
 		if (Array.isArray(currentVal)) {
 			if (typeof val === 'object') {
 				// Replace array
-				console.log('safeSet array', key, val);
+				// 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?

+ 17 - 11
src/app/dynaform/services/dynaform.service.ts

@@ -27,7 +27,7 @@
  *
  * build(model)				- build everything from the model
  * build({}, meta)			- build everything from the metadata
- * build(model, meta)		- build by combining the model with metadata, lazyily (not every model field needs metadata, as sensible defaults)
+ * build(model, meta)		- build by combining the model with metadata, lazyily (not every model field needs metadata, as there are sensible defaults)
  * build(model, meta, true)	- build by combining model with metadata, creating new fields from metadata points that don't occur in the model
  *
  *
@@ -40,7 +40,7 @@
  * Set the build strategy using the setBuildStrategy method e.g.
  *
  * setBuildStrategy('MODELFIRST') - the default
- * setBuildStrategy('NETAFIRST')
+ * setBuildStrategy('METAFIRST')
  *
  * then use the build function in the normal way
  *
@@ -73,15 +73,17 @@
  *
  * LOWER-LEVEL METHODS
  * -------------------
+ * 
+ * buildModeledMeta(metadata) - Generate modeled metadata from sparse metadata - useful when patching forms dynamically
  *
  * autoBuildFormGroupAndMeta(model, meta, createFromMeta) - synonym for build
- * autoBuildModeledMeta(model, meta, createFromMeta) - takes a model and (lazy)metadata and returns expanded metadata
+ * autoBuildModeledMeta(model, meta, createFromMeta)      - takes a model and (lazy)metadata and returns expanded metadata
  *
- * buildFormGroup(metadata) - builds FormGroups from modelled metdata, recursively if necessary
- * buildFieldSpecificMeta(metadata) - use field metadata models to fill out metadata
+ * buildFormGroup(metadata)               - builds FormGroups from modelled metdata, recursively if necessary
+ * buildFieldSpecificMeta(metadata)       - use field metadata models to fill out metadata
  * combineModelWithMeta(model, extraMeta) - automatically generated metadata for model then combines extra metadata
- * combineExtraMeta(metadata, extraMeta) - combine extra metadata into metatdata, lazyly and recursively
- * 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
+ * autoMeta(model)                        - generate basic metadata from a raw or mapped model, recursively if necessary
  *
  *
  * NOTES
@@ -375,12 +377,16 @@ export class DynaformService {
 		};
 	}
 
-	autoBuildModeledMeta(model: StringMap<any>, meta = {}, createFromMeta = false) {
+	autoBuildModeledMeta(model: StringMap<any>, meta = {}, createFromMeta = false): StringMap<any> {
 		const modelWithMeta = this.combineModelWithMeta(model, meta, createFromMeta);
 		const reorderedMeta = execMetaReorderingInstructions(modelWithMeta);
 		return this.buildFieldSpecificMeta(reorderedMeta);
 	}
 
+	buildModeledMeta(meta: StringMap<any>): StringMap<any> {
+		return this.autoBuildModeledMeta({}, meta, true);
+	}
+
 	// -----------------------------------------------------------------------------------------------------------------
 	// Build field-type-specific metadata using the form field models (see dynaform/models)
 
@@ -391,15 +397,15 @@ export class DynaformService {
 	// -----------------------------------------------------------------------------------------------------------------
 	// Lower-level methods
 
-	combineModelWithMeta(model: StringMap<any>, meta, createFromMeta = false) {
+	combineModelWithMeta(model: StringMap<any>, meta, createFromMeta = false): StringMap<any> {
 		return combineModelWithMeta(model, meta, createFromMeta);
 	}
 
-	combineExtraMeta(meta, extraMeta, createFromExtra = false) {
+	combineExtraMeta(meta, extraMeta, createFromExtra = false): StringMap<any> {
 		return combineExtraMeta(meta, extraMeta, createFromExtra);
 	}
 
-	autoMeta(model) {
+	autoMeta(model: StringMap<any>): StringMap<any> {
 		return autoMeta(model);
 	}
 

+ 2 - 2
src/app/dynaform/utils.ts

@@ -19,9 +19,9 @@ const standardTransformer: ValueTransformer = {
 			modifier = 'Starts with';
 		}
 		const transformedVal = val.replace(/%/g, '').trim();
-		return { modifier, value: transformedVal };
+		return { value: transformedVal,  modifier };
 	},
-	outputFn: (mod, val) => {
+	outputFn: (val, mod) => {
 		let transformedValue;
 		switch (mod) {
 			case 'Starts with':