dynaform.service.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /* *********************************************************************************************************************
  2. * Dynaform Service
  3. * *********************************************************************************************************************
  4. *
  5. * BUILD
  6. * -----
  7. * This is the main method you'll use to build forms from models and metadata:
  8. *
  9. * build(model, meta = {}, createFromMeta = false)
  10. *
  11. * Takes a model and (lazy)metadata and returns an object containing a FormGroup and Modeled MetaData :
  12. * {
  13. * form: FormGroup,
  14. * meta: ModeledMetaData
  15. * }
  16. *
  17. * meta is optional, and if not supplied all the model fields will become Text inputs in the FormGroup,
  18. * with their labels set to the un-camel-cased property name
  19. *
  20. * createFromMeta is optional.
  21. * If true it will create new fields in the FormGroup even when they don't already exist in the model,
  22. * but exist in the metadata. This is NOT the default behaviour, except when an empty model is supplied.
  23. * i.e. It defaults to false, except when model == {}
  24. *
  25. * Usage
  26. * -----
  27. *
  28. * build(model) - build everything from the model
  29. * build({}, meta) - build everything from the metadata
  30. * build(model, meta) - build by combining the model with metadata, lazyily (not every model field needs metadata, as sensible defaults)
  31. * build(model, meta, true) - build by combining model with metadata, creating new fields from metadata points that don't occur in the model
  32. *
  33. *
  34. * BUILD STRATEGYS
  35. * ---------------
  36. *
  37. * MODELFIRST - The model determinies the shape of the form
  38. * METAFIRST - The metadata determines the shape of the form
  39. *
  40. * Set the build strategy using the setBuildStrategy method e.g.
  41. *
  42. * setBuildStrategy('MODELFIRST') - the default
  43. * setBuildStrategy('NETAFIRST')
  44. *
  45. * then use the build function in the normal way
  46. *
  47. *
  48. * REGISTER
  49. * --------
  50. * Registers callbacks attached to the form (e.g. to buttons), identified by strings.
  51. *
  52. * register({
  53. * 'SAYHELLO': () => { alert('HELLO') },
  54. * 'SEARCH': execSearch,
  55. * 'NEW': addNew,
  56. * }, varToBind);
  57. *
  58. * If varToBind is supplied it is bound as 'this' to the functions.
  59. * Typically you'd supply the component class instance, so that 'this' used in callbacks refers to the host component.
  60. *
  61. *
  62. * DATA IN & DATA OUT
  63. * ------------------
  64. * updateForm - patch form values in FormGroup
  65. * buildNewModel
  66. *
  67. *
  68. * VALIDATION
  69. * ----------
  70. * getValidationErrors - get all the validation erros
  71. * updateValidity - mark all controls as touched, and update their validity state
  72. * resetValidity - mark all controls as pristine, and remove all validation failure messages
  73. *
  74. * LOWER-LEVEL METHODS
  75. * -------------------
  76. *
  77. * autoBuildFormGroupAndMeta(model, meta, createFromMeta) - synonym for build
  78. * autoBuildModeledMeta(model, meta, createFromMeta) - takes a model and (lazy)metadata and returns expanded metadata
  79. *
  80. * buildFormGroup(metadata) - builds FormGroups from modelled metdata, recursively if necessary
  81. * buildFieldSpecificMeta(metadata) - use field metadata models to fill out metadata
  82. * combineModelWithMeta(model, extraMeta) - automatically generated metadata for model then combines extra metadata
  83. * combineExtraMeta(metadata, extraMeta) - combine extra metadata into metatdata, lazyly and recursively
  84. * autoMeta(model) - generate basic metadata from a raw or mapped model, recursively if necessary
  85. *
  86. *
  87. * NOTES
  88. * -----
  89. * This class acts as an injectable wraper around the exports of _formdata-utils.ts,
  90. * as well as creating a buildFormGroup function using the injected FormBuilder singleton
  91. *
  92. *
  93. * EXAMPLES
  94. * --------
  95. *
  96. * TO ADD ...
  97. *
  98. */
  99. import { Injectable, ComponentRef } from '@angular/core';
  100. import { FormBuilder, FormGroup } from '@angular/forms';
  101. import { SuperForm } from 'angular-super-validator';
  102. import { ModelMapperService } from './model-mapper.service';
  103. import {
  104. autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
  105. buildFieldSpecificMetaInClosure, extractFieldMappings,
  106. buildFormGroupFunctionFactory,
  107. resetAsyncValidatorsRecursive,
  108. touchAndUpdateValidityRecursive, resetValidityRecursive, advanceUpdateStategyOfInvalidControlsRecursive,
  109. generateNewModel, updateMeta
  110. } from './_formdata-utils';
  111. import { Option } from './../models/field.model';
  112. export interface IFormAndMeta {
  113. form: FormGroup;
  114. meta: StringMap<any>;
  115. }
  116. export interface ICallbacks {
  117. [index: string]: () => void;
  118. }
  119. @Injectable()
  120. export class DynaformService {
  121. public form: FormGroup;
  122. public meta: StringMap<any>;
  123. private context: any;
  124. public buildFormGroup: (meta) => FormGroup;
  125. private buildStrategy: 'MODELFIRST' | 'METAFIRST' = 'MODELFIRST'; // Make ENUM type
  126. private callbacks: ICallbacks = {};
  127. private conGreen = 'color: white; background-color: green; font-weight: bold;';
  128. constructor(private fb: FormBuilder, private modelMapper: ModelMapperService) {
  129. this.buildFormGroup = buildFormGroupFunctionFactory(fb);
  130. }
  131. setBuildStrategy(str): void {
  132. switch (str.toUpperCase()) {
  133. case 'MODELFIRST': // Build from model, combining optional extra metadata
  134. case 'METAFIRST': // Build from metadata, sourcing values from the model (optionally, using meta's 'source' attribute)
  135. this.buildStrategy = str.toUpperCase();
  136. break;
  137. default:
  138. throw new Error(`DYNAFORM: Unknown Build Strategy ${str} - should be MODELFIRST or METAFIRST`);
  139. }
  140. }
  141. setContext(data: any): void {
  142. // Set any runtime data needed to build the form, e.g. options values
  143. this.context = data;
  144. }
  145. build(model: StringMap<any>, meta = {}, createFromMeta = false): IFormAndMeta {
  146. // Executes autoBuildFormGroupAndMeta and stores the result
  147. const result = this.autoBuildFormGroupAndMeta(model, meta, createFromMeta);
  148. ({ form: this.form, meta: this.meta } = result);
  149. return result;
  150. }
  151. register(callbacks: ICallbacks, cref: ComponentRef<any>['instance']) {
  152. if (cref) {
  153. // Bind the component instance to the callback, so that 'this' has the context of the component
  154. Object.entries(callbacks).forEach(([key, fn]) => this.callbacks[key] = fn.bind(cref));
  155. } else {
  156. Object.assign(this.callbacks, callbacks);
  157. }
  158. }
  159. call(fnId: string) {
  160. // Handle callbacks
  161. if (typeof this.callbacks[fnId] === 'function') {
  162. try {
  163. this.callbacks[fnId]();
  164. } catch (e) {
  165. console.error('Error thrown during callback', fnId);
  166. console.error(e);
  167. }
  168. } else {
  169. console.error('Dynaform has no registered callback for', fnId);
  170. }
  171. }
  172. updateForm(newModel: StringMap<any>, form?: FormGroup, meta?: StringMap<any>): void {
  173. const mapping = extractFieldMappings(meta || this.meta); // Memoize
  174. const mappedModel = this.modelMapper.reverseMap(newModel, mapping);
  175. (form || this.form).patchValue(mappedModel);
  176. }
  177. updateMeta(newMeta: StringMap<any>, path: string = '/', meta?: StringMap<any>): StringMap<any> {
  178. const newFullMeta = updateMeta(newMeta, path, meta || this.meta);
  179. if (!meta) {
  180. this.meta = newFullMeta;
  181. }
  182. return newFullMeta;
  183. }
  184. resetForm(form?: FormGroup): void {
  185. // Loop through the form and call reset on any Async validators
  186. resetAsyncValidatorsRecursive(form || this.form);
  187. (form || this.form).reset();
  188. }
  189. buildNewModel(originalModel: StringMap<any>, formVal?: StringMap<any>, meta?: StringMap<any>): StringMap<any> {
  190. console.log('%c *** buildNewModel *** ', this.conGreen);
  191. const mapping = extractFieldMappings(meta || this.meta); // Memoize
  192. console.dir(mapping);
  193. const updates = this.modelMapper.forwardMap(formVal || this.form.value, mapping);
  194. console.log('%c *** Updates *** ', this.conGreen);
  195. console.dir(updates);
  196. return generateNewModel(originalModel, updates);
  197. }
  198. getValidationErrors(form?: FormGroup): StringMap<string> | false {
  199. const _form = form || this.form;
  200. if (!_form.valid) {
  201. const errorsFlat = SuperForm.getAllErrorsFlat(_form);
  202. return errorsFlat;
  203. }
  204. return false;
  205. }
  206. updateValidity(form?: FormGroup): void {
  207. touchAndUpdateValidityRecursive(form || this.form);
  208. }
  209. resetValidity(form?: FormGroup): void {
  210. resetValidityRecursive(form || this.form);
  211. }
  212. goRealTime(form?: FormGroup): void {
  213. advanceUpdateStategyOfInvalidControlsRecursive(form || this.form);
  214. }
  215. // -----------------------------------------------------------------------------------------------------------------
  216. // Convenience methods combining several steps
  217. autoBuildFormGroupAndMeta(model: StringMap<any>, meta = {}, createFromMeta = false): IFormAndMeta {
  218. let _model;
  219. if (this.buildStrategy === 'MODELFIRST') {
  220. _model = model;
  221. } else if (this.buildStrategy === 'METAFIRST') {
  222. // Generate mapping from the 'source' attributes in the metadata (compatible with the model-mapper service)
  223. // Then grab values from the model
  224. const mapping = extractFieldMappings(meta);
  225. _model = this.modelMapper.reverseMap(model, mapping, false); // <-- false - excludes bits of model not explicitly referenced in meta
  226. console.log('%c *** Mapping and Mapped Model *** ', this.conGreen);
  227. console.dir(mapping);
  228. console.dir(_model);
  229. }
  230. if (this.buildStrategy === 'METAFIRST' || Object.keys(model).length === 0) {
  231. createFromMeta = true;
  232. }
  233. const fullMeta = this.autoBuildModeledMeta(_model, meta, createFromMeta);
  234. return {
  235. form: this.buildFormGroup(fullMeta),
  236. meta: fullMeta
  237. };
  238. }
  239. autoBuildModeledMeta(model: StringMap<any>, meta = {}, createFromMeta = false) {
  240. const modelWithMeta = this.combineModelWithMeta(model, meta, createFromMeta);
  241. const reorderedMeta = execMetaReorderingInstructions(modelWithMeta);
  242. return this.buildFieldSpecificMeta(reorderedMeta);
  243. }
  244. // -----------------------------------------------------------------------------------------------------------------
  245. // Build field-type-specific metadata using the form field models (see dynaform/models)
  246. buildFieldSpecificMeta(meta) {
  247. return buildFieldSpecificMetaInClosure(meta, this.context);
  248. }
  249. // -----------------------------------------------------------------------------------------------------------------
  250. // Lower-level methods
  251. combineModelWithMeta(model: StringMap<any>, meta, createFromMeta = false) {
  252. return combineModelWithMeta(model, meta, createFromMeta);
  253. }
  254. combineExtraMeta(meta, extraMeta, createFromExtra = false) {
  255. return combineExtraMeta(meta, extraMeta, createFromExtra);
  256. }
  257. autoMeta(model) {
  258. return autoMeta(model);
  259. }
  260. // -----------------------------------------------------------------------------------------------------------------
  261. // Convenience methods
  262. buildOptions(options): Option[] {
  263. if (Array.isArray(options)) {
  264. return options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
  265. } else {
  266. throw new Error('Options must be supplied as an array');
  267. }
  268. }
  269. }