_formdata-utils.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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. *
  21. */
  22. import { FormBuilder, FormGroup, FormArray, FormControl, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
  23. import { reduce, cloneDeep } from 'lodash/fp';
  24. import * as fmdModels from '../models/field.model';
  25. // REMINDER: Import this directly from @angular/forms when we upgrade to Angular 6
  26. // While we're still on Angular 5 just declare it
  27. interface AbstractControlOptions {
  28. validators?: ValidatorFn | ValidatorFn[] | null;
  29. asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null;
  30. updateOn?: 'change' | 'blur' | 'submit';
  31. }
  32. // ---------------------------------------------------------------------------------------------------------------------
  33. // AutoMeta: Generate Automatic Metadata from a raw model (or mapped raw model)
  34. // ---------------------------------------------------------------------------------------------------------------------
  35. const isScalar = val => typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string';
  36. const isArray = val => Array.isArray(val);
  37. const keyValPairToMeta = (val, key) => ({ name: key, [isScalar(val) ? 'value' : 'meta']: val });
  38. const keyValPairToMetaRecursive = ( [key, val] ) => {
  39. if (val === null || val === undefined) {
  40. val = ''; // Treat null or undefined as empty string, for purpose of form
  41. }
  42. const innerVal = isScalar(val) ? val : (isArray(val) ? arrayToMeta(val) : autoMeta(val));
  43. const metaVal = keyValPairToMeta(innerVal, key);
  44. return [key, metaVal];
  45. };
  46. const arrayMemberAutoName = val => isScalar(val) ? val : val.__typename || val.constructor.name;
  47. const arrayToMeta = array => array.map(val => ({ name: arrayMemberAutoName(val), 'value' : val }));
  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
  54. // ---------------------------------------------------------------------------------------------------------------------
  55. // containerSeed = Metadata from container which seeds all contained fields
  56. const combineMetaForField = (metaF, containerSeed, extraMetaF) => Object.assign(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) {
  61. const isCon = isContainer(val);
  62. const metaFoG = metaG[key] || {};
  63. const seed = isCon ? {} : containerSeed; // Container's don't seed themselves
  64. const extra = isCon ?
  65. {
  66. ...val,
  67. meta: combineExtraMeta( // RECURSION
  68. metaFoG.meta || {},
  69. val['meta'],
  70. createFromExtra,
  71. val['seed'] || containerSeed // Inherit seeded data if this group's seed isn't set
  72. )
  73. }
  74. :
  75. val;
  76. combinedMeta[key] = combineMetaForField(metaFoG, seed, extra);
  77. }
  78. });
  79. return { ...metaG, ...combinedMeta };
  80. };
  81. // Combine model with overrides (after automatically generating metadata from the model)
  82. const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => combineExtraMeta(autoMeta(model), extraMeta, createFromExtra);
  83. // ---------------------------------------------------------------------------------------------------------------------
  84. // Build Form-Field-Type-Specific Metadata (using the field models in dynaform/models)
  85. // ---------------------------------------------------------------------------------------------------------------------
  86. const buildFieldClassName = (t = 'text') => {
  87. const start = t[0].toUpperCase() + t.slice(1);
  88. if (start === 'Container' || start === 'Heading' || t.slice(-5) === 'Group') {
  89. return start;
  90. }
  91. return start + 'Field';
  92. };
  93. const buildModeledField = metaFoG => {
  94. const type = isContainer(metaFoG) ? 'container' : metaFoG.type;
  95. const className = buildFieldClassName(type);
  96. if (!fmdModels[className]) {
  97. throw new Error(`No metadata model "${className}" for type "${type}"`);
  98. }
  99. return new fmdModels[className](metaFoG);
  100. };
  101. // Build Form Group Member
  102. const buildModeledFieldGroupMember = metaFoG => {
  103. const modeledGroupMember = buildModeledField(metaFoG);
  104. if (isContainer(metaFoG)) {
  105. modeledGroupMember.meta = _buildFieldSpecificMeta(metaFoG.meta);
  106. }
  107. return modeledGroupMember;
  108. };
  109. // Build Form Group
  110. const buildModeledFieldGroupReducerIteree = (res, metaFoG) => Object.assign(res, { [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
  111. const _buildFieldSpecificMeta = metaG => reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
  112. const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
  113. // ---------------------------------------------------------------------------------------------------------------------
  114. // Generate mapping from source attributes
  115. // (used to grab data from model when using METAFIRST form generation)
  116. // ---------------------------------------------------------------------------------------------------------------------
  117. // Each container CAN have a datasource instead of the original model
  118. // SO ... If container source is a functional mapping, store the mapping to get datasource object later
  119. const isAbsPath = path => typeof path === 'string' && path[0] === '/';
  120. const isRootPath = path => path === '/';
  121. const processPath = (parentPath, path) => isAbsPath(path) ? path : `${parentPath}.${path}`;
  122. const prependParentPathRecursive = (parentPath: string, obj: StringMap) => {
  123. return Object.entries(obj)
  124. .map( ([key, mapping] ) => {
  125. let mappingRes;
  126. switch (typeof mapping) {
  127. case 'string':
  128. mappingRes = processPath(parentPath, mapping);
  129. break;
  130. case 'object':
  131. if (Array.isArray(mapping)) {
  132. // A functional mapping of the form [fn, fn] or [source, fn, fn]
  133. if (typeof mapping[0] === 'string') {
  134. const source = processPath(parentPath, mapping[0]);
  135. mappingRes = [source, ...mapping.slice(1)];
  136. } else {
  137. const source = processPath(parentPath, mapping);
  138. mappingRes = [source, ...mapping];
  139. }
  140. } else {
  141. mappingRes = prependParentPathRecursive(parentPath, mapping);
  142. }
  143. break;
  144. }
  145. return [key, mappingRes];
  146. })
  147. .reduce((acc, [key, val]) => addProp(acc, key, val), {});
  148. };
  149. const _extractFieldMapping = ( [key, metaFoG] ) => {
  150. let source;
  151. if (isGroup(metaFoG)) {
  152. if (Array.isArray(metaFoG.source)) {
  153. source = extractFieldMappings(metaFoG.meta);
  154. source.__ = metaFoG.source; // Store the functional mapping (including function executed later to provide container's data)
  155. } else {
  156. source = extractFieldMappings(metaFoG.meta, metaFoG.source || key);
  157. }
  158. } else {
  159. source = metaFoG.source || key;
  160. }
  161. return [key, source];
  162. };
  163. const extractFieldMappings = (metaG, parentPath = '') => Object.entries(metaG)
  164. .map(_extractFieldMapping)
  165. .reduce((res, [key, mapping]) => {
  166. // Work out the path prefix
  167. let prefix;
  168. if (parentPath) {
  169. if (isRootPath(parentPath) || isAbsPath(metaG[key].source) || Array.isArray(parentPath)) {
  170. // If the parentPath is the root of the data structure, or the source is an absolute path or functional datasource,
  171. // then there's no path prefix
  172. prefix = '';
  173. } else {
  174. // Otherwise create a prefix from the parentPath
  175. prefix = parentPath ? (parentPath[0] === '/' ? parentPath.slice(1) : parentPath) : '';
  176. }
  177. }
  178. // Work out the mapping result
  179. let mappingRes;
  180. if (typeof mapping === 'string') {
  181. // DIRECT MAPPING
  182. if (mapping[0] === '/') {
  183. mappingRes = mapping.slice(1);
  184. } else if (prefix) {
  185. mappingRes = `${prefix}.${mapping}`;
  186. } else {
  187. mappingRes = mapping;
  188. }
  189. } else if (Array.isArray(mapping)) {
  190. // FUNCTIONAL MAPPING
  191. // of the form [fn, fn] or [source, fn, fn]
  192. if (prefix) {
  193. if (typeof mapping[0] === 'function') {
  194. // Mapping of form [fn, fn] with existing parent path
  195. mappingRes = [`${prefix}.${key}`, ...mapping];
  196. } else if (typeof mapping[0] === 'string') {
  197. // Mapping of form [source, fn, fn] with existing parent path
  198. const source = mapping[0][0] === '/' ? mapping[0].slice(1) : `${prefix}.${mapping[0]}`;
  199. mappingRes = [source, ...mapping.slice(1)];
  200. }
  201. } else {
  202. if (typeof mapping[0] === 'function') {
  203. // Mapping of form [fn, fn] with NO parent path
  204. mappingRes = [key, ...mapping];
  205. } else if (typeof mapping[0] === 'string') {
  206. // Mapping of form [source, fn, fn] with NO parent path
  207. const source = mapping[0][0] === '/' ? mapping[0].slice(1) : mapping[0];
  208. mappingRes = [source, ...mapping.slice(1)];
  209. }
  210. }
  211. } else if (typeof mapping === 'object' && prefix && !mapping.__) {
  212. // CONTAINER with parentPath, and WITHOUT its own functional datasource stored in __
  213. // For every contained value recursively prepend the parentPath to give an absolute path
  214. mappingRes = prependParentPathRecursive(prefix, mapping);
  215. } else {
  216. mappingRes = mapping;
  217. }
  218. return addProp(res, key, mappingRes);
  219. }, {});
  220. // ---------------------------------------------------------------------------------------------------------------------
  221. // Build Form Group Function Factory
  222. // returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
  223. // ---------------------------------------------------------------------------------------------------------------------
  224. const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup => {
  225. // Establishes a closure over the supplied FormBuilder and returns a function that builds FormGroups from metadata
  226. // ( it's done this way so we can use the FormBuilder singleton without reinitialising )
  227. // Build Form Control
  228. const buildControlState = metaF => ({ value: metaF.value || metaF.default, disabled: metaF.isDisabled });
  229. const buildValidators = (metaF): AbstractControlOptions => ({
  230. validators: metaF.validators,
  231. asyncValidators: metaF.asyncValidators,
  232. // blur not working for custom components, so use change for custom and blur for text
  233. updateOn: metaF.type === 'text' || metaF.type === 'textarea' ? 'blur' : 'change'
  234. });
  235. const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
  236. // Build Form Array
  237. const buildFormArray = (metaG): FormArray => fb.array(metaG.map(metaF => buildFormControl(metaF)));
  238. // Build Form Group Member - builds a FormControl, FormArray, or another FormGroup which can contain any of these
  239. const buildFormGroupMember = metaFoG => isGroup(metaFoG) ?
  240. (isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta) : _buildFormGroup(metaFoG.meta)) :
  241. buildFormControl(metaFoG);
  242. const buildFormGroupReducerIteree = (res, metaFoG) => {
  243. return metaFoG.noFormControls ? res : Object.assign(res, { [metaFoG.name]: buildFormGroupMember(metaFoG) });
  244. };
  245. const _buildFormGroup = _metaG => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG));
  246. // The main function - builds FormGroups containing other FormGroups, FormArrays and FormControls
  247. const buildFormGroup = metaG => {
  248. // Ensure that we have Field-Specific Metadata, not raw Objects
  249. const metaWithNameKeys = addMissingNames(metaG);
  250. // MAYBE only run this if first entry isn't right, for reasons of efficiency
  251. const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
  252. return _buildFormGroup(fieldModeledMeta);
  253. };
  254. return buildFormGroup;
  255. };
  256. // ---------------------------------------------------------------------------------------------------------------------
  257. // Reordering ( support for before / after instructions in metadata )
  258. // ---------------------------------------------------------------------------------------------------------------------
  259. // Object slice function
  260. const slice = (obj, start, end = null) => Object.entries(obj)
  261. .slice(start, end !== null ? end : Object.keys(obj).length)
  262. .reduce((res, [key, val]) => addProp(res, key, val), {});
  263. const objConcat = (obj, pos, key, val = null) => {
  264. const existsAlready = Object.keys(obj).indexOf(key);
  265. if (existsAlready) {
  266. val = obj[key];
  267. }
  268. const start = slice(obj, 0, pos);
  269. const finish = slice(obj, pos);
  270. delete start[key];
  271. delete finish[key];
  272. return { ...start, [key]: val, ...finish };
  273. };
  274. const insertBefore = (obj, beforeKey, key, val = null) => {
  275. const targetPosition = Object.keys(obj).indexOf(beforeKey);
  276. return objConcat(obj, targetPosition, key, val);
  277. };
  278. const insertAfter = (obj, afterKey, key, val = null) => {
  279. const targetPosition = Object.keys(obj).indexOf(afterKey) + 1;
  280. return objConcat(obj, targetPosition, key, val);
  281. };
  282. // Process reordeing instructions recursively
  283. const _execMetaReorderingInstructions = (metaG: StringMap) => {
  284. let reorderedGroup = { ...metaG };
  285. Object.entries(metaG).forEach(([key, metaFoG]) => {
  286. if (metaFoG.before) {
  287. reorderedGroup = insertBefore(reorderedGroup, metaFoG.before, key);
  288. } else if (metaFoG.after) {
  289. reorderedGroup = insertAfter(reorderedGroup, metaFoG.after, key);
  290. }
  291. if (isContainer(metaFoG)) {
  292. reorderedGroup[key].meta = execMetaReorderingInstructions(metaFoG.meta);
  293. }
  294. });
  295. return reorderedGroup;
  296. };
  297. const execMetaReorderingInstructions = (metaG: StringMap) => {
  298. return _execMetaReorderingInstructions(cloneDeep(metaG));
  299. };
  300. // ---------------------------------------------------------------------------------------------------------------------
  301. // Generate new model, without mutating original
  302. // (used to produce an updated copy of a model when form values are changed - will not create new keys)
  303. // ---------------------------------------------------------------------------------------------------------------------
  304. const generateNewModel = (originalModel, updates) => {
  305. return updateObject(originalModel, updates);
  306. };
  307. const updateObject = (obj, updates, createAdditionalKeys = false) => {
  308. // THIS DOES NOT MUTATE obj, instead returning a new object
  309. const shallowClone = { ...obj };
  310. Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
  311. return shallowClone;
  312. };
  313. const safeSet = (obj, key, val, createAdditionalKeys = false) => {
  314. // THIS MUTATES obj - consider the wisdom of this
  315. if (!createAdditionalKeys && !obj.hasOwnProperty(key)) {
  316. return;
  317. }
  318. const currentVal = obj[key];
  319. if (val === currentVal) {
  320. return;
  321. }
  322. if (nullOrScaler(currentVal)) {
  323. console.log('safeSet nullOrScaler', key, val);
  324. obj[key] = val;
  325. } else {
  326. if (Array.isArray(currentVal)) {
  327. if (typeof val === 'object') {
  328. // Replace array
  329. console.log('safeSet array', key, val);
  330. obj[key] = Array.isArray(val) ? val : Object.values(val);
  331. } else {
  332. // Append to end of existing array? Create a single element array?
  333. throw new Error(`safeSet Error: Expected array or object @key ${key} but got scalar
  334. Rejected update was ${val}`);
  335. }
  336. } else if (typeof val === 'object') {
  337. // Deep merge
  338. obj[key] = updateObject(obj[key], val, createAdditionalKeys);
  339. } else {
  340. throw new Error(`safeSet Error: Can't deep merge object into scalar @key ${key}
  341. Rejected update was ${val}`);
  342. }
  343. }
  344. };
  345. const nullOrScaler = val => {
  346. if (val === null) { return true; }
  347. const t = typeof val;
  348. return t === 'number' || t === 'string' || t === 'boolean';
  349. };
  350. // ---------------------------------------------------------------------------------------------------------------------
  351. // Helper Functions
  352. // ---------------------------------------------------------------------------------------------------------------------
  353. // Add Property to object, returning updated object
  354. const addProp = (obj, key, val) => { obj[key] = val; return obj; };
  355. // Is Group
  356. // Helper function to distinguish group from field
  357. const isGroup = (metaFoG): boolean => !!metaFoG.meta;
  358. // Is Container
  359. // Helper function to distinguish container group (a group of child fields)
  360. const isContainer = (metaFoG): boolean => isGroup(metaFoG) && (!metaFoG.type || metaFoG.type.toLowerCase() === 'container');
  361. // Add Missing Names
  362. // Helper function to add any missing 'name' properties to Fields and Groups using property's key, recursively
  363. const addNameIfMissing = (metaFoG, key) => metaFoG.name ? metaFoG : addProp(metaFoG, 'name', key);
  364. const addNameToSelfAndChildren = ( [key, metaFoG] ) => {
  365. metaFoG = addNameIfMissing(metaFoG, key);
  366. if (isGroup(metaFoG)) {
  367. metaFoG.meta = isArray(metaFoG.meta) ? Object.values(addMissingNames(metaFoG.meta)) : addMissingNames(metaFoG.meta); // Recursion
  368. }
  369. return [key, metaFoG];
  370. };
  371. const addMissingNames = metaG => Object.entries(metaG)
  372. .map(addNameToSelfAndChildren)
  373. .reduce((res, [key, val]) => addProp(res, key, val), {});
  374. // Add Missing Field-Specific Meta
  375. // Helper function to add any missing Field-Specific Metadata (using models in dynaform/models), recursively
  376. // Checks the constrctor, which should NOT be a plain Object, but rather TextField, TextareaField, SelectField, etc.
  377. const add_FSM_IfMissing = metaFoG => metaFoG.constructor.name === 'Object' ? buildModeledFieldGroupMember(metaFoG) : metaFoG;
  378. const add_FSM_ToSelfAndChildren = ( [key, metaFoG] ) => {
  379. metaFoG = add_FSM_IfMissing(metaFoG);
  380. return [key, metaFoG];
  381. };
  382. const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
  383. .map(add_FSM_ToSelfAndChildren)
  384. .reduce((res, [key, val]) => addProp(res, key, val), {});
  385. // ---------------------------------------------------------------------------------------------------------------------
  386. // Exports
  387. // ---------------------------------------------------------------------------------------------------------------------
  388. export {
  389. autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
  390. buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory,
  391. generateNewModel
  392. };