_formdata-utils.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  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. * metaRF = metadata for Repeating Field
  19. * metaG = metadata for Group (possibly nested)
  20. * metaFoG = metadata for Field or Group
  21. * rcMem = Repeating Container member
  22. * metaFoRCMem = metadata for Field or Repeating Container member
  23. *
  24. */
  25. import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControl, AbstractControlOptions } from '@angular/forms';
  26. import { cloneDeep, omit, reduce } from 'lodash/fp';
  27. import * as fmdModels from '../models/field.model';
  28. import { meta } from '@mock/testfields.v15';
  29. // ---------------------------------------------------------------------------------------------------------------------
  30. // AutoMeta: Generate Automatic Metadata from a model
  31. // ---------------------------------------------------------------------------------------------------------------------
  32. const isScalar = (val: any) => typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string';
  33. const isArray = (val: any) => Array.isArray(val);
  34. const keyValPairToMeta = (val: any, key: string) => ({ name: key, [isScalar(val) ? 'value' : 'meta']: val });
  35. const keyValPairToMetaRecursive = ( [key, val] ) => {
  36. if (val === null || val === undefined) {
  37. val = ''; // Treat null or undefined as empty string, for purpose of form
  38. }
  39. const innerVal = isScalar(val) ? val : (isArray(val) ? arrayToMeta(val) : autoMeta(val));
  40. const metaVal = keyValPairToMeta(innerVal, key);
  41. return [key, metaVal];
  42. };
  43. const arrayToMeta = array => array.map((val, i) => {
  44. if (isScalar(val)) {
  45. return { name: `item${i + 1}`, 'value' : val };
  46. } else {
  47. return { name: `group${i + 1}`, meta: autoMeta(val) };
  48. }
  49. });
  50. const autoMeta = model => Object.entries(model)
  51. .filter(kvPair => kvPair[0] !== '__')
  52. .map(keyValPairToMetaRecursive)
  53. .reduce((res, [key, val]) => addProp(res, key, val), {});
  54. // ---------------------------------------------------------------------------------------------------------------------
  55. // Combine automatically generated metadata with overrides (extraMeta)
  56. // ---------------------------------------------------------------------------------------------------------------------
  57. // containerSeed = Metadata from container which seeds all contained fields
  58. const combineMetaForField = (metaF, containerSeed, extraMetaF) => ({ ...metaF, ...containerSeed, ...extraMetaF });
  59. const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSeed = {}) => {
  60. const combinedMeta = {};
  61. Object.entries(extraMeta).forEach(([key, val]: [string, StringMap<any>]) => {
  62. if (typeof metaG[key] === 'object' || createFromExtra) { // If the key exists (in the model) OR we're creating from metadata
  63. const isCon = isContainer(val);
  64. const metaFoG = metaG[key] || {};
  65. const seed = isCon ? {} : containerSeed; // Container's don't seed themselves, only their children
  66. /*
  67. console.log('******************* BEFORE MODELLING *******************');
  68. console.log(val);
  69. console.log('IS REPEATING', isRepeating(val));
  70. console.log('HAS META', hasMeta(val));
  71. console.log('IS ARRAY', Array.isArray(val.meta));
  72. console.log('MEMBER 1 DEFINED', !!val.meta[0]);
  73. console.log('OBJECT VALUES .........', Object.values(val.meta[0]));
  74. console.log('MEMBER 1 1st entry .........', Object.values(val.meta[0])[0]);
  75. console.log('MEMBER 1 1st entry is SCALAR', isScalar(Object.values(val.meta[0])));
  76. */
  77. /*
  78. console.log('IS REP Field', isRepeatingField(val));
  79. console.log('IS REP Container', isRepeatingContainer(val));
  80. console.log('IS ORD Container', isContainer(val));
  81. */
  82. if (isRepeatingContainer(val))
  83. {
  84. // We've got a Repeating Container
  85. const baseObjWithAllKeys = getRCBaseObjectWithAllKeys(metaFoG, val, createFromExtra);
  86. metaFoG.meta = generateRepeatedGroup(metaFoG, val, baseObjWithAllKeys);
  87. const extra = {
  88. ...omit(['meta', 'seed'], val as StringMap<any>),
  89. meta: metaFoG.meta.map(
  90. rgMem => combineExtraMeta(
  91. rgMem.meta,
  92. val['meta'][0],
  93. createFromExtra,
  94. val['seed'] || containerSeed
  95. )
  96. )
  97. };
  98. combinedMeta[key] = combineMetaForField(metaFoG, {}, extra);
  99. // Stash a 'conbtainer template' for adding extra containers to the repeating container
  100. combinedMeta[key].__containerTemplate = combineExtraMeta(
  101. cloneDeep(baseObjWithAllKeys),
  102. val['meta'][0],
  103. false,
  104. val['seed'] || containerSeed
  105. );
  106. }
  107. else // ----------------------------------------------
  108. {
  109. let extra: StringMap<any>;
  110. if (isCon) {
  111. // We've got a container
  112. extra = {
  113. ...val,
  114. meta: combineExtraMeta( // RECURSION
  115. metaFoG.meta || {},
  116. val['meta'],
  117. createFromExtra,
  118. val['seed'] || containerSeed // Inherit seeded data if this group's seed isn't set
  119. )
  120. }
  121. }
  122. else // ----------------------------------------------
  123. {
  124. if (val.minRepeat || val.maxRepeat || val.initialRepeat) {
  125. // We've got a repeating field
  126. const metaForFieldToRepeat = {
  127. ...containerSeed,
  128. ...omit(['seed', 'minRepeat', 'maxRepeat', 'initialRepeat', 'showAddControl', 'showDeleteControl'], val as StringMap<any>)
  129. };
  130. delete val.type;
  131. extra = {
  132. ...val,
  133. meta: Array.from(Array(val.initialRepeat || 1).keys()).map((f, i) => ({ ...metaForFieldToRepeat, label: `${val.label || key} ${i + 1}` }))
  134. }
  135. } else {
  136. // We've got a standard field
  137. extra = val;
  138. }
  139. }
  140. combinedMeta[key] = combineMetaForField(metaFoG, seed, extra);
  141. }
  142. }
  143. });
  144. return { ...metaG, ...combinedMeta };
  145. };
  146. // Combine model with overrides (after automatically generating metadata from the model)
  147. const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => combineExtraMeta(autoMeta(model), extraMeta, createFromExtra);
  148. // <--- Utilities supporting Repreating Containers --->
  149. const generateRepeatedGroup = (metaFoG, extraMeta, baseObjWithAllKeys): StringMap<any>[] => {
  150. // Calculate the number of repeats
  151. const repeatInAutoMeta = Array.isArray(metaFoG.meta) ? metaFoG.meta.length : 0;
  152. const repeatInExtraMeta = extraMeta['initialRepeat'] || extraMeta['minRepeat'];
  153. const repeat = Math.max(repeatInAutoMeta, repeatInExtraMeta);
  154. metaFoG.meta = metaFoG.meta.map( rcMem => ({ ...rcMem, meta: { ...baseObjWithAllKeys, ...rcMem.meta } }) ); // Add extra keys to model meta
  155. // Extend repeated group from model (if any) to correct length, and add any missing names
  156. const repeatedGroup = repeatInAutoMeta ?
  157. [ ...metaFoG.meta, ...Array(repeat - repeatInAutoMeta).fill({ meta: baseObjWithAllKeys }) ] :
  158. Array(repeat).fill({ meta: baseObjWithAllKeys });
  159. const fullyNamedRepeatedGroup = repeatedGroup.map((rgMem, i) => rgMem.name ? rgMem : { name: `group${i + 1}`, ...rgMem });
  160. return fullyNamedRepeatedGroup;
  161. }
  162. // Get Repeating Container Base Object With All Keys
  163. const getRCBaseObjectWithAllKeys = (metaFoG, extraMeta, createFromExtra = false): StringMap<any> => {
  164. // If creating from extra, make sure all group members have all keys in both model and meta (as this is a repeating group)
  165. const keysFromModel = isArray(metaFoG.meta) && metaFoG.meta.length ? Object.keys(metaFoG.meta[0].meta) : [];
  166. const keysFromExtraMeta = extraMeta['meta'] && extraMeta['meta'][0] ? Object.keys(extraMeta['meta'][0]) : [];
  167. const keysToInclude = createFromExtra ? Array.from(new Set([...keysFromModel, ...keysFromExtraMeta])) : keysFromModel;
  168. const baseObjWithAllKeys = autoMeta(keysToInclude.reduce((acc, key) => addProp(acc, key, ''), {}));
  169. return baseObjWithAllKeys;
  170. }
  171. // ---------------------------------------------------------------------------------------------------------------------
  172. // Build Form-Field-Type-Specific Metadata (using the field models in dynaform/models)
  173. // ---------------------------------------------------------------------------------------------------------------------
  174. // MAYBE CHANGE INTO A MODULE SO WE CAN USE CLOSURE BUT ALSO METHODS INSIDE IT
  175. // More Elegant But Not Urgent
  176. const buildModeledFieldGroupMember = metaFog => metaFog; // THIS MAY BREAK THINGS NOW WE'VE MOVED FUNCTION INTO CLOSURE BELOW
  177. const buildFieldSpecificMetaInClosure = (metaG, context) => {
  178. const resolveType = (metaFoG: StringMap<any>): string => {
  179. if (metaFoG.type) {
  180. return metaFoG.type;
  181. }
  182. if (isRepeatingField(metaFoG)) {
  183. return 'repeatingField';
  184. }
  185. if (isContainer(metaFoG)) {
  186. return 'container';
  187. }
  188. if (isRepeatingContainer(metaFoG)) {
  189. return 'repeatingContainer';
  190. }
  191. return 'text';
  192. }
  193. const buildFieldClassName = (t: string): string => {
  194. const start = t[0].toUpperCase() + t.slice(1);
  195. if (['RepeatingField', 'Container', 'RepeatingContainer', 'Heading'].includes(start) || t.slice(-5) === 'Group') {
  196. return start;
  197. }
  198. return start + 'Field';
  199. };
  200. const buildModeledField = (metaFoG) => {
  201. const type = resolveType(metaFoG);
  202. const className = buildFieldClassName(type);
  203. if (!fmdModels[className]) {
  204. throw new Error(`No metadata model "${className}" for type "${type}"`);
  205. }
  206. return new fmdModels[className](metaFoG, context);
  207. };
  208. // Build Form Group Member
  209. const buildModeledFieldGroupMember = (metaFoG) => {
  210. const modeledGroupMember = buildModeledField(metaFoG);
  211. console.log('------------- DEBUGGING -------------');
  212. console.log(modeledGroupMember);
  213. console.log('IS REP Field', isRepeatingField(modeledGroupMember));
  214. console.log('IS REP Container', isRepeatingContainer(modeledGroupMember));
  215. console.log('IS ORD Container', isContainer(modeledGroupMember));
  216. console.log(modeledGroupMember);
  217. if (isRepeatingField(modeledGroupMember)) {
  218. modeledGroupMember.meta = modeledGroupMember.meta.map(metaF => buildModeledField(metaF));
  219. } else if (isContainer(modeledGroupMember)) {
  220. modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
  221. } else if (isRepeatingContainer(modeledGroupMember)) {
  222. modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
  223. modeledGroupMember.__containerTemplate = {
  224. ...modeledGroupMember.meta[0],
  225. meta: _buildFieldSpecificMeta(modeledGroupMember.__containerTemplate),
  226. name: '__containerTemplate',
  227. button: ''
  228. };
  229. }
  230. return modeledGroupMember;
  231. };
  232. // Build Form Group
  233. const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
  234. const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
  235. metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) :
  236. reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
  237. const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
  238. // DEBUGGING
  239. console.log('buildFieldSpecificMetaInClosure', metaG, context);
  240. return buildFieldSpecificMeta(metaG); // RUN CODE
  241. }
  242. // ---------------------------------------------------------------------------------------------------------------------
  243. // Generate mapping from source attributes
  244. // (used to grab data from model when using METAFIRST form generation)
  245. // ---------------------------------------------------------------------------------------------------------------------
  246. // Each container CAN have a datasource instead of the original model
  247. // SO ... If container source is a functional mapping, store the mapping to get datasource object later
  248. const isAbsPath = path => typeof path === 'string' && path[0] === '/';
  249. const isRootPath = path => path === '/';
  250. const processPath = (parentPath, path) => isAbsPath(path) ? path : `${parentPath}.${path}`;
  251. const prependParentPathRecursive = (parentPath: string, obj: StringMap<any>) => {
  252. return Object.entries(obj)
  253. .map( ([key, mapping] ) => {
  254. let mappingRes;
  255. switch (typeof mapping) {
  256. case 'string':
  257. mappingRes = processPath(parentPath, mapping);
  258. break;
  259. case 'object':
  260. if (Array.isArray(mapping)) {
  261. // A functional mapping of the form [fn, fn] or [source, fn, fn]
  262. if (typeof mapping[0] === 'string') {
  263. const source = processPath(parentPath, mapping[0]);
  264. mappingRes = [source, ...mapping.slice(1)];
  265. } else {
  266. const source = processPath(parentPath, mapping);
  267. mappingRes = [source, ...mapping];
  268. }
  269. } else {
  270. mappingRes = prependParentPathRecursive(parentPath, mapping);
  271. }
  272. break;
  273. }
  274. return [key, mappingRes];
  275. })
  276. .reduce((acc, [key, val]) => addProp(acc, key, val), {});
  277. };
  278. const _extractFieldMapping = ( [key, metaFoG] ) => {
  279. let source;
  280. if (hasMeta(metaFoG)) {
  281. if (Array.isArray(metaFoG.source)) {
  282. source = extractFieldMappings(metaFoG.meta);
  283. source.__ = metaFoG.source; // Store the functional mapping (including function executed later to provide container's data)
  284. } else {
  285. source = extractFieldMappings(metaFoG.meta, metaFoG.source || key);
  286. }
  287. } else {
  288. source = metaFoG.source || key;
  289. }
  290. return [key, source];
  291. };
  292. const extractFieldMappings = (metaG, parentPath = '') => Object.entries(metaG)
  293. .map(_extractFieldMapping)
  294. .reduce((res, [key, mapping]) => {
  295. // Work out the path prefix
  296. let prefix;
  297. if (parentPath) {
  298. if (isRootPath(parentPath) || isAbsPath(metaG[key].source) || Array.isArray(parentPath)) {
  299. // If the parentPath is the root of the data structure, or the source is an absolute path or functional datasource,
  300. // then there's no path prefix
  301. prefix = '';
  302. } else {
  303. // Otherwise create a prefix from the parentPath
  304. prefix = parentPath ? (parentPath[0] === '/' ? parentPath.slice(1) : parentPath) : '';
  305. }
  306. }
  307. // Work out the mapping result
  308. let mappingRes;
  309. if (typeof mapping === 'string') {
  310. // DIRECT MAPPING
  311. if (mapping[0] === '/') {
  312. mappingRes = mapping.slice(1);
  313. } else if (prefix) {
  314. mappingRes = `${prefix}.${mapping}`;
  315. } else {
  316. mappingRes = mapping;
  317. }
  318. } else if (Array.isArray(mapping)) {
  319. // FUNCTIONAL MAPPING
  320. // of the form [fn, fn] or [source, fn, fn]
  321. if (prefix) {
  322. if (typeof mapping[0] === 'function') {
  323. // Mapping of form [fn, fn] with existing parent path
  324. mappingRes = [`${prefix}.${key}`, ...mapping];
  325. } else if (typeof mapping[0] === 'string') {
  326. // Mapping of form [source, fn, fn] with existing parent path
  327. const source = mapping[0][0] === '/' ? mapping[0].slice(1) : `${prefix}.${mapping[0]}`;
  328. mappingRes = [source, ...mapping.slice(1)];
  329. }
  330. } else {
  331. if (typeof mapping[0] === 'function') {
  332. // Mapping of form [fn, fn] with NO parent path
  333. mappingRes = [key, ...mapping];
  334. } else if (typeof mapping[0] === 'string') {
  335. // Mapping of form [source, fn, fn] with NO parent path
  336. const source = mapping[0][0] === '/' ? mapping[0].slice(1) : mapping[0];
  337. mappingRes = [source, ...mapping.slice(1)];
  338. }
  339. }
  340. } else if (typeof mapping === 'object' && prefix && !mapping.__) {
  341. // CONTAINER with parentPath, and WITHOUT its own functional datasource stored in __
  342. // For every contained value recursively prepend the parentPath to give an absolute path
  343. mappingRes = prependParentPathRecursive(prefix, mapping);
  344. } else {
  345. mappingRes = mapping;
  346. }
  347. return addProp(res, key, mappingRes);
  348. }, {});
  349. // ---------------------------------------------------------------------------------------------------------------------
  350. // Build Form Group Function Factory
  351. // returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
  352. // ---------------------------------------------------------------------------------------------------------------------
  353. // TODO: In progress: elegantly adding validators at FormGroup and FormArray levels
  354. // Working, but code needs a rework
  355. const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup => {
  356. // Establishes a closure over the supplied FormBuilder and returns a function that builds FormGroups from metadata
  357. // ( it's done this way so we can use the FormBuilder singleton without reinitialising )
  358. // Build Form Control
  359. const buildControlState = metaF => ({ value: metaF.value || metaF.default, disabled: metaF.disabled });
  360. const buildValidators = (metaFoG): AbstractControlOptions => ({
  361. validators: metaFoG.validators,
  362. asyncValidators: metaFoG.asyncValidators,
  363. // blur not working for custom components, so use change for custom and blur for text
  364. updateOn: getUpdateOn(metaFoG.type)
  365. });
  366. const BVAL = metaFoG => {
  367. if (!metaFoG || !(metaFoG.validators || metaFoG.asyncValidators)) {
  368. return undefined;
  369. }
  370. console.log(metaFoG);
  371. const res = buildValidators(metaFoG);
  372. console.log(res);
  373. return res;
  374. }
  375. const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
  376. // Build Form Array containing either Form Controls or Form Groups
  377. const buildFormArray = (metaG, grMeta?): FormArray => {
  378. return fb.array(
  379. metaG.map(m => isContainer(m) ? _buildFormGroup(m.meta) : buildFormControl(m)),
  380. buildValidators(grMeta)
  381. );
  382. };
  383. // Build Form Group Member
  384. // Builds a FormControl, FormArray or another FormGroup - which in turn can contain any of these
  385. const buildFormGroupMember = metaFoG => hasMeta(metaFoG) ?
  386. (isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta, metaFoG) : _buildFormGroup(metaFoG.meta, metaFoG)) : // TODO: STINKY! REWORK with 1 param
  387. buildFormControl(metaFoG);
  388. const buildFormGroupReducerIteree = (res, metaFoG) => {
  389. return metaFoG.noFormControls ? res : { ...res, [metaFoG.name]: buildFormGroupMember(metaFoG) };
  390. };
  391. const _buildFormGroup = (_metaG, grMeta?) => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG), BVAL(grMeta));
  392. // The main function - builds FormGroups containing other FormGroups, FormArrays and FormControls
  393. const buildFormGroup = metaG => {
  394. // Ensure that we have Field-Specific Metadata, not raw Objects
  395. const metaWithNameKeys = addMissingNames(metaG); // <!--- DO WE REALLY HAVE TO CALL addMissingNames again here - it should have been done already?
  396. // MAYBE only run this if first entry isn't right, for reasons of efficiency
  397. // const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
  398. const fieldModeledMeta = metaWithNameKeys;
  399. return _buildFormGroup(fieldModeledMeta);
  400. };
  401. return buildFormGroup;
  402. };
  403. // Get the 'update on' strategy for validators
  404. const getUpdateOn = (type: string): 'blur'|'change'|'submit' => {
  405. const t = type.toLowerCase();
  406. let res;
  407. if (t === 'text' || t === 'textarea' || t === 'password') {
  408. res = 'blur';
  409. } else {
  410. res = 'change';
  411. }
  412. return res;
  413. };
  414. // ---------------------------------------------------------------------------------------------------------------------
  415. // Send RESET message to any attached AsyncValidators
  416. // This resets the stored initail value on controls where the field is required
  417. // (otherwise it isn't reset when the form is reset, and retails its previous value)
  418. // ---------------------------------------------------------------------------------------------------------------------
  419. const resetAsyncValidatorsRecursive = (group: FormGroup | FormArray): void => {
  420. for (const key in group.controls) {
  421. if (group.controls[key] instanceof FormControl) {
  422. const asv = group.controls[key].asyncValidator;
  423. if (asv) {
  424. Array.isArray(asv) ? asv.forEach(f => f('RESET')) : asv('RESET');
  425. }
  426. } else {
  427. resetAsyncValidatorsRecursive(group.controls[key]);
  428. }
  429. }
  430. };
  431. // ---------------------------------------------------------------------------------------------------------------------
  432. // Touch and update the validity of all controls in a FormGroup or FormArray / Reset validity of all controls
  433. // useful for displaying validation failures on submit
  434. // ---------------------------------------------------------------------------------------------------------------------
  435. const touchAndUpdateValidityRecursive = (group: FormGroup | FormArray): void => {
  436. group.markAsTouched();
  437. group.updateValueAndValidity();
  438. for (const key in group.controls) {
  439. if (group.controls[key] instanceof FormControl) {
  440. group.controls[key].markAsTouched();
  441. group.controls[key].updateValueAndValidity();
  442. } else {
  443. touchAndUpdateValidityRecursive(group.controls[key]);
  444. }
  445. }
  446. };
  447. const resetValidityRecursive = (group: FormGroup | FormArray): void => {
  448. group.markAsUntouched();
  449. group.updateValueAndValidity();
  450. for (const key in group.controls) {
  451. if (group.controls[key] instanceof FormControl) {
  452. group.controls[key].markAsUntouched();
  453. group.controls[key].updateValueAndValidity();
  454. } else {
  455. resetValidityRecursive(group.controls[key]);
  456. }
  457. }
  458. };
  459. /*
  460. const advanceUpdateStategyOfInvalidControlsRecursive = (group: FormGroup | FormArray): void => {
  461. // For all invalid text / select / password control, change the updateOn startegy from 'blur' to 'change'
  462. // For as-you-type feedback if validation has failed once
  463. // NB: Only for the syncronous validators
  464. for (const key in group.controls) {
  465. if (group.controls[key] instanceof FormControl) {
  466. console.log(key, group.controls[key].touched, group.controls[key].invalid);
  467. if (group.controls[key].touched && group.controls[key].invalid) {
  468. console.log('Replacing control', key);
  469. const newControl = new FormControl(
  470. group.controls[key].value,
  471. { updateOn: 'change', validators: group.controls[key].validator },
  472. group.controls[key].asyncValidator
  473. );
  474. (group as any).setControl(key as any, newControl);
  475. }
  476. } else {
  477. advanceUpdateStategyOfInvalidControlsRecursive(group.controls[key]);
  478. }
  479. }
  480. };
  481. */
  482. // ---------------------------------------------------------------------------------------------------------------------
  483. // Reordering ( support for before / after instructions in metadata )
  484. // ---------------------------------------------------------------------------------------------------------------------
  485. // Object slice function
  486. const slice = (obj, start, end = null) => Object.entries(obj)
  487. .slice(start, end !== null ? end : Object.keys(obj).length)
  488. .reduce((res, [key, val]) => addProp(res, key, val), {});
  489. const objConcat = (obj, pos, key, val = null) => {
  490. const existsAlready = Object.keys(obj).indexOf(key);
  491. if (existsAlready) {
  492. val = obj[key];
  493. }
  494. const start = slice(obj, 0, pos);
  495. const finish = slice(obj, pos);
  496. delete start[key];
  497. delete finish[key];
  498. return { ...start, [key]: val, ...finish };
  499. };
  500. const insertBefore = (obj, beforeKey, key, val = null) => {
  501. const targetPosition = Object.keys(obj).indexOf(beforeKey);
  502. return objConcat(obj, targetPosition, key, val);
  503. };
  504. const insertAfter = (obj, afterKey, key, val = null) => {
  505. const targetPosition = Object.keys(obj).indexOf(afterKey) + 1;
  506. return objConcat(obj, targetPosition, key, val);
  507. };
  508. // Process reordeing instructions recursively
  509. const _execMetaReorderingInstructions = (metaG: StringMap<any>) => {
  510. let reorderedGroup = { ...metaG };
  511. Object.entries(metaG).forEach(([key, metaFoG]) => {
  512. if (metaFoG.before) {
  513. reorderedGroup = insertBefore(reorderedGroup, metaFoG.before, key);
  514. } else if (metaFoG.after) {
  515. reorderedGroup = insertAfter(reorderedGroup, metaFoG.after, key);
  516. }
  517. if (isContainer(metaFoG)) {
  518. reorderedGroup[key].meta = execMetaReorderingInstructions(metaFoG.meta);
  519. }
  520. });
  521. return reorderedGroup;
  522. };
  523. const execMetaReorderingInstructions = (metaG: StringMap<any>) => {
  524. // Repeating Containers (which have array meta *at this point*) can't be reordered, but other types of containers can
  525. return Array.isArray(metaG) ? cloneDeep(metaG) : _execMetaReorderingInstructions(cloneDeep(metaG));
  526. };
  527. // ---------------------------------------------------------------------------------------------------------------------
  528. // Generate new model, without mutating original
  529. // (used to produce an updated copy of a model when form values are changed - will not create new keys)
  530. // ---------------------------------------------------------------------------------------------------------------------
  531. const isRealObject = val => val !== null && typeof val === 'object'; // js quirk - typeof null ---> 'object'
  532. const generateNewModel = (originalModel, updates) => {
  533. return updateObject(originalModel, updates);
  534. };
  535. // NOT FINISHED!!!
  536. const updateMeta = (newMeta: StringMap<any>, path: string, meta: StringMap<any>): StringMap<any> => {
  537. // TODO: Finish this later
  538. if (path === '/') {
  539. const updatedMeta = updateObject(meta || this.meta, newMeta, true);
  540. return updatedMeta;
  541. }
  542. // Drill down and update the branch specified by 'path' - all but final segment indicates a container
  543. // What about array types? Think about this later!
  544. // What about group components. Think about this later.
  545. // What about bad paths. Think about this later.
  546. console.log(path);
  547. const segments = path.split('.');
  548. console.log(segments);
  549. let branchMeta = meta;
  550. while (isContainer(branchMeta)) {
  551. const s = segments.shift();
  552. console.log(s, branchMeta[s].meta);
  553. // TODO: add array check
  554. branchMeta = branchMeta[s].meta;
  555. }
  556. while (segments.length > 1) {
  557. const s = segments.shift();
  558. console.log(s, branchMeta[s]);
  559. // TODO: add array check
  560. branchMeta = branchMeta[s];
  561. }
  562. branchMeta = branchMeta[segments[0]];
  563. console.log(segments[0], branchMeta);
  564. // Then something like...
  565. const updatedMeta = updateObject(branchMeta, newMeta, true);
  566. branchMeta = updatedMeta;
  567. console.log(branchMeta);
  568. return meta;
  569. };
  570. const updateObject = (obj, updates, createAdditionalKeys = false) => {
  571. // THIS DOES NOT MUTATE obj, instead returning a new object
  572. if (!isRealObject(obj)) {
  573. obj = {};
  574. }
  575. if (Object.keys(obj).length === 0) {
  576. createAdditionalKeys = true;
  577. }
  578. const shallowClone = { ...obj }; // This might be inefficient - consider using immutable or immer
  579. Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
  580. return shallowClone;
  581. };
  582. const safeSet = (obj, key, val, createAdditionalKeys = false) => {
  583. // THIS MUTATES obj - consider the wisdom of this
  584. if (!createAdditionalKeys && !obj.hasOwnProperty(key)) {
  585. return;
  586. }
  587. const currentVal = obj[key];
  588. if (val === currentVal) {
  589. return;
  590. }
  591. if (undefinedNullOrScalar(currentVal)) {
  592. // console.log('safeSet undefinedNullOrScalar', key, val);
  593. obj[key] = val;
  594. } else {
  595. if (Array.isArray(currentVal)) {
  596. if (typeof val === 'object') {
  597. // Replace array
  598. console.log('safeSet array', key, val);
  599. obj[key] = Array.isArray(val) ? val : Object.values(val);
  600. } else {
  601. // Append to end of existing array? Create a single element array?
  602. throw new Error(`safeSet Error: Expected array or object @key ${key} but got scalar
  603. Rejected update was ${val}`);
  604. }
  605. } else if (typeof val === 'object') {
  606. // Deep merge
  607. obj[key] = updateObject(obj[key], val, createAdditionalKeys);
  608. } else {
  609. throw new Error(`safeSet Error: Can't deep merge object into scalar @key ${key}
  610. Rejected update was ${val}`);
  611. }
  612. }
  613. };
  614. const undefinedNullOrScalar = val => {
  615. if (val === null || val === undefined) { return true; }
  616. const t = typeof val;
  617. return t === 'number' || t === 'string' || t === 'boolean';
  618. };
  619. // ---------------------------------------------------------------------------------------------------------------------
  620. // Helper Functions
  621. // ---------------------------------------------------------------------------------------------------------------------
  622. // Add Property to object, returning updated object
  623. const addProp = (obj, key, val) => { obj[key] = val; return obj; };
  624. // Has Meta
  625. // Helper function to distinguish a group from a simple field - where a group is a container os repeating container, or repeating field * AFTER* modelling
  626. const hasMeta = (metaFoG): boolean => !!metaFoG.meta;
  627. // Is Repeating
  628. const isRepeating = (metaFoG): boolean => !!(metaFoG.minRepeat || metaFoG.maxRepeat || metaFoG.initialRepeat);
  629. // Is Repeating Field
  630. // Helper function to distinguish a repeating field (a field that can be repeated 1...N times)
  631. const isRepeatingField = (metaRF): boolean => metaRF.type && metaRF.type.toLowerCase() === 'repeatingfield'
  632. || (
  633. !metaRF.type
  634. && isRepeating(metaRF)
  635. && (
  636. !hasMeta(metaRF) || hasMeta (metaRF)
  637. && Array.isArray(metaRF.meta)
  638. && metaRF.meta[0]
  639. && isScalar(Object.values(metaRF.meta[0])[0])
  640. )
  641. );
  642. // Is Container
  643. // Helper function to distinguish container group (a group of child fields)
  644. const isContainer = (metaFoG): boolean => metaFoG.type && metaFoG.type.toLowerCase() === 'container'
  645. || (
  646. !metaFoG.type
  647. && hasMeta(metaFoG)
  648. && !Array.isArray(metaFoG.meta)
  649. );
  650. // Is Repeating Container
  651. // Helper function to distinguish a repeating container group (a group of child fields that can be repeated 1...N times)
  652. const isRepeatingContainer = (metaFoG): boolean => metaFoG.type && metaFoG.type.toLowerCase() === 'repeatingcontainer'
  653. || (
  654. !metaFoG.type
  655. && isRepeating(metaFoG)
  656. && hasMeta(metaFoG)
  657. && Array.isArray(metaFoG.meta) && metaFoG.meta[0]
  658. && !isScalar(Object.values(metaFoG.meta[0])[0])
  659. );
  660. // Add Missing Names
  661. // Helper function to add any missing 'name' properties to Fields and Groups using property's key, recursively
  662. // BUT not to repeatingContainer members
  663. const addNameIfMissing = (metaFoG, key) => metaFoG.name ? metaFoG : addProp(metaFoG, 'name', key);
  664. const addNameToSelfAndChildren = ( [key, metaFoG] ) => {
  665. metaFoG = addNameIfMissing(metaFoG, key);
  666. if (hasMeta(metaFoG) && !isRepeatingContainer(metaFoG)) {
  667. metaFoG.meta = isArray(metaFoG.meta) ? Object.values(addMissingNames(metaFoG.meta)) : addMissingNames(metaFoG.meta); // Recursion
  668. }
  669. return [key, metaFoG];
  670. };
  671. const addMissingNames = metaG => Object.entries(metaG)
  672. .map(addNameToSelfAndChildren)
  673. .reduce((res, [key, val]) => addProp(res, key, val), {});
  674. // Add Missing Field-Specific Meta
  675. // Helper function to add any missing Field-Specific Metadata (using models in dynaform/models), recursively
  676. // Checks the constrctor, which should NOT be a plain Object, but rather TextField, TextareaField, SelectField, etc.
  677. const add_FSM_IfMissing = metaFoG => metaFoG.constructor.name === 'Object' ? buildModeledFieldGroupMember(metaFoG) : metaFoG;
  678. const add_FSM_ToSelfAndChildren = ( [key, metaFoG] ) => {
  679. metaFoG = add_FSM_IfMissing(metaFoG);
  680. return [key, metaFoG];
  681. };
  682. const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
  683. .map(add_FSM_ToSelfAndChildren)
  684. .reduce((res, [key, val]) => addProp(res, key, val), {});
  685. // ---------------------------------------------------------------------------------------------------------------------
  686. // Exports
  687. // ---------------------------------------------------------------------------------------------------------------------
  688. export {
  689. autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
  690. buildFieldSpecificMetaInClosure, extractFieldMappings,
  691. buildFormGroupFunctionFactory,
  692. resetAsyncValidatorsRecursive,
  693. touchAndUpdateValidityRecursive, resetValidityRecursive,
  694. generateNewModel, updateMeta,
  695. };