|
@@ -6,19 +6,22 @@
|
|
|
* 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
|
|
|
* combineModelWithMeta(model, extraMeta) - automatically generate metadata from model then combine extra metadata
|
|
|
+ * execMetaReorderingInstructions(meta) - reorder metadata id ant 'before' or 'after' instructions are found
|
|
|
* buildFieldSpecificMeta(metadata) - use field-type-specific metadata models to expand metadata
|
|
|
+ * extractFieldMappings(metadata) - extrtact mappings from metadata 'source' attributes
|
|
|
* buildFormGroupFunctionFactory(fb) - return a function to buildFormGroups using the supplied FormBuilder singleton
|
|
|
+ * generateNewModel(originalModel, updates) - returns an updated copy of the model
|
|
|
*
|
|
|
* Variable names
|
|
|
* --------------
|
|
|
* metaF = metadata for Field
|
|
|
* metaG = metadata for Group (possibly nested)
|
|
|
- * metaFoG = metadata for Field Or Group
|
|
|
+ * metaFoG = metadata for Field or Group
|
|
|
*
|
|
|
*/
|
|
|
|
|
|
import { FormBuilder, FormGroup, FormArray, FormControl, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
|
|
|
-import { reduce } from 'lodash/fp';
|
|
|
+import { reduce, cloneDeep } from 'lodash/fp';
|
|
|
import * as fmdModels from '../models/field.model';
|
|
|
|
|
|
// REMINDER: Import this directly from @angular/forms when we upgrade to Angular 6
|
|
@@ -49,7 +52,8 @@ const keyValPairToMetaRecursive = ( [key, val] ) => {
|
|
|
const arrayMemberAutoName = val => isScalar(val) ? val : val.__typename || val.constructor.name;
|
|
|
const arrayToMeta = array => array.map(val => ({ name: arrayMemberAutoName(val), 'value' : val }));
|
|
|
|
|
|
-const autoMeta = model => Object.entries(model)
|
|
|
+const autoMeta = model => Object.entries(model)
|
|
|
+ .filter(kvPair => kvPair[0] !== '__')
|
|
|
.map(keyValPairToMetaRecursive)
|
|
|
.reduce((res, [key, val]) => addProp(res, key, val), {});
|
|
|
|
|
@@ -64,7 +68,7 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false) => {
|
|
|
Object.entries(extraMeta).forEach(([key, val]) => {
|
|
|
if (typeof metaG[key] === 'object' || createFromExtra) {
|
|
|
combinedMeta[key] = metaG[key] && (<any>val).meta ?
|
|
|
- combineMetaForField(metaG[key], { meta: combineExtraMeta(metaG[key].meta, (<any>val).meta, createFromExtra) }) :
|
|
|
+ combineMetaForField(metaG[key], { ...val, meta: combineExtraMeta(metaG[key].meta, (<any>val).meta, createFromExtra) }) :
|
|
|
combineMetaForField(metaG[key] || {}, val);
|
|
|
}
|
|
|
});
|
|
@@ -81,7 +85,7 @@ const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => comb
|
|
|
|
|
|
const buildFieldClassName = (t = 'text') => {
|
|
|
const start = t[0].toUpperCase() + t.slice(1);
|
|
|
- if (start === 'Container' || t.slice(-5) === 'Group') {
|
|
|
+ if (start === 'Container' || start === 'Heading' || t.slice(-5) === 'Group') {
|
|
|
return start;
|
|
|
}
|
|
|
return start + 'Field';
|
|
@@ -111,6 +115,131 @@ const _buildFieldSpecificMeta = metaG => reduce(buildModeledFieldGroupReducerIte
|
|
|
const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
|
|
|
|
|
|
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+// Generate mapping from source attributes
|
|
|
+// (used to grab data from model when using METAFIRST form generation)
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+
|
|
|
+// Each container CAN have a datasource instead of the original model
|
|
|
+// SO ... If container source is a functional mapping, store the mapping to get datasource object later
|
|
|
+
|
|
|
+const isAbsPath = path => typeof path === 'string' && path[0] === '/';
|
|
|
+const isRootPath = path => path === '/';
|
|
|
+const processPath = (parentPath, path) => isAbsPath(path) ? path : `${parentPath}.${path}`;
|
|
|
+
|
|
|
+const prependParentPathRecursive = (parentPath: string, obj: StringMap) => {
|
|
|
+ return Object.entries(obj)
|
|
|
+ .map( ([key, mapping] ) => {
|
|
|
+ let mappingRes;
|
|
|
+ switch(typeof mapping) {
|
|
|
+ case 'string':
|
|
|
+ mappingRes = processPath(parentPath, mapping);
|
|
|
+ break;
|
|
|
+ case 'object':
|
|
|
+ if (Array.isArray(mapping)) {
|
|
|
+ // A functional mapping of the form [fn, fn] or [source, fn, fn]
|
|
|
+ if (typeof mapping[0] === 'string') {
|
|
|
+ const source = processPath(parentPath, mapping[0]);
|
|
|
+ mappingRes = [source, ...mapping.slice(1)];
|
|
|
+ } else {
|
|
|
+ const source = processPath(parentPath, mapping);
|
|
|
+ mappingRes = [source, ...mapping];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ mappingRes = prependParentPathRecursive(parentPath, mapping)
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return [key, mappingRes];
|
|
|
+ })
|
|
|
+ .reduce((acc, [key, val]) => addProp(acc, key, val), {});
|
|
|
+}
|
|
|
+
|
|
|
+const _extractFieldMapping = ( [key, metaFoG] ) => {
|
|
|
+ let source;
|
|
|
+ if (isGroup(metaFoG)) {
|
|
|
+ if (Array.isArray(metaFoG.source)) {
|
|
|
+ source = extractFieldMappings(metaFoG.meta);
|
|
|
+ source.__ = metaFoG.source; // Store the functional mapping (including function executed later to provide container's data)
|
|
|
+ } else {
|
|
|
+ source = extractFieldMappings(metaFoG.meta, metaFoG.source || key);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ source = metaFoG.source || key;
|
|
|
+ }
|
|
|
+ return [key, source];
|
|
|
+};
|
|
|
+
|
|
|
+const extractFieldMappings = (metaG, parentPath = '') => Object.entries(metaG)
|
|
|
+ .map(_extractFieldMapping)
|
|
|
+ .reduce((res, [key, mapping]) => {
|
|
|
+
|
|
|
+ // Work out the path prefix
|
|
|
+ let prefix;
|
|
|
+ if (parentPath) {
|
|
|
+ if (isRootPath(parentPath) || isAbsPath(metaG[key].source) || Array.isArray(parentPath)) {
|
|
|
+ // If the parentPath is the root of the data structure, or the source is an absolute path or functional datasource,
|
|
|
+ // then there's no path prefix
|
|
|
+ prefix = ''
|
|
|
+ } else {
|
|
|
+ // Otherwise create a prefix from the parentPath
|
|
|
+ prefix = parentPath ? (parentPath[0] === '/' ? parentPath.slice(1) : parentPath) : '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Work out the mapping result
|
|
|
+ let mappingRes;
|
|
|
+ if (typeof mapping === 'string') {
|
|
|
+
|
|
|
+ // DIRECT MAPPING
|
|
|
+ if (mapping[0] === '/') {
|
|
|
+ mappingRes = mapping.slice(1);
|
|
|
+ } else if (prefix) {
|
|
|
+ mappingRes = `${prefix}.${mapping}`;
|
|
|
+ } else {
|
|
|
+ mappingRes = mapping;
|
|
|
+ }
|
|
|
+
|
|
|
+ } else if (Array.isArray(mapping)) {
|
|
|
+
|
|
|
+ // FUNCTIONAL MAPPING
|
|
|
+ // of the form [fn, fn] or [source, fn, fn]
|
|
|
+ if (prefix) {
|
|
|
+ if (typeof mapping[0] === 'function') {
|
|
|
+ // Mapping of form [fn, fn] with existing parent path
|
|
|
+ mappingRes = [`${prefix}.${key}`, ...mapping];
|
|
|
+ } else if (typeof mapping[0] === 'string') {
|
|
|
+ // Mapping of form [source, fn, fn] with existing parent path
|
|
|
+ const source = mapping[0][0] === '/' ? mapping[0].slice(1) : `${prefix}.${mapping[0]}`;
|
|
|
+ mappingRes = [source, ...mapping.slice(1)];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (typeof mapping[0] === 'function') {
|
|
|
+ // Mapping of form [fn, fn] with NO parent path
|
|
|
+ mappingRes = [key, ...mapping];
|
|
|
+ } else if (typeof mapping[0] === 'string') {
|
|
|
+ // Mapping of form [source, fn, fn] with NO parent path
|
|
|
+ const source = mapping[0][0] === '/' ? mapping[0].slice(1) : mapping[0];
|
|
|
+ mappingRes = [source, ...mapping.slice(1)];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ } else if (typeof mapping === 'object' && prefix && !mapping.__) {
|
|
|
+
|
|
|
+ // CONTAINER with parentPath, and WITHOUT its own functional datasource stored in __
|
|
|
+ // For every contained value recursively prepend the parentPath to give an absolute path
|
|
|
+ mappingRes = prependParentPathRecursive(prefix, mapping);
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ mappingRes = mapping;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return addProp(res, key, mappingRes);
|
|
|
+ }, {});
|
|
|
+
|
|
|
+
|
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
|
// Build Form Group Function Factory
|
|
|
// returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
|
|
@@ -125,7 +254,7 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
|
|
|
const buildValidators = (metaF): AbstractControlOptions => ({
|
|
|
validators: metaF.validators,
|
|
|
asyncValidators: metaF.asyncValidators,
|
|
|
- updateOn: 'change' // blur not working for custom components - maybe use change for custom and blur for text
|
|
|
+ updateOn: metaF.type === 'text' || metaF.type === 'textarea' ? 'blur' : 'change' // blur not working for custom components, so use change for custom and blur for text
|
|
|
});
|
|
|
const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
|
|
|
|
|
@@ -152,6 +281,114 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
|
|
|
};
|
|
|
|
|
|
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+// Reordering ( support for before / after instructions in metadata )
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+
|
|
|
+// Object slice function
|
|
|
+const slice = (obj, start, end = null) => Object.entries(obj)
|
|
|
+ .slice(start, end !== null ? end : Object.keys(obj).length)
|
|
|
+ .reduce((res, [key, val]) => addProp(res, key, val), {});
|
|
|
+
|
|
|
+const objConcat = (obj, pos, key, val = null) => {
|
|
|
+ const existsAlready = Object.keys(obj).indexOf(key);
|
|
|
+ if (existsAlready) {
|
|
|
+ val = obj[key];
|
|
|
+ }
|
|
|
+ const start = slice(obj, 0, pos);
|
|
|
+ const finish = slice(obj, pos);
|
|
|
+ delete start[key];
|
|
|
+ delete finish[key];
|
|
|
+ return { ...start, [key]: val, ...finish };
|
|
|
+}
|
|
|
+
|
|
|
+const insertBefore = (obj, beforeKey, key, val = null) => {
|
|
|
+ const targetPosition = Object.keys(obj).indexOf(beforeKey);
|
|
|
+ return objConcat(obj, targetPosition, key, val);
|
|
|
+}
|
|
|
+
|
|
|
+const insertAfter = (obj, afterKey, key, val = null) => {
|
|
|
+ const targetPosition = Object.keys(obj).indexOf(afterKey) + 1;
|
|
|
+ return objConcat(obj, targetPosition, key, val);
|
|
|
+}
|
|
|
+
|
|
|
+// Process reordeing instructions recursively
|
|
|
+const _execMetaReorderingInstructions = (metaG: StringMap) => {
|
|
|
+ let reorderedGroup = { ...metaG };
|
|
|
+ Object.entries(metaG).forEach(([key, metaFoG]) => {
|
|
|
+ if (metaFoG.before) {
|
|
|
+ reorderedGroup = insertBefore(reorderedGroup, metaFoG.before, key);
|
|
|
+ } else if (metaFoG.after) {
|
|
|
+ reorderedGroup = insertAfter(reorderedGroup, metaFoG.after, key);
|
|
|
+ }
|
|
|
+ if (isContainer(metaFoG)) {
|
|
|
+ reorderedGroup[key].meta = execMetaReorderingInstructions(metaFoG.meta);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return reorderedGroup;
|
|
|
+}
|
|
|
+
|
|
|
+const execMetaReorderingInstructions = (metaG: StringMap) => {
|
|
|
+ return _execMetaReorderingInstructions(cloneDeep(metaG));
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+// Generate new model, without mutating original
|
|
|
+// (used to produce an updated copy of a model when form values are changed - will not create new keys)
|
|
|
+// ---------------------------------------------------------------------------------------------------------------------
|
|
|
+
|
|
|
+const generateNewModel = (originalModel, updates) => {
|
|
|
+ return updateObject(originalModel, updates);
|
|
|
+}
|
|
|
+
|
|
|
+const updateObject = (obj, updates, createAdditionalKeys = false) => {
|
|
|
+ // THIS DOES NOT MUTATE obj, instead returning a new object
|
|
|
+ const shallowClone = { ...obj };
|
|
|
+ Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
|
|
|
+ return shallowClone;
|
|
|
+};
|
|
|
+
|
|
|
+const safeSet = (obj, key, val, createAdditionalKeys = false) => {
|
|
|
+ // THIS MUTATES obj - consider the wisdom of this
|
|
|
+ if (!createAdditionalKeys && !obj.hasOwnProperty(key)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let currentVal = obj[key];
|
|
|
+ if (val === currentVal) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (nullOrScaler(currentVal)) {
|
|
|
+ console.log('safeSet nullOrScaler', key, val);
|
|
|
+ obj[key] = val;
|
|
|
+ } else {
|
|
|
+ if (Array.isArray(currentVal)) {
|
|
|
+ if (typeof val === 'object') {
|
|
|
+ // Replace array
|
|
|
+ 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?
|
|
|
+ throw new Error(`safeSet Error: Expected array or object @key ${key} but got scalar
|
|
|
+ Rejected update was ${val}`);
|
|
|
+ }
|
|
|
+ } else if (typeof val === 'object') {
|
|
|
+ // Deep merge
|
|
|
+ obj[key] = updateObject(obj[key], val, createAdditionalKeys);
|
|
|
+ } else {
|
|
|
+ throw new Error(`safeSet Error: Can't deep merge object into scalar @key ${key}
|
|
|
+ Rejected update was ${val}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const nullOrScaler = val => {
|
|
|
+ if (val === null) return true;
|
|
|
+ const t = typeof val;
|
|
|
+ return t === 'number' || t === 'string' || t === 'boolean';
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
|
// Helper Functions
|
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -199,4 +436,8 @@ const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
|
|
|
// Exports
|
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
|
|
|
|
-export { autoMeta, combineModelWithMeta, combineExtraMeta, buildFieldSpecificMeta, buildFormGroupFunctionFactory };
|
|
|
+export {
|
|
|
+ autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
|
|
|
+ buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory,
|
|
|
+ generateNewModel
|
|
|
+};
|