_formdata-utils.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. /*
  2. * FORM METADATA UTILITIES
  3. *
  4. * Exports
  5. * -------
  6. * autoMeta(model) - generate basic metadata from a raw or mapped model, recursively if necessary
  7. * combineExtraMeta(metadata, extraMeta) - combine extra metadata into metatdata, lazyly and recursively
  8. * combineModelWithMeta(model, extraMeta) - automatically generate metadata from model then combine extra metadata
  9. * execMetaReorderingInstructions(meta) - reorder metadata id ant 'before' or 'after' instructions are found
  10. * buildFieldSpecificMeta(metadata) - use field-type-specific metadata models to expand metadata
  11. * extractFieldMappings(metadata) - extrtact mappings from metadata 'source' attributes
  12. * buildFormGroupFunctionFactory(fb) - return a function to buildFormGroups using the supplied FormBuilder singleton
  13. * generateNewModel(originalModel, updates) - returns an updated copy of the model
  14. *
  15. * Variable names
  16. * --------------
  17. * metaF = metadata for Field
  18. * metaG = metadata for Group (possibly nested)
  19. * metaFoG = metadata for Field or Group
  20. * rcMem = Repeating Container member
  21. * metaFoRCMem = metadata for Field or Repeating Container member
  22. *
  23. */
  24. import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControlOptions } from '@angular/forms';
  25. import { cloneDeep, omit, reduce } from 'lodash/fp';
  26. import * as fmdModels from '../models/field.model';
  27. import { ComponentFactoryResolver } from '@angular/core/src/render3';
  28. // ---------------------------------------------------------------------------------------------------------------------
  29. // AutoMeta: Generate Automatic Metadata from a model
  30. // ---------------------------------------------------------------------------------------------------------------------
  31. const isScalar = val => typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string';
  32. const isArray = val => Array.isArray(val);
  33. const keyValPairToMeta = (val, key) => ({ name: key, [isScalar(val) ? 'value' : 'meta']: val });
  34. const keyValPairToMetaRecursive = ( [key, val] ) => {
  35. if (val === null || val === undefined) {
  36. val = ''; // Treat null or undefined as empty string, for purpose of form
  37. }
  38. const innerVal = isScalar(val) ? val : (isArray(val) ? arrayToMeta(val) : autoMeta(val));
  39. const metaVal = keyValPairToMeta(innerVal, key);
  40. return [key, metaVal];
  41. };
  42. const arrayToMeta = array => array.map((val, i) => {
  43. if (isScalar(val)) {
  44. return { name: `item${i + 1}`, 'value' : val };
  45. } else {
  46. return { name: `group${i + 1}`, meta: autoMeta(val) };
  47. }
  48. });
  49. const autoMeta = model => Object.entries(model)
  50. .filter(kvPair => kvPair[0] !== '__')
  51. .map(keyValPairToMetaRecursive)
  52. .reduce((res, [key, val]) => addProp(res, key, val), {});
  53. // ---------------------------------------------------------------------------------------------------------------------
  54. // Combine automatically generated metadata with overrides (extraMeta)
  55. // ---------------------------------------------------------------------------------------------------------------------
  56. // containerSeed = Metadata from container which seeds all contained fields
  57. const combineMetaForField = (metaF, containerSeed, extraMetaF) => ({ ...metaF, ...containerSeed, ...extraMetaF });
  58. const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSeed = {}) => {
  59. const combinedMeta = {};
  60. Object.entries(extraMeta).forEach(([key, val]) => {
  61. if (typeof metaG[key] === 'object' || createFromExtra) { // If the key exists (in the model) OR we're creating from metadata
  62. const isCon = isContainer(val);
  63. const isRepeating = isRepeatingContainer(val);
  64. const metaFoG = metaG[key] || {};
  65. const seed = isCon ? {} : containerSeed; // Container's don't seed themselves, only their children
  66. if (isRepeating)
  67. {
  68. // We've got a Repeating Container
  69. metaFoG.meta = generateRepeatedGroup(metaFoG, val, createFromExtra);
  70. const extra = {
  71. ...omit(['meta', 'seed'], val),
  72. meta: metaFoG.meta.map(
  73. rgMem => combineExtraMeta(
  74. rgMem.meta,
  75. val['meta'][0],
  76. createFromExtra,
  77. val['seed'] || containerSeed
  78. )
  79. )
  80. };
  81. combinedMeta[key] = combineMetaForField(metaFoG, {}, extra);
  82. }
  83. else
  84. {
  85. // We've got a Container or a Field
  86. const extra = isCon ?
  87. {
  88. ...val,
  89. meta: combineExtraMeta( // RECURSION
  90. metaFoG.meta || {},
  91. val['meta'],
  92. createFromExtra,
  93. val['seed'] || containerSeed // Inherit seeded data if this group's seed isn't set
  94. )
  95. }
  96. :
  97. val;
  98. combinedMeta[key] = combineMetaForField(metaFoG, seed, extra);
  99. }
  100. }
  101. });
  102. return { ...metaG, ...combinedMeta };
  103. };
  104. // Combine model with overrides (after automatically generating metadata from the model)
  105. const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => combineExtraMeta(autoMeta(model), extraMeta, createFromExtra);
  106. // <--- Utilities supporting Repreating Containers --->
  107. const generateRepeatedGroup = (metaFoG, extraMeta, createFromExtra = false): StringMap[] => {
  108. // Calculate the number of repeats
  109. const repeatInAutoMeta = Array.isArray(metaFoG.meta) ? metaFoG.meta.length : 0;
  110. const repeatInExtraMeta = extraMeta['initialRepeat'] || extraMeta['minRepeat'];
  111. const repeat = Math.max(repeatInAutoMeta, repeatInExtraMeta);
  112. // If creating from extra, make sure all group members have all keys in both model and meta (as this is a repeating group)
  113. const keysFromModel = repeatInAutoMeta ? Object.keys(metaFoG.meta[0].meta) : [];
  114. const keysFromExtraMeta = extraMeta['meta'] && extraMeta['meta'][0] ? Object.keys(extraMeta['meta'][0]) : [];
  115. const keysToInclude = createFromExtra ? Array.from(new Set([...keysFromModel, ...keysFromExtraMeta])) : keysFromModel;
  116. const baseObjWithAllKeys = autoMeta(keysToInclude.reduce((acc, key) => addProp(acc, key, ''), {}));
  117. metaFoG.meta = metaFoG.meta.map( rcMem => ({ ...rcMem, meta: { ...baseObjWithAllKeys, ...rcMem.meta } }) ); // Add extra keys to model meta
  118. // Extend repeated group from model (if any) to correct length, and add any missing names
  119. const repeatedGroup = repeatInAutoMeta ?
  120. [ ...metaFoG.meta, ...Array(repeat - repeatInAutoMeta).fill({ meta: baseObjWithAllKeys }) ] :
  121. Array(repeat).fill({ meta: baseObjWithAllKeys });
  122. const fullyNamedRepeatedGroup = repeatedGroup.map((rgMem, i) => rgMem.name ? rgMem : { name: `group${i + 1}`, ...rgMem });
  123. return fullyNamedRepeatedGroup;
  124. }
  125. // ---------------------------------------------------------------------------------------------------------------------
  126. // Build Form-Field-Type-Specific Metadata (using the field models in dynaform/models)
  127. // ---------------------------------------------------------------------------------------------------------------------
  128. const resolveType = (metaFoG: StringMap): string => {
  129. if (metaFoG.type) {
  130. return metaFoG.type;
  131. }
  132. if (isContainer(metaFoG)) {
  133. return 'container';
  134. }
  135. if (isRepeatingContainer(metaFoG)) {
  136. return 'repeatingContainer';
  137. }
  138. return 'text';
  139. }
  140. const buildFieldClassName = (t: string): string => {
  141. const start = t[0].toUpperCase() + t.slice(1);
  142. if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
  143. return start;
  144. }
  145. return start + 'Field';
  146. };
  147. const buildModeledField = metaFoG => {
  148. const type = resolveType(metaFoG);
  149. const className = buildFieldClassName(type);
  150. if (!fmdModels[className]) {
  151. throw new Error(`No metadata model "${className}" for type "${type}"`);
  152. }
  153. return new fmdModels[className](metaFoG);
  154. };
  155. // Build Form Group Member
  156. const buildModeledFieldGroupMember = metaFoG => {
  157. const modeledGroupMember = buildModeledField(metaFoG);
  158. if (isContainer(metaFoG)) {
  159. modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
  160. } else if (isRepeatingContainer(metaFoG)) {
  161. modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
  162. }
  163. return modeledGroupMember;
  164. };
  165. // Build Form Group
  166. const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
  167. const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
  168. metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) :
  169. reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
  170. const buildFieldSpecificMeta = metaG => {
  171. const withNames = addMissingNames(metaG);
  172. return _buildFieldSpecificMeta(addMissingNames(metaG));
  173. }
  174. // ---------------------------------------------------------------------------------------------------------------------
  175. // Generate mapping from source attributes
  176. // (used to grab data from model when using METAFIRST form generation)
  177. // ---------------------------------------------------------------------------------------------------------------------
  178. // Each container CAN have a datasource instead of the original model
  179. // SO ... If container source is a functional mapping, store the mapping to get datasource object later
  180. const isAbsPath = path => typeof path === 'string' && path[0] === '/';
  181. const isRootPath = path => path === '/';
  182. const processPath = (parentPath, path) => isAbsPath(path) ? path : `${parentPath}.${path}`;
  183. const prependParentPathRecursive = (parentPath: string, obj: StringMap) => {
  184. return Object.entries(obj)
  185. .map( ([key, mapping] ) => {
  186. let mappingRes;
  187. switch (typeof mapping) {
  188. case 'string':
  189. mappingRes = processPath(parentPath, mapping);
  190. break;
  191. case 'object':
  192. if (Array.isArray(mapping)) {
  193. // A functional mapping of the form [fn, fn] or [source, fn, fn]
  194. if (typeof mapping[0] === 'string') {
  195. const source = processPath(parentPath, mapping[0]);
  196. mappingRes = [source, ...mapping.slice(1)];
  197. } else {
  198. const source = processPath(parentPath, mapping);
  199. mappingRes = [source, ...mapping];
  200. }
  201. } else {
  202. mappingRes = prependParentPathRecursive(parentPath, mapping);
  203. }
  204. break;
  205. }
  206. return [key, mappingRes];
  207. })
  208. .reduce((acc, [key, val]) => addProp(acc, key, val), {});
  209. };
  210. const _extractFieldMapping = ( [key, metaFoG] ) => {
  211. let source;
  212. if (isGroup(metaFoG)) {
  213. if (Array.isArray(metaFoG.source)) {
  214. source = extractFieldMappings(metaFoG.meta);
  215. source.__ = metaFoG.source; // Store the functional mapping (including function executed later to provide container's data)
  216. } else {
  217. source = extractFieldMappings(metaFoG.meta, metaFoG.source || key);
  218. }
  219. } else {
  220. source = metaFoG.source || key;
  221. }
  222. return [key, source];
  223. };
  224. const extractFieldMappings = (metaG, parentPath = '') => Object.entries(metaG)
  225. .map(_extractFieldMapping)
  226. .reduce((res, [key, mapping]) => {
  227. // Work out the path prefix
  228. let prefix;
  229. if (parentPath) {
  230. if (isRootPath(parentPath) || isAbsPath(metaG[key].source) || Array.isArray(parentPath)) {
  231. // If the parentPath is the root of the data structure, or the source is an absolute path or functional datasource,
  232. // then there's no path prefix
  233. prefix = '';
  234. } else {
  235. // Otherwise create a prefix from the parentPath
  236. prefix = parentPath ? (parentPath[0] === '/' ? parentPath.slice(1) : parentPath) : '';
  237. }
  238. }
  239. // Work out the mapping result
  240. let mappingRes;
  241. if (typeof mapping === 'string') {
  242. // DIRECT MAPPING
  243. if (mapping[0] === '/') {
  244. mappingRes = mapping.slice(1);
  245. } else if (prefix) {
  246. mappingRes = `${prefix}.${mapping}`;
  247. } else {
  248. mappingRes = mapping;
  249. }
  250. } else if (Array.isArray(mapping)) {
  251. // FUNCTIONAL MAPPING
  252. // of the form [fn, fn] or [source, fn, fn]
  253. if (prefix) {
  254. if (typeof mapping[0] === 'function') {
  255. // Mapping of form [fn, fn] with existing parent path
  256. mappingRes = [`${prefix}.${key}`, ...mapping];
  257. } else if (typeof mapping[0] === 'string') {
  258. // Mapping of form [source, fn, fn] with existing parent path
  259. const source = mapping[0][0] === '/' ? mapping[0].slice(1) : `${prefix}.${mapping[0]}`;
  260. mappingRes = [source, ...mapping.slice(1)];
  261. }
  262. } else {
  263. if (typeof mapping[0] === 'function') {
  264. // Mapping of form [fn, fn] with NO parent path
  265. mappingRes = [key, ...mapping];
  266. } else if (typeof mapping[0] === 'string') {
  267. // Mapping of form [source, fn, fn] with NO parent path
  268. const source = mapping[0][0] === '/' ? mapping[0].slice(1) : mapping[0];
  269. mappingRes = [source, ...mapping.slice(1)];
  270. }
  271. }
  272. } else if (typeof mapping === 'object' && prefix && !mapping.__) {
  273. // CONTAINER with parentPath, and WITHOUT its own functional datasource stored in __
  274. // For every contained value recursively prepend the parentPath to give an absolute path
  275. mappingRes = prependParentPathRecursive(prefix, mapping);
  276. } else {
  277. mappingRes = mapping;
  278. }
  279. return addProp(res, key, mappingRes);
  280. }, {});
  281. // ---------------------------------------------------------------------------------------------------------------------
  282. // Build Form Group Function Factory
  283. // returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
  284. // ---------------------------------------------------------------------------------------------------------------------
  285. const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup => {
  286. // Establishes a closure over the supplied FormBuilder and returns a function that builds FormGroups from metadata
  287. // ( it's done this way so we can use the FormBuilder singleton without reinitialising )
  288. // Build Form Control
  289. const buildControlState = metaF => ({ value: metaF.value || metaF.default, disabled: metaF.disabled });
  290. const buildValidators = (metaF): AbstractControlOptions => ({
  291. validators: metaF.validators,
  292. asyncValidators: metaF.asyncValidators,
  293. // blur not working for custom components, so use change for custom and blur for text
  294. updateOn: metaF.type === 'text' || metaF.type === 'textarea' ? 'blur' : 'change'
  295. });
  296. const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
  297. // Build Form Array containing either Form Controls or Form Groups
  298. const buildFormArray = (metaG): FormArray => {
  299. return fb.array(
  300. metaG.map(m => isContainer(m) ? _buildFormGroup(m.meta) : buildFormControl(m))
  301. );
  302. };
  303. // Build Form Group Member - builds a FormControl, FormArray or another FormGroup - which in turn can contain any of these
  304. const buildFormGroupMember = metaFoG => isGroup(metaFoG) ?
  305. (isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta) : _buildFormGroup(metaFoG.meta)) :
  306. buildFormControl(metaFoG);
  307. const buildFormGroupReducerIteree = (res, metaFoG) => {
  308. return metaFoG.noFormControls ? res : { ...res, [metaFoG.name]: buildFormGroupMember(metaFoG) };
  309. };
  310. const _buildFormGroup = _metaG => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG));
  311. // The main function - builds FormGroups containing other FormGroups, FormArrays and FormControls
  312. const buildFormGroup = metaG => {
  313. // Ensure that we have Field-Specific Metadata, not raw Objects
  314. const metaWithNameKeys = addMissingNames(metaG); // <!--- DO WE REALLY HAVE TO CALL addMissingManes again here - it should have been done already?
  315. // MAYBE only run this if first entry isn't right, for reasons of efficiency
  316. const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
  317. return _buildFormGroup(fieldModeledMeta);
  318. };
  319. return buildFormGroup;
  320. };
  321. // ---------------------------------------------------------------------------------------------------------------------
  322. // Reordering ( support for before / after instructions in metadata )
  323. // ---------------------------------------------------------------------------------------------------------------------
  324. // Object slice function
  325. const slice = (obj, start, end = null) => Object.entries(obj)
  326. .slice(start, end !== null ? end : Object.keys(obj).length)
  327. .reduce((res, [key, val]) => addProp(res, key, val), {});
  328. const objConcat = (obj, pos, key, val = null) => {
  329. const existsAlready = Object.keys(obj).indexOf(key);
  330. if (existsAlready) {
  331. val = obj[key];
  332. }
  333. const start = slice(obj, 0, pos);
  334. const finish = slice(obj, pos);
  335. delete start[key];
  336. delete finish[key];
  337. return { ...start, [key]: val, ...finish };
  338. };
  339. const insertBefore = (obj, beforeKey, key, val = null) => {
  340. const targetPosition = Object.keys(obj).indexOf(beforeKey);
  341. return objConcat(obj, targetPosition, key, val);
  342. };
  343. const insertAfter = (obj, afterKey, key, val = null) => {
  344. const targetPosition = Object.keys(obj).indexOf(afterKey) + 1;
  345. return objConcat(obj, targetPosition, key, val);
  346. };
  347. // Process reordeing instructions recursively
  348. const _execMetaReorderingInstructions = (metaG: StringMap) => {
  349. let reorderedGroup = { ...metaG };
  350. Object.entries(metaG).forEach(([key, metaFoG]) => {
  351. if (metaFoG.before) {
  352. reorderedGroup = insertBefore(reorderedGroup, metaFoG.before, key);
  353. } else if (metaFoG.after) {
  354. reorderedGroup = insertAfter(reorderedGroup, metaFoG.after, key);
  355. }
  356. if (isContainer(metaFoG)) {
  357. reorderedGroup[key].meta = execMetaReorderingInstructions(metaFoG.meta);
  358. }
  359. });
  360. return reorderedGroup;
  361. };
  362. const execMetaReorderingInstructions = (metaG: StringMap) => {
  363. // Repeating Containers (which have array meta *at this point*) can't be reordered, but other types of containers can
  364. return Array.isArray(metaG) ? cloneDeep(metaG) : _execMetaReorderingInstructions(cloneDeep(metaG));
  365. };
  366. // ---------------------------------------------------------------------------------------------------------------------
  367. // Generate new model, without mutating original
  368. // (used to produce an updated copy of a model when form values are changed - will not create new keys)
  369. // ---------------------------------------------------------------------------------------------------------------------
  370. const generateNewModel = (originalModel, updates) => {
  371. return updateObject(originalModel, updates);
  372. };
  373. const updateObject = (obj, updates, createAdditionalKeys = false) => {
  374. // THIS DOES NOT MUTATE obj, instead returning a new object
  375. const shallowClone = { ...obj };
  376. Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
  377. return shallowClone;
  378. };
  379. const safeSet = (obj, key, val, createAdditionalKeys = false) => {
  380. // THIS MUTATES obj - consider the wisdom of this
  381. if (!createAdditionalKeys && !obj.hasOwnProperty(key)) {
  382. return;
  383. }
  384. const currentVal = obj[key];
  385. if (val === currentVal) {
  386. return;
  387. }
  388. if (nullOrScaler(currentVal)) {
  389. console.log('safeSet nullOrScaler', key, val);
  390. obj[key] = val;
  391. } else {
  392. if (Array.isArray(currentVal)) {
  393. if (typeof val === 'object') {
  394. // Replace array
  395. console.log('safeSet array', key, val);
  396. obj[key] = Array.isArray(val) ? val : Object.values(val);
  397. } else {
  398. // Append to end of existing array? Create a single element array?
  399. throw new Error(`safeSet Error: Expected array or object @key ${key} but got scalar
  400. Rejected update was ${val}`);
  401. }
  402. } else if (typeof val === 'object') {
  403. // Deep merge
  404. obj[key] = updateObject(obj[key], val, createAdditionalKeys);
  405. } else {
  406. throw new Error(`safeSet Error: Can't deep merge object into scalar @key ${key}
  407. Rejected update was ${val}`);
  408. }
  409. }
  410. };
  411. const nullOrScaler = val => {
  412. if (val === null) { return true; }
  413. const t = typeof val;
  414. return t === 'number' || t === 'string' || t === 'boolean';
  415. };
  416. // ---------------------------------------------------------------------------------------------------------------------
  417. // Helper Functions
  418. // ---------------------------------------------------------------------------------------------------------------------
  419. // Add Property to object, returning updated object
  420. const addProp = (obj, key, val) => { obj[key] = val; return obj; };
  421. // Is Group
  422. // Helper function to distinguish group from field
  423. const isGroup = (metaFoG): boolean => !!metaFoG.meta;
  424. // Is Container
  425. // Helper function to distinguish container group (a group of child fields)
  426. const isContainer = (metaFoG): boolean => isGroup(metaFoG)
  427. && !Array.isArray(metaFoG.meta)
  428. && (!metaFoG.type || metaFoG.type.toLowerCase() === 'container');
  429. // Is Repeating Container
  430. // Helper function to distinguish a repeating container group (a group of child fields that can be repeated 1...N times)
  431. const isRepeatingContainer = (metaFoG): boolean => isGroup(metaFoG)
  432. && Array.isArray(metaFoG.meta)
  433. && (!metaFoG.type || metaFoG.type.toLowerCase() === 'repeatingContainer');
  434. // Add Missing Names
  435. // Helper function to add any missing 'name' properties to Fields and Groups using property's key, recursively
  436. // BUT not to repeatingContainer members
  437. const addNameIfMissing = (metaFoG, key) => metaFoG.name ? metaFoG : addProp(metaFoG, 'name', key);
  438. const addNameToSelfAndChildren = ( [key, metaFoG] ) => {
  439. metaFoG = addNameIfMissing(metaFoG, key);
  440. if (isGroup(metaFoG) && !isRepeatingContainer(metaFoG)) {
  441. metaFoG.meta = isArray(metaFoG.meta) ? Object.values(addMissingNames(metaFoG.meta)) : addMissingNames(metaFoG.meta); // Recursion
  442. }
  443. return [key, metaFoG];
  444. };
  445. const addMissingNames = metaG => Object.entries(metaG)
  446. .map(addNameToSelfAndChildren)
  447. .reduce((res, [key, val]) => addProp(res, key, val), {});
  448. // Add Missing Field-Specific Meta
  449. // Helper function to add any missing Field-Specific Metadata (using models in dynaform/models), recursively
  450. // Checks the constrctor, which should NOT be a plain Object, but rather TextField, TextareaField, SelectField, etc.
  451. const add_FSM_IfMissing = metaFoG => metaFoG.constructor.name === 'Object' ? buildModeledFieldGroupMember(metaFoG) : metaFoG;
  452. const add_FSM_ToSelfAndChildren = ( [key, metaFoG] ) => {
  453. metaFoG = add_FSM_IfMissing(metaFoG);
  454. return [key, metaFoG];
  455. };
  456. const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
  457. .map(add_FSM_ToSelfAndChildren)
  458. .reduce((res, [key, val]) => addProp(res, key, val), {});
  459. // ---------------------------------------------------------------------------------------------------------------------
  460. // Exports
  461. // ---------------------------------------------------------------------------------------------------------------------
  462. export {
  463. autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
  464. buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory,
  465. generateNewModel
  466. };