dynaform.component.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { Component, Input, Output, EventEmitter, TemplateRef, Optional, OnInit, ChangeDetectionStrategy } from '@angular/core';
  2. import { FormControl, FormGroup, FormArray, FormGroupName, AbstractControl, ControlContainer } from '@angular/forms';
  3. import { SuperForm } from 'angular-super-validator';
  4. import { createMeta } from '@angular/platform-browser/src/browser/meta';
  5. export interface DynarowContext {
  6. control: AbstractControl;
  7. meta: StringMap;
  8. }
  9. @Component({
  10. selector: 'app-dynaform',
  11. templateUrl: './dynaform.component.html',
  12. styleUrls: ['./dynaform.component.scss'],
  13. changeDetection: ChangeDetectionStrategy.OnPush
  14. })
  15. export class DynaformComponent implements OnInit {
  16. /*
  17. * DynaformComponent: <app-dynaform>
  18. *
  19. * USAGE:
  20. *
  21. * Supply component with either a FormGroup or the name of a FormGroup,
  22. * the forms full metadata tree,
  23. * and, optionally, a TemplateRef used as a field row template (overrides the default template)
  24. *
  25. * e.g.
  26. * <app-dynaform [formGroup]="myForm" [meta]="myFormMetaData"></app-dynaform>
  27. * <app-dynaform formGroupName="myFormGroupName" [meta]="myFormMetaData"></app-dynaform>
  28. * <app-dynaform [formGroup]="myForm" [meta]="myFormMetaData" [template]="myFieldTemplate"></app-dynaform>
  29. *
  30. * If supplied with just a FormGroupName it will retieve the FormGroup from the injected ControlContainer
  31. *
  32. */
  33. @Input()
  34. formGroup?: FormGroup;
  35. @Input()
  36. formGroupName?: FormGroupName;
  37. @Input()
  38. set meta(data) {
  39. this.formMetaData = this.formMetaData || data;
  40. }
  41. @Input()
  42. template?: TemplateRef<any>;
  43. @Input()
  44. debug = false;
  45. @Output()
  46. call: EventEmitter<string> = new EventEmitter<string>();
  47. formMetaData: StringMap; // TODO: Tighten up type
  48. controlNames: string[];
  49. dynaFormRows: string[];
  50. path: string[]; // path of current FormGroup - can be used to respond differently based on nesting level in template
  51. // Colours for CSS in console
  52. conRed = 'color: white; background-color: maroon; font-weight: bold;';
  53. conGreen = 'color: white; background-color: green; font-weight: bold;';
  54. constructor(
  55. @Optional() private cc: ControlContainer,
  56. ) {}
  57. ngOnInit() {
  58. // Get the formGroup from the formGroupName if necessary
  59. if (!this.formGroup && this.formGroupName) {
  60. this.formGroup = this.cc.control as FormGroup; // Get theFormGroup from the injected ControlContainer
  61. }
  62. if (!this.formGroup) {
  63. throw new Error('Dynaform Component initialised without [formGroup] or formGroupName');
  64. }
  65. if (typeof this.formMetaData !== 'object') {
  66. throw new Error('Dynaform: [meta] should be an object');
  67. }
  68. this.controlNames = Object.keys(this.formGroup.controls);
  69. this.path = this.getFormGroupPath();
  70. if (this.debug && this.path.length < 2) {
  71. this.displayDebuggingInConsole();
  72. }
  73. // If we're given a formGroupName or nested FormGroup, and the form's full (or partial but fuller) metadata tree,
  74. // drill down to find *this* FormGroup's metadata
  75. const path = [...this.path]; // Clone to avoid mutating this.path
  76. const metaDataKeysExpected = this.controlNames.join(',');
  77. while (path.length && metaDataKeysExpected !== this.getContolKeysCSVFromMetadata(this.formMetaData)) {
  78. const branch = path.pop();
  79. this.formMetaData = this.formMetaData[branch].meta;
  80. }
  81. this.dynaFormRows = Object.keys(this.formMetaData);
  82. // Check we've got a "FormGroup <---> MetaData" match
  83. const metaDataKeysFound = this.getContolKeysCSVFromMetadata(this.formMetaData);
  84. if (metaDataKeysFound !== metaDataKeysExpected) {
  85. throw new Error(`
  86. Dynaform can't match FormGroup's controls with metadata
  87. Expected ${metaDataKeysExpected}
  88. Got ${metaDataKeysFound}`
  89. );
  90. }
  91. }
  92. getFormGroupPath(): string[] {
  93. // Get the full path of the FormGroup (used to match and validate metadata)
  94. let path = [];
  95. if (!this.formGroup && this.formGroupName) {
  96. // formGroupName supplied, so get the full path from the ControlContainer
  97. path = this.cc.path.reverse();
  98. } else {
  99. // formGroup supplied directly, so reconstruct current FormGroup's full path
  100. let fg = this.formGroup;
  101. while (fg.parent) {
  102. // Find the identity of 'fg' in the parent FormGroup's controls
  103. const fgIdentity = Object.entries(fg.parent.controls).find(([key, candidate]) => candidate === fg);
  104. path.push(fgIdentity[0]);
  105. fg = fg.parent as FormGroup;
  106. }
  107. }
  108. return path;
  109. }
  110. isField(meta: StringMap): boolean {
  111. return !meta.type.includes('Container');
  112. }
  113. isRepeatingContainer(meta: StringMap): boolean {
  114. return meta.type === 'RepeatingContainer';
  115. }
  116. getTemplateContext(controlName: string): DynarowContext {
  117. return {
  118. control: this.formGroup.get(controlName),
  119. meta: this.formMetaData[controlName]
  120. };
  121. }
  122. getRCTemplateContext(repeatingContainerName: string, index: number): DynarowContext {
  123. const repeatingContainerFormArray = this.formGroup.get(repeatingContainerName) as FormArray;
  124. const result = {
  125. control: repeatingContainerFormArray.at(index),
  126. meta: this.formMetaData[repeatingContainerName]['meta'][index]
  127. };
  128. return result;
  129. }
  130. getRowClass(control: FormControl, meta: StringMap): string {
  131. const fieldTypeClass = meta.type ? meta.type.toLowerCase().replace('component', '') : '';
  132. const fieldClass = Array.isArray(meta.class) ? meta.class.join(' ') : meta.class;
  133. const containerClass = fieldClass ? ` container-${fieldClass}` : '';
  134. const errorClass = control && control.touched && control.invalid ? ' dyna-error' : '';
  135. return `row-${fieldTypeClass}${containerClass}${errorClass}`;
  136. }
  137. getRCLabel(repeatingContainerName: string, index: number): string {
  138. // Get the label for a repeating container,
  139. // used on buttons to switch between containers (when only one is shown at a time i.e display = 'SINGLE')
  140. const rcMeta = this.formMetaData[repeatingContainerName];
  141. const primaryField = rcMeta.primaryField;
  142. if (primaryField) {
  143. // The primaryField has been specified, so return its value
  144. const repeatingContainerFormArray = this.formGroup.get(repeatingContainerName) as FormArray;
  145. const formGroup = repeatingContainerFormArray.at(index);
  146. return formGroup.get(primaryField).value;
  147. } else {
  148. // Otherwise return the 'button' of the container (in the array of containers)
  149. return rcMeta.meta[index].button;
  150. }
  151. }
  152. focusContainer(repeatingContainerName: string, index: number): void {
  153. // Show a particular container of a Repeating Container group (used when only one is shown at a time i.e display = 'SINGLE')
  154. const rcMeta = this.formMetaData[repeatingContainerName];
  155. rcMeta.meta = rcMeta.meta.map( (container, i) => ({ ...container, focussed: i === index }) );
  156. }
  157. getValidationFailureMessage(control: FormControl, meta: StringMap) {
  158. if (control.errors) {
  159. const errKeys = Object.keys(control.errors);
  160. console.log(errKeys);
  161. return meta.valFailureMessages[errKeys[0]];
  162. }
  163. }
  164. getValidationErrors() {
  165. if (!this.formGroup.valid) {
  166. const errorsFlat = SuperForm.getAllErrorsFlat(this.formGroup);
  167. return errorsFlat;
  168. }
  169. return 'No Errors';
  170. }
  171. handleCallback(fnId: string) {
  172. this.call.emit(fnId);
  173. }
  174. private getContolKeysCSVFromMetadata(metadata): string {
  175. // Return CSV of control keys in current nesting-level's metadata,
  176. // excluding metadata points that don't create FormControls, FromGroups or FormArrays
  177. // (identified by their 'noFormControsl' flag)
  178. // e.g. ButtonGroups, HTMLChunks, etc.
  179. return Object.entries(metadata)
  180. .filter(([key, val]) => !(val as StringMap).noFormControls)
  181. .reduce((acc, [key]) => [...acc, key], [])
  182. .join(',');
  183. }
  184. private displayDebuggingInConsole(): void {
  185. if (this.debug) {
  186. console.log('%c *** MetaData *** ', this.conGreen);
  187. console.dir(this.formMetaData);
  188. console.log('%c *** FormGroup *** ', this.conGreen);
  189. console.dir(this.formGroup);
  190. }
  191. }
  192. }