|
@@ -22,7 +22,7 @@
|
|
|
*
|
|
|
*/
|
|
|
|
|
|
-import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControlOptions } from '@angular/forms';
|
|
|
+import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControl, AbstractControlOptions } from '@angular/forms';
|
|
|
import { cloneDeep, omit, reduce } from 'lodash/fp';
|
|
|
import * as fmdModels from '../models/field.model';
|
|
|
|
|
@@ -93,7 +93,7 @@ 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(
|
|
|
cloneDeep(baseObjWithAllKeys),
|
|
@@ -138,7 +138,7 @@ const generateRepeatedGroup = (metaFoG, extraMeta, baseObjWithAllKeys): StringMa
|
|
|
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 ?
|
|
|
+ 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 });
|
|
@@ -179,7 +179,7 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
|
|
|
}
|
|
|
return 'text';
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const buildFieldClassName = (t: string): string => {
|
|
|
const start = t[0].toUpperCase() + t.slice(1);
|
|
|
if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
|
|
@@ -187,7 +187,7 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
|
|
|
}
|
|
|
return start + 'Field';
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
const buildModeledField = (metaFoG) => {
|
|
|
const type = resolveType(metaFoG);
|
|
|
const className = buildFieldClassName(type);
|
|
@@ -196,7 +196,7 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
|
|
|
}
|
|
|
return new fmdModels[className](metaFoG, context);
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
// Build Form Group Member
|
|
|
const buildModeledFieldGroupMember = (metaFoG) => {
|
|
|
const modeledGroupMember = buildModeledField(metaFoG);
|
|
@@ -213,11 +213,11 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
|
|
|
}
|
|
|
return modeledGroupMember;
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
// Build Form Group
|
|
|
const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
|
|
|
const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
|
|
|
- metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) :
|
|
|
+ metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) :
|
|
|
reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
|
|
|
const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
|
|
|
|
|
@@ -355,36 +355,51 @@ 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
|
|
|
+
|
|
|
const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup => {
|
|
|
// Establishes a closure over the supplied FormBuilder and returns a function that builds FormGroups from metadata
|
|
|
// ( it's done this way so we can use the FormBuilder singleton without reinitialising )
|
|
|
|
|
|
// Build Form Control
|
|
|
const buildControlState = metaF => ({ value: metaF.value || metaF.default, disabled: metaF.disabled });
|
|
|
- const buildValidators = (metaF): AbstractControlOptions => ({
|
|
|
- validators: metaF.validators,
|
|
|
- asyncValidators: metaF.asyncValidators,
|
|
|
+ const buildValidators = (metaFoG): AbstractControlOptions => ({
|
|
|
+ validators: metaFoG.validators,
|
|
|
+ asyncValidators: metaFoG.asyncValidators,
|
|
|
// blur not working for custom components, so use change for custom and blur for text
|
|
|
- updateOn: metaF.type === 'text' || metaF.type === 'textarea' ? 'blur' : 'change'
|
|
|
+ updateOn: getUpdateOn(metaFoG.type)
|
|
|
});
|
|
|
+ const BVAL = metaFoG => {
|
|
|
+ if (!metaFoG || !(metaFoG.validators || metaFoG.asyncValidators)) {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ // console.log(metaFoG);
|
|
|
+ const res = buildValidators(metaFoG);
|
|
|
+ // console.log(res);
|
|
|
+ return res;
|
|
|
+ }
|
|
|
const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
|
|
|
|
|
|
// Build Form Array containing either Form Controls or Form Groups
|
|
|
- const buildFormArray = (metaG): FormArray => {
|
|
|
+ const buildFormArray = (metaG, grMeta?): FormArray => {
|
|
|
return fb.array(
|
|
|
- metaG.map(m => isContainer(m) ? _buildFormGroup(m.meta) : buildFormControl(m))
|
|
|
+ metaG.map(m => isContainer(m) ? _buildFormGroup(m.meta) : buildFormControl(m)),
|
|
|
+ buildValidators(grMeta)
|
|
|
);
|
|
|
};
|
|
|
|
|
|
- // Build Form Group Member - builds a FormControl, FormArray or another FormGroup - which in turn can contain any of these
|
|
|
+ // Build Form Group Member
|
|
|
+ // Builds a FormControl, FormArray or another FormGroup - which in turn can contain any of these
|
|
|
const buildFormGroupMember = metaFoG => isGroup(metaFoG) ?
|
|
|
- (isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta) : _buildFormGroup(metaFoG.meta)) :
|
|
|
+ (isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta, metaFoG) : _buildFormGroup(metaFoG.meta, metaFoG)) : // TODO: STINKY! REWORK with 1 param
|
|
|
buildFormControl(metaFoG);
|
|
|
|
|
|
const buildFormGroupReducerIteree = (res, metaFoG) => {
|
|
|
return metaFoG.noFormControls ? res : { ...res, [metaFoG.name]: buildFormGroupMember(metaFoG) };
|
|
|
};
|
|
|
- const _buildFormGroup = _metaG => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG));
|
|
|
+ const _buildFormGroup = (_metaG, grMeta?) => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG), BVAL(grMeta));
|
|
|
|
|
|
// The main function - builds FormGroups containing other FormGroups, FormArrays and FormControls
|
|
|
const buildFormGroup = metaG => {
|
|
@@ -398,6 +413,72 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
|
|
|
return buildFormGroup;
|
|
|
};
|
|
|
|
|
|
+// Get the 'update on' strategy for validators
|
|
|
+const getUpdateOn = (type: string): 'blur'|'change'|'submit' => {
|
|
|
+ const t = type.toLowerCase();
|
|
|
+ let res;
|
|
|
+ if (t === 'text' || t === 'textarea' || t === 'password') {
|
|
|
+ res = 'blur';
|
|
|
+ } else {
|
|
|
+ res = 'change';
|
|
|
+ }
|
|
|
+ return res;
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+// Touch and update the validity of all controls in a FormGroup or FormArray / Reset validity of all controls
|
|
|
+// useful for displaying validation failures on submit
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+
|
|
|
+const touchAndUpdateValidityRecursive = (group: FormGroup | FormArray): void => {
|
|
|
+ group.markAsTouched();
|
|
|
+ group.updateValueAndValidity();
|
|
|
+ for (const key in group.controls) {
|
|
|
+ if (group.controls[key] instanceof FormControl) {
|
|
|
+ group.controls[key].markAsTouched();
|
|
|
+ group.controls[key].updateValueAndValidity();
|
|
|
+ } else {
|
|
|
+ touchAndUpdateValidityRecursive(group.controls[key]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const resetValidityRecursive = (group: FormGroup | FormArray): void => {
|
|
|
+ group.markAsUntouched();
|
|
|
+ group.updateValueAndValidity();
|
|
|
+ for (const key in group.controls) {
|
|
|
+ if (group.controls[key] instanceof FormControl) {
|
|
|
+ group.controls[key].markAsUntouched();
|
|
|
+ group.controls[key].updateValueAndValidity();
|
|
|
+ } else {
|
|
|
+ resetValidityRecursive(group.controls[key]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const advanceUpdateStategyOfInvalidControlsRecursive = (group: FormGroup | FormArray): void => {
|
|
|
+ // For all invalid text / select / password control, change the updateOn startegy from 'blur' to 'change'
|
|
|
+ // For as-you-type feedback if validation has failed once
|
|
|
+ // NB: Only for the syncronous validators
|
|
|
+ for (const key in group.controls) {
|
|
|
+ if (group.controls[key] instanceof FormControl) {
|
|
|
+ console.log(key, group.controls[key].touched, group.controls[key].invalid);
|
|
|
+ if (group.controls[key].touched && group.controls[key].invalid) {
|
|
|
+ console.log('Replacing control', key);
|
|
|
+ const newControl = new FormControl(
|
|
|
+ group.controls[key].value,
|
|
|
+ { updateOn: 'change', validators: group.controls[key].validator },
|
|
|
+ group.controls[key].asyncValidator
|
|
|
+ );
|
|
|
+ (group as any).setControl(key as any, newControl);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ advanceUpdateStategyOfInvalidControlsRecursive(group.controls[key]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
|
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
|
// Reordering ( support for before / after instructions in metadata )
|
|
@@ -463,16 +544,51 @@ const generateNewModel = (originalModel, updates) => {
|
|
|
return updateObject(originalModel, updates);
|
|
|
};
|
|
|
|
|
|
+const updateMeta = (newMeta: StringMap<any>, path: string, meta: StringMap<any>): StringMap<any> => {
|
|
|
+ // TODO: Finish this later
|
|
|
+ if (path === '/') {
|
|
|
+ const updatedMeta = updateObject(meta || this.meta, newMeta, true);
|
|
|
+ return updatedMeta;
|
|
|
+ }
|
|
|
+ // Drill down and update the branch specified by 'path' - all but final segment indicates a container
|
|
|
+ // What about array types? Think about this later!
|
|
|
+ // What about group components. Think about this later.
|
|
|
+ // What about bad paths. Thinkl about this later.
|
|
|
+ console.log(path);
|
|
|
+ const segments = path.split('.');
|
|
|
+ console.log(segments);
|
|
|
+ let branchMeta = meta;
|
|
|
+ while(isContainer(branchMeta)) {
|
|
|
+ const s = segments.shift();
|
|
|
+ console.log(s, branchMeta[s].meta);
|
|
|
+ // TODO: add array check
|
|
|
+ branchMeta = branchMeta[s].meta;
|
|
|
+ }
|
|
|
+ while(segments.length > 1) {
|
|
|
+ const s = segments.shift();
|
|
|
+ console.log(s, branchMeta[s]);
|
|
|
+ // TODO: add array check
|
|
|
+ branchMeta = branchMeta[s];
|
|
|
+ }
|
|
|
+ branchMeta = branchMeta[segments[0]];
|
|
|
+ console.log(segments[0], branchMeta);
|
|
|
+
|
|
|
+ // Then something like...
|
|
|
+ const updatedMeta = updateObject(branchMeta, newMeta, true);
|
|
|
+ branchMeta = updatedMeta;
|
|
|
+ console.log(branchMeta);
|
|
|
+ return meta;
|
|
|
+}
|
|
|
+
|
|
|
const updateObject = (obj, updates, createAdditionalKeys = false) => {
|
|
|
// THIS DOES NOT MUTATE obj, instead returning a new object
|
|
|
if (!isRealObject(obj)) {
|
|
|
obj = {};
|
|
|
}
|
|
|
- console.log('obj is', obj, typeof obj);
|
|
|
if (Object.keys(obj).length === 0) {
|
|
|
createAdditionalKeys = true;
|
|
|
}
|
|
|
- const shallowClone = { ...obj };
|
|
|
+ const shallowClone = { ...obj }; // This might be inefficient - consider using immutable or immer
|
|
|
Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
|
|
|
return shallowClone;
|
|
|
};
|
|
@@ -488,7 +604,7 @@ const safeSet = (obj, key, val, createAdditionalKeys = false) => {
|
|
|
}
|
|
|
|
|
|
if (undefinedNullOrScalar(currentVal)) {
|
|
|
- console.log('safeSet undefinedNullOrScalar', key, val);
|
|
|
+ // console.log('safeSet undefinedNullOrScalar', key, val);
|
|
|
obj[key] = val;
|
|
|
} else {
|
|
|
if (Array.isArray(currentVal)) {
|
|
@@ -576,6 +692,8 @@ const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
|
|
|
|
|
|
export {
|
|
|
autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
|
|
|
- buildFieldSpecificMetaInClosure, extractFieldMappings, buildFormGroupFunctionFactory,
|
|
|
- generateNewModel
|
|
|
+ buildFieldSpecificMetaInClosure, extractFieldMappings,
|
|
|
+ buildFormGroupFunctionFactory,
|
|
|
+ touchAndUpdateValidityRecursive, resetValidityRecursive, advanceUpdateStategyOfInvalidControlsRecursive,
|
|
|
+ generateNewModel, updateMeta,
|
|
|
};
|