ソースを参照

Syncing changes from AMP

Richard Knight 6 年 前
コミット
9a68e17a91

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

@@ -1,4 +1,4 @@
-import { Input, OnInit } from '@angular/core';
+import { Input, Output, EventEmitter, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 
 export abstract class NativeInputComponent implements OnInit {
@@ -9,6 +9,9 @@ export abstract class NativeInputComponent implements OnInit {
 	@Input()
 	meta;
 
+	@Output()
+	call: EventEmitter<string> = new EventEmitter<string>();
+
 	exposeMetaInTemplate: string[] = [];
 
 	ngOnInit() {
@@ -16,4 +19,8 @@ export abstract class NativeInputComponent implements OnInit {
 		this.exposeMetaInTemplate.map(p => this[p] = this.meta[p] !== undefined ? this.meta[p] : this[p]);
 	}
 
+	handle(fnId: string, val: any) {
+		this.call.emit(fnId);
+	}
+
 }

+ 4 - 0
src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.ts

@@ -1,5 +1,6 @@
 import { Component } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
+import { meta } from '@modules/dynaform/testdata/testset.1';
 
 @Component({
 	selector: 'app-checkbox',
@@ -12,6 +13,9 @@ export class ClrCheckboxComponent extends NativeInputComponent {
 
 	setValue(cb: HTMLInputElement) {
 		this.control.setValue(cb.checked ? this.meta.checkedValue : false);
+		if (this.meta.change) {
+			this.handle(this.meta.change, this.control.value);
+		}
 	}
 
 }

+ 2 - 1
src/app/dynaform/components/clarity/radio/clr-radio.component.html

@@ -1,4 +1,5 @@
-<div [ngClass]="{'clr-control-inline' : horizontal}">
+<div class="clr-radio-container" [ngClass]="{'clr-control-inline' : horizontal}">
+	<label class="small">{{ label }}</label>
 	<clr-radio-wrapper *ngFor="let opt of options; let i = index;">
 		<input type="radio" clrRadio [formControl]="control" [value]="opt.value" [name]="prefix" [id]="prefix + i">
 		<label [for]="prefix + i">{{ opt.label }}</label>

+ 1 - 1
src/app/dynaform/components/clarity/radio/clr-radio.component.ts

@@ -8,7 +8,7 @@ import { NativeInputComponent } from '../../_abstract/native-input.component';
 })
 export class ClrRadioComponent extends NativeInputComponent {
 
-	exposeMetaInTemplate: string[] = ['name', 'options', 'horizontal'];
+	exposeMetaInTemplate: string[] = ['name', 'label', 'options', 'horizontal'];
 	prefix: string;
 
 	constructor() {

+ 74 - 3
src/app/dynaform/dynaform.module.ts

@@ -4,6 +4,7 @@ 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';
@@ -11,10 +12,31 @@ import { DynafieldDirective } from './directives/dynafield.directive';
 import { DynaformService } from './services/dynaform.service';
 import { ModelMapperService } from './services/model-mapper.service';
 
+// NOTE: Importing from barrel may not be supported with AOT compilation,
+// so instead we MAY have to do things the long-winded way (see per-component imports below...)
+// Using JIT for now, as a non-ideal workaround, with: ng build --prod --aot=false --build-optimizer=false
+// See: https://github.com/angular/angular/issues/20604
 import * as formFieldComponents from './components';
 const ffcArr = Object.values(formFieldComponents); // Array of all the Form Field Components
+/*
+import { HiddenComponent } from './components/native/hidden/hidden.component';
+import { ClrTextComponent as TextComponent } from './components/clarity/text/clr-text.component';
+import { ClrTextareaComponent as TextareaComponent } from './components/clarity/textarea/clr-textarea.component';
+import { ClrPasswordComponent as PasswordComponent } from './components/clarity/password/clr-password.component';
+import { ClrSelectComponent as SelectComponent } from './components/clarity/select/clr-select.component';
+import { ClrRadioComponent as RadioComponent } from './components/clarity/radio/clr-radio.component';
+import { ClrCheckboxComponent as CheckboxComponent } from './components/clarity/checkbox/clr-checkbox.component';
+import { ClrDatepickerComponent } from './components/clarity/datepicker/datepicker.component';
+import { CheckbuttonComponent } from './components/custom/checkbutton/checkbutton.component';
+import { DropdownModifiedInputComponent } from './components/custom/dropdown-modified-input/dropdown-modified-input.component';
+import { MultilineComponent } from './components/custom/multiline/multiline.component';
+import { ClrCheckboxGroupComponent as CheckboxGroupComponent } from './components/group/checkbox-group/clr-checkbox-group.component';
+import { CheckbuttonGroupComponent } from './components/group/checkbutton-group/checkbutton-group.component';
+import { ButtonGroupComponent } from './components/nocontrol/button-group/button-group.component';
+import { HeadingComponent } from './components/nocontrol/heading/heading.component';
+import { DisplayComponent } from './components/nocontrol/display/display.component';
+*/
 
-// import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
 
 @NgModule({
 	imports: [
@@ -30,8 +52,41 @@ const ffcArr = Object.values(formFieldComponents); // Array of all the Form Fiel
 		DynaformComponent,
 		DynafieldDirective,
 		...ffcArr
+		// HiddenComponent,
+		// TextComponent,
+		// TextareaComponent,
+		// PasswordComponent,
+		// SelectComponent,
+		// RadioComponent,
+		// CheckboxComponent,
+		// ClrDatepickerComponent,
+		// CheckbuttonComponent,
+		// DropdownModifiedInputComponent,
+		// MultilineComponent,
+		// CheckboxGroupComponent,
+		// CheckbuttonGroupComponent,
+		// ButtonGroupComponent,
+		// HeadingComponent,
+		// DisplayComponent
+	],
+	entryComponents: [ ffcArr
+		// HiddenComponent,
+		// TextComponent,
+		// TextareaComponent,
+		// PasswordComponent,
+		// SelectComponent,
+		// RadioComponent,
+		// CheckboxComponent,
+		// ClrDatepickerComponent,
+		// CheckbuttonComponent,
+		// DropdownModifiedInputComponent,
+		// MultilineComponent,
+		// CheckboxGroupComponent,
+		// CheckbuttonGroupComponent,
+		// ButtonGroupComponent,
+		// HeadingComponent,
+		// DisplayComponent
 	],
-	entryComponents: ffcArr,
 	providers: [
 		DynaformService,
 		ModelMapperService
@@ -41,7 +96,23 @@ const ffcArr = Object.values(formFieldComponents); // Array of all the Form Fiel
 		ReactiveFormsModule,
 		DynaformComponent,
 		DynafieldDirective,
-		ffcArr
+		...ffcArr
+		// HiddenComponent,
+		// TextComponent,
+		// TextareaComponent,
+		// PasswordComponent,
+		// SelectComponent,
+		// RadioComponent,
+		// CheckboxComponent,
+		// ClrDatepickerComponent,
+		// CheckbuttonComponent,
+		// DropdownModifiedInputComponent,
+		// MultilineComponent,
+		// CheckboxGroupComponent,
+		// CheckbuttonGroupComponent,
+		// ButtonGroupComponent,
+		// HeadingComponent,
+		// DisplayComponent
 	]
 })
 export class DynaformModule { }

+ 11 - 3
src/app/dynaform/models/field.model.ts

@@ -23,10 +23,11 @@ interface ISimpleFieldMetaData {
 	before?: string;						// Ordering instruction - move before <name of another key in group>
 	after?: string;							// Ordering instruction - move after <name of another key in group>
 	disabled?: boolean;						// Whether field is initially disabled
+	change?: string;						// Name of function in host component to call when value changes
 	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
-	onChange?: (val) => {};					// Function to call when field's value changes
+	// onChange?: (val) => {};				// Function to call when field's value changes
 }
 
 interface IOption {
@@ -81,6 +82,7 @@ abstract class SimpleField {
 	class?: string | string[];
 	id?: string;
 	disabled = false;
+	change?: string;
 	validators: ValidatorFn[] = [];
 	asyncValidators: AsyncValidatorFn[] = [];
 	valFailureMsgs: StringMap<any> = {};
@@ -122,9 +124,15 @@ class Option implements IOption {
 
 abstract class OptionsField extends SimpleField {
 	options: Option[] = [];
-	constructor(meta: IOptionsFieldMetaData) {
+	constructor(meta: IOptionsFieldMetaData, context: any) {
 		super(meta);
-		let options = typeof meta.options === 'function' ? meta.options() : meta.options;
+		let options;
+		if (typeof meta.options === 'function') {
+			const boundFn = meta.options.bind(context);
+			options = boundFn();
+		} else {
+			options = meta.options;
+		}
 		if (Array.isArray(options)) {
 			this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
 		} else {

+ 73 - 58
src/app/dynaform/services/_formdata-utils.ts

@@ -26,7 +26,6 @@ import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControlOptions
 import { cloneDeep, omit, reduce } from 'lodash/fp';
 import * as fmdModels from '../models/field.model';
 
-
 // ---------------------------------------------------------------------------------------------------------------------
 // AutoMeta: Generate Automatic Metadata from a model
 // ---------------------------------------------------------------------------------------------------------------------
@@ -160,61 +159,68 @@ const getRCBaseObjectWithAllKeys = (metaFoG, extraMeta, createFromExtra = false)
 // Build Form-Field-Type-Specific Metadata (using the field models in dynaform/models)
 // ---------------------------------------------------------------------------------------------------------------------
 
-const resolveType = (metaFoG: StringMap<any>): string => {
-	if (metaFoG.type) {
-		return metaFoG.type;
-	}
-	if (isContainer(metaFoG)) {
-		return 'container';
-	}
-	if (isRepeatingContainer(metaFoG)) {
-		return 'repeatingContainer';
-	}
-	return 'text';
-}
+// MAYBE CHANGE INTO A MODULE SO WE CAN USE CLOSURE BUT ALSO METHODS INSIDE IT
+// More Elegant But Not Urgent
 
-const buildFieldClassName = (t: string): string => {
-	const start = t[0].toUpperCase() + t.slice(1);
-	if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
-		return start;
-	}
-	return start + 'Field';
-};
+const buildModeledFieldGroupMember = metaFog => metaFog; // THIS MAY BREAK THINGS NOW WE'VE MOVED FUNCTION INTO CLOSURE BELOW
 
-const buildModeledField = (metaFoG, context) => {
-	const type = resolveType(metaFoG);
-	const className = buildFieldClassName(type);
-	if (!fmdModels[className]) {
-		throw new Error(`No metadata model "${className}" for type "${type}"`);
-	}
-	return new fmdModels[className](metaFoG, context);
-};
+const buildFieldSpecificMetaInClosure = (metaG, context) => {
 
-// Build Form Group Member
-const buildModeledFieldGroupMember = (metaFoG, context = {}) => {
-	const modeledGroupMember = buildModeledField(metaFoG, context);
-	if (isContainer(metaFoG)) {
-		modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
-	} else if (isRepeatingContainer(metaFoG)) {
-		modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
-		modeledGroupMember.__defaultContainer = {
-			...modeledGroupMember.meta[0],
-			meta: _buildFieldSpecificMeta(modeledGroupMember.__defaultContainer),
-			name: '__defaultContainer',
-			button: ''
-		};
+	const resolveType = (metaFoG: StringMap<any>): string => {
+		if (metaFoG.type) {
+			return metaFoG.type;
+		}
+		if (isContainer(metaFoG)) {
+			return 'container';
+		}
+		if (isRepeatingContainer(metaFoG)) {
+			return 'repeatingContainer';
+		}
+		return 'text';
 	}
-	return modeledGroupMember;
-};
-
-// Build Form Group
- const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
-const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
-	metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) : 
-	reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
-const buildFieldSpecificMeta = metaG => {
-	const withNames = addMissingNames(metaG);
-	return _buildFieldSpecificMeta(addMissingNames(metaG));
+	
+	const buildFieldClassName = (t: string): string => {
+		const start = t[0].toUpperCase() + t.slice(1);
+		if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
+			return start;
+		}
+		return start + 'Field';
+	};
+	
+	const buildModeledField = (metaFoG) => {
+		const type = resolveType(metaFoG);
+		const className = buildFieldClassName(type);
+		if (!fmdModels[className]) {
+			throw new Error(`No metadata model "${className}" for type "${type}"`);
+		}
+		return new fmdModels[className](metaFoG, context);
+	};
+	
+	// Build Form Group Member
+	const buildModeledFieldGroupMember = (metaFoG) => {
+		const modeledGroupMember = buildModeledField(metaFoG);
+		if (isContainer(metaFoG)) {
+			modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
+		} else if (isRepeatingContainer(metaFoG)) {
+			modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
+			modeledGroupMember.__defaultContainer = {
+				...modeledGroupMember.meta[0],
+				meta: _buildFieldSpecificMeta(modeledGroupMember.__defaultContainer),
+				name: '__defaultContainer',
+				button: ''
+			};
+		}
+		return modeledGroupMember;
+	};
+	
+	// Build Form Group
+	const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
+	const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
+		metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) : 
+		reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
+	const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
+
+	return buildFieldSpecificMeta(metaG);
 }
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -383,7 +389,8 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 		// Ensure that we have Field-Specific Metadata, not raw Objects
 		const metaWithNameKeys = addMissingNames(metaG); // <!--- DO WE REALLY HAVE TO CALL addMissingManes again here - it should have been done already?
 		// MAYBE only run this if first entry isn't right, for reasons of efficiency
-		const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
+		// const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
+		const fieldModeledMeta = metaWithNameKeys;
 		return _buildFormGroup(fieldModeledMeta);
 	};
 	return buildFormGroup;
@@ -454,6 +461,13 @@ const generateNewModel = (originalModel, updates) => {
 
 const updateObject = (obj, updates, createAdditionalKeys = false) => {
 	// THIS DOES NOT MUTATE obj, instead returning a new object
+	if (typeof obj !== 'object') {
+		obj = {};
+	}
+	console.log('obj is', obj, typeof obj);
+	if (Object.keys(obj).length === 0) {
+		createAdditionalKeys = true;
+	}
 	const shallowClone = { ...obj };
 	Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
 	return shallowClone;
@@ -468,8 +482,9 @@ const safeSet = (obj, key, val, createAdditionalKeys = false) => {
 	if (val === currentVal) {
 		return;
 	}
-	if (nullOrScaler(currentVal)) {
-		console.log('safeSet nullOrScaler', key, val);
+
+	if (undefinedNullOrScalar(currentVal)) {
+		console.log('safeSet undefinedNullOrScalar', key, val);
 		obj[key] = val;
 	} else {
 		if (Array.isArray(currentVal)) {
@@ -492,8 +507,8 @@ const safeSet = (obj, key, val, createAdditionalKeys = false) => {
 	}
 };
 
-const nullOrScaler = val => {
-	if (val === null) { return true; }
+const undefinedNullOrScalar = val => {
+	if (val === null || val === undefined) { return true; }
 	const t = typeof val;
 	return t === 'number' || t === 'string' || t === 'boolean';
 };
@@ -557,6 +572,6 @@ const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
 
 export {
 	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
-	buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory,
+	buildFieldSpecificMetaInClosure, extractFieldMappings, buildFormGroupFunctionFactory,
 	generateNewModel
 };

+ 9 - 3
src/app/dynaform/services/dynaform.service.ts

@@ -72,7 +72,7 @@
  * 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 metadta models to fill out metadata
+ * 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
@@ -97,7 +97,7 @@ import { ModelMapperService } from './model-mapper.service';
 
 import {
 	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
-	buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory, generateNewModel
+	buildFieldSpecificMetaInClosure, extractFieldMappings, buildFormGroupFunctionFactory, generateNewModel
 } from './_formdata-utils';
 import { formArrayNameProvider } from '@angular/forms/src/directives/reactive_directives/form_group_name';
 
@@ -115,6 +115,7 @@ export class DynaformService {
 
 	public form: FormGroup;
 	public meta: StringMap<any>;
+	private context: any;
 
 	public buildFormGroup: (meta) => FormGroup;
 	private buildStrategy: 'MODELFIRST' | 'METAFIRST' = 'MODELFIRST'; // Make ENUM type
@@ -137,6 +138,11 @@ export class DynaformService {
 		}
 	}
 
+	setContext(data: any): void {
+		// Set any runtime  data needed to build the form, e.g. options values
+		this.context = data;
+	}
+
 	build(model: StringMap<any>, meta = {}, createFromMeta = false): IFormAndMeta {
 		// Executes autoBuildFormGroupAndMeta and stores the result
 		const result = this.autoBuildFormGroupAndMeta(model, meta, createFromMeta);
@@ -219,7 +225,7 @@ export class DynaformService {
 	// Build field-type-specific metadata using the form field models (see dynaform/models)
 
 	buildFieldSpecificMeta(meta) {
-		return buildFieldSpecificMeta(meta);
+		return buildFieldSpecificMetaInClosure(meta, this.context);
 	}
 
 	// -----------------------------------------------------------------------------------------------------------------