_formdata-utils.ts 23 KB

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