123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- /*
- * FORM METADATA UTILITIES
- *
- * Exports
- * -------
- * 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
- * rcMem = Repeating Container member
- * metaFoRCMem = metadata for Field or Repeating Container member
- *
- */
- import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControlOptions } from '@angular/forms';
- import { cloneDeep, omit, reduce } from 'lodash/fp';
- import * as fmdModels from '../models/field.model';
- import { ComponentFactoryResolver } from '@angular/core/src/render3';
- // ---------------------------------------------------------------------------------------------------------------------
- // AutoMeta: Generate Automatic Metadata from a model
- // ---------------------------------------------------------------------------------------------------------------------
- const isScalar = val => typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string';
- const isArray = val => Array.isArray(val);
- const keyValPairToMeta = (val, key) => ({ name: key, [isScalar(val) ? 'value' : 'meta']: val });
- const keyValPairToMetaRecursive = ( [key, val] ) => {
- if (val === null || val === undefined) {
- val = ''; // Treat null or undefined as empty string, for purpose of form
- }
- const innerVal = isScalar(val) ? val : (isArray(val) ? arrayToMeta(val) : autoMeta(val));
- const metaVal = keyValPairToMeta(innerVal, key);
- return [key, metaVal];
- };
- const arrayToMeta = array => array.map((val, i) => {
- if (isScalar(val)) {
- return { name: `item${i + 1}`, 'value' : val };
- } else {
- return { name: `group${i + 1}`, meta: autoMeta(val) };
- }
- });
- const autoMeta = model => Object.entries(model)
- .filter(kvPair => kvPair[0] !== '__')
- .map(keyValPairToMetaRecursive)
- .reduce((res, [key, val]) => addProp(res, key, val), {});
- // ---------------------------------------------------------------------------------------------------------------------
- // Combine automatically generated metadata with overrides (extraMeta)
- // ---------------------------------------------------------------------------------------------------------------------
- // containerSeed = Metadata from container which seeds all contained fields
- const combineMetaForField = (metaF, containerSeed, extraMetaF) => ({ ...metaF, ...containerSeed, ...extraMetaF });
- const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSeed = {}) => {
- const combinedMeta = {};
- Object.entries(extraMeta).forEach(([key, val]) => {
- if (typeof metaG[key] === 'object' || createFromExtra) { // If the key exists (in the model) OR we're creating from metadata
- const isCon = isContainer(val);
- const isRepeating = isRepeatingContainer(val);
- const metaFoG = metaG[key] || {};
- const seed = isCon ? {} : containerSeed; // Container's don't seed themselves, only their children
- if (isRepeating)
- {
- // We've got a Repeating Container
- metaFoG.meta = generateRepeatedGroup(metaFoG, val, createFromExtra);
- const extra = {
- ...omit(['meta', 'seed'], val),
- meta: metaFoG.meta.map(
- rgMem => combineExtraMeta(
- rgMem.meta,
- val['meta'][0],
- createFromExtra,
- val['seed'] || containerSeed
- )
- )
- };
- combinedMeta[key] = combineMetaForField(metaFoG, {}, extra);
- }
- else
- {
- // We've got a Container or a Field
- const extra = isCon ?
- {
- ...val,
- meta: combineExtraMeta( // RECURSION
- metaFoG.meta || {},
- val['meta'],
- createFromExtra,
- val['seed'] || containerSeed // Inherit seeded data if this group's seed isn't set
- )
- }
- :
- val;
- combinedMeta[key] = combineMetaForField(metaFoG, seed, extra);
- }
- }
- });
- return { ...metaG, ...combinedMeta };
- };
- // Combine model with overrides (after automatically generating metadata from the model)
- const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => combineExtraMeta(autoMeta(model), extraMeta, createFromExtra);
- // <--- Utilities supporting Repreating Containers --->
- const generateRepeatedGroup = (metaFoG, extraMeta, createFromExtra = false): StringMap[] => {
- // Calculate the number of repeats
- const repeatInAutoMeta = Array.isArray(metaFoG.meta) ? metaFoG.meta.length : 0;
- const repeatInExtraMeta = extraMeta['initialRepeat'] || extraMeta['minRepeat'];
- const repeat = Math.max(repeatInAutoMeta, repeatInExtraMeta);
- // If creating from extra, make sure all group members have all keys in both model and meta (as this is a repeating group)
- const keysFromModel = repeatInAutoMeta ? Object.keys(metaFoG.meta[0].meta) : [];
- const keysFromExtraMeta = extraMeta['meta'] && extraMeta['meta'][0] ? Object.keys(extraMeta['meta'][0]) : [];
- const keysToInclude = createFromExtra ? Array.from(new Set([...keysFromModel, ...keysFromExtraMeta])) : keysFromModel;
- const baseObjWithAllKeys = autoMeta(keysToInclude.reduce((acc, key) => addProp(acc, key, ''), {}));
- 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 });
- return fullyNamedRepeatedGroup;
- }
- // ---------------------------------------------------------------------------------------------------------------------
- // Build Form-Field-Type-Specific Metadata (using the field models in dynaform/models)
- // ---------------------------------------------------------------------------------------------------------------------
- const resolveType = (metaFoG: StringMap): string => {
- if (metaFoG.type) {
- return metaFoG.type;
- }
- if (isContainer(metaFoG)) {
- return 'container';
- }
- if (isRepeatingContainer(metaFoG)) {
- return 'repeatingContainer';
- }
- 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') {
- 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);
- };
- // 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) }));
- }
- 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));
- }
- // ---------------------------------------------------------------------------------------------------------------------
- // 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
- // ---------------------------------------------------------------------------------------------------------------------
- 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,
- // blur not working for custom components, so use change for custom and blur for text
- updateOn: metaF.type === 'text' || metaF.type === 'textarea' ? 'blur' : 'change'
- });
- const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
- // Build Form Array containing either Form Controls or Form Groups
- const buildFormArray = (metaG): FormArray => {
- return fb.array(
- metaG.map(m => isContainer(m) ? _buildFormGroup(m.meta) : buildFormControl(m))
- );
- };
- // 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)) :
- buildFormControl(metaFoG);
- const buildFormGroupReducerIteree = (res, metaFoG) => {
- return metaFoG.noFormControls ? res : { ...res, [metaFoG.name]: buildFormGroupMember(metaFoG) };
- };
- const _buildFormGroup = _metaG => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG));
- // The main function - builds FormGroups containing other FormGroups, FormArrays and FormControls
- const buildFormGroup = metaG => {
- // 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);
- return _buildFormGroup(fieldModeledMeta);
- };
- return buildFormGroup;
- };
- // ---------------------------------------------------------------------------------------------------------------------
- // 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) => {
- // Repeating Containers (which have array meta *at this point*) can't be reordered, but other types of containers can
- return Array.isArray(metaG) ? cloneDeep(metaG) : _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;
- }
- const 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
- // ---------------------------------------------------------------------------------------------------------------------
- // Add Property to object, returning updated object
- const addProp = (obj, key, val) => { obj[key] = val; return obj; };
- // Is Group
- // Helper function to distinguish group from field
- const isGroup = (metaFoG): boolean => !!metaFoG.meta;
- // Is Container
- // Helper function to distinguish container group (a group of child fields)
- const isContainer = (metaFoG): boolean => isGroup(metaFoG)
- && !Array.isArray(metaFoG.meta)
- && (!metaFoG.type || metaFoG.type.toLowerCase() === 'container');
- // Is Repeating Container
- // Helper function to distinguish a repeating container group (a group of child fields that can be repeated 1...N times)
- const isRepeatingContainer = (metaFoG): boolean => isGroup(metaFoG)
- && Array.isArray(metaFoG.meta)
- && (!metaFoG.type || metaFoG.type.toLowerCase() === 'repeatingContainer');
- // Add Missing Names
- // Helper function to add any missing 'name' properties to Fields and Groups using property's key, recursively
- // BUT not to repeatingContainer members
- const addNameIfMissing = (metaFoG, key) => metaFoG.name ? metaFoG : addProp(metaFoG, 'name', key);
- const addNameToSelfAndChildren = ( [key, metaFoG] ) => {
- metaFoG = addNameIfMissing(metaFoG, key);
- if (isGroup(metaFoG) && !isRepeatingContainer(metaFoG)) {
- metaFoG.meta = isArray(metaFoG.meta) ? Object.values(addMissingNames(metaFoG.meta)) : addMissingNames(metaFoG.meta); // Recursion
- }
- return [key, metaFoG];
- };
- const addMissingNames = metaG => Object.entries(metaG)
- .map(addNameToSelfAndChildren)
- .reduce((res, [key, val]) => addProp(res, key, val), {});
- // Add Missing Field-Specific Meta
- // Helper function to add any missing Field-Specific Metadata (using models in dynaform/models), recursively
- // Checks the constrctor, which should NOT be a plain Object, but rather TextField, TextareaField, SelectField, etc.
- const add_FSM_IfMissing = metaFoG => metaFoG.constructor.name === 'Object' ? buildModeledFieldGroupMember(metaFoG) : metaFoG;
- const add_FSM_ToSelfAndChildren = ( [key, metaFoG] ) => {
- metaFoG = add_FSM_IfMissing(metaFoG);
- return [key, metaFoG];
- };
- const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
- .map(add_FSM_ToSelfAndChildren)
- .reduce((res, [key, val]) => addProp(res, key, val), {});
- // ---------------------------------------------------------------------------------------------------------------------
- // Exports
- // ---------------------------------------------------------------------------------------------------------------------
- export {
- autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
- buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory,
- generateNewModel
- };
|