123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- import { Component, Input, Output, EventEmitter, TemplateRef, Optional, OnInit, ChangeDetectionStrategy } from '@angular/core';
- import { FormControl, FormGroup, FormArray, FormGroupName, AbstractControl, ControlContainer } from '@angular/forms';
- import { SuperForm } from 'angular-super-validator';
- import { createMeta } from '@angular/platform-browser/src/browser/meta';
- export interface DynarowContext {
- control: AbstractControl;
- meta: StringMap;
- }
- @Component({
- selector: 'app-dynaform',
- templateUrl: './dynaform.component.html',
- styleUrls: ['./dynaform.component.scss'],
- changeDetection: ChangeDetectionStrategy.OnPush
- })
- export class DynaformComponent implements OnInit {
- /*
- * DynaformComponent: <app-dynaform>
- *
- * USAGE:
- *
- * Supply component with either a FormGroup or the name of a FormGroup,
- * the forms full metadata tree,
- * and, optionally, a TemplateRef used as a field row template (overrides the default template)
- *
- * e.g.
- * <app-dynaform [formGroup]="myForm" [meta]="myFormMetaData"></app-dynaform>
- * <app-dynaform formGroupName="myFormGroupName" [meta]="myFormMetaData"></app-dynaform>
- * <app-dynaform [formGroup]="myForm" [meta]="myFormMetaData" [template]="myFieldTemplate"></app-dynaform>
- *
- * If supplied with just a FormGroupName it will retieve the FormGroup from the injected ControlContainer
- *
- */
- @Input()
- formGroup?: FormGroup;
- @Input()
- formGroupName?: FormGroupName;
- @Input()
- set meta(data) {
- this.formMetaData = this.formMetaData || data;
- }
- @Input()
- template?: TemplateRef<any>;
- @Input()
- debug = false;
- @Output()
- call: EventEmitter<string> = new EventEmitter<string>();
- formMetaData: StringMap; // TODO: Tighten up type
- controlNames: string[];
- dynaFormRows: string[];
- path: string[]; // path of current FormGroup - can be used to respond differently based on nesting level in template
- // Colours for CSS in console
- conRed = 'color: white; background-color: maroon; font-weight: bold;';
- conGreen = 'color: white; background-color: green; font-weight: bold;';
- constructor(
- @Optional() private cc: ControlContainer,
- ) {}
- ngOnInit() {
- // Get the formGroup from the formGroupName if necessary
- if (!this.formGroup && this.formGroupName) {
- this.formGroup = this.cc.control as FormGroup; // Get theFormGroup from the injected ControlContainer
- }
- if (!this.formGroup) {
- throw new Error('Dynaform Component initialised without [formGroup] or formGroupName');
- }
- if (typeof this.formMetaData !== 'object') {
- throw new Error('Dynaform: [meta] should be an object');
- }
- this.controlNames = Object.keys(this.formGroup.controls);
- this.path = this.getFormGroupPath();
- if (this.debug && this.path.length < 2) {
- this.displayDebuggingInConsole();
- }
- // If we're given a formGroupName or nested FormGroup, and the form's full (or partial but fuller) metadata tree,
- // drill down to find *this* FormGroup's metadata
- const path = [...this.path]; // Clone to avoid mutating this.path
- const metaDataKeysExpected = this.controlNames.join(',');
- while (path.length && metaDataKeysExpected !== this.getContolKeysCSVFromMetadata(this.formMetaData)) {
- const branch = path.pop();
- this.formMetaData = this.formMetaData[branch].meta;
- }
- this.dynaFormRows = Object.keys(this.formMetaData);
- // Check we've got a "FormGroup <---> MetaData" match
- const metaDataKeysFound = this.getContolKeysCSVFromMetadata(this.formMetaData);
- if (metaDataKeysFound !== metaDataKeysExpected) {
- throw new Error(`
- Dynaform can't match FormGroup's controls with metadata
- Expected ${metaDataKeysExpected}
- Got ${metaDataKeysFound}`
- );
- }
- }
- getFormGroupPath(): string[] {
- // Get the full path of the FormGroup (used to match and validate metadata)
- let path = [];
- if (!this.formGroup && this.formGroupName) {
- // formGroupName supplied, so get the full path from the ControlContainer
- path = this.cc.path.reverse();
- } else {
- // formGroup supplied directly, so reconstruct current FormGroup's full path
- let fg = this.formGroup;
- while (fg.parent) {
- // Find the identity of 'fg' in the parent FormGroup's controls
- const fgIdentity = Object.entries(fg.parent.controls).find(([key, candidate]) => candidate === fg);
- path.push(fgIdentity[0]);
- fg = fg.parent as FormGroup;
- }
- }
- return path;
- }
- isField(meta: StringMap): boolean {
- return !meta.type.includes('Container');
- }
-
- isRepeatingContainer(meta: StringMap): boolean {
- return meta.type === 'RepeatingContainer';
- }
- getTemplateContext(controlName: string): DynarowContext {
- return {
- control: this.formGroup.get(controlName),
- meta: this.formMetaData[controlName]
- };
- }
- getRCTemplateContext(repeatingContainerName: string, index: number): DynarowContext {
- const repeatingContainerFormArray = this.formGroup.get(repeatingContainerName) as FormArray;
- const result = {
- control: repeatingContainerFormArray.at(index),
- meta: this.formMetaData[repeatingContainerName]['meta'][index]
- };
- return result;
- }
- getRowClass(control: FormControl, meta: StringMap): string {
- const fieldTypeClass = meta.type ? meta.type.toLowerCase().replace('component', '') : '';
- const fieldClass = Array.isArray(meta.class) ? meta.class.join(' ') : meta.class;
- const containerClass = fieldClass ? ` container-${fieldClass}` : '';
- const errorClass = control && control.touched && control.invalid ? ' dyna-error' : '';
- return `row-${fieldTypeClass}${containerClass}${errorClass}`;
- }
- getRCLabel(repeatingContainerName: string, index: number): string {
- // Get the label for a repeating container,
- // used on buttons to switch between containers (when only one is shown at a time i.e display = 'SINGLE')
- const rcMeta = this.formMetaData[repeatingContainerName];
- const primaryField = rcMeta.primaryField;
- if (primaryField) {
- // The primaryField has been specified, so return its value
- const repeatingContainerFormArray = this.formGroup.get(repeatingContainerName) as FormArray;
- const formGroup = repeatingContainerFormArray.at(index);
- return formGroup.get(primaryField).value;
- } else {
- // Otherwise return the 'button' of the container (in the array of containers)
- return rcMeta.meta[index].button;
- }
- }
- focusContainer(repeatingContainerName: string, index: number): void {
- // Show a particular container of a Repeating Container group (used when only one is shown at a time i.e display = 'SINGLE')
- const rcMeta = this.formMetaData[repeatingContainerName];
- rcMeta.meta = rcMeta.meta.map( (container, i) => ({ ...container, focussed: i === index }) );
- }
- getValidationFailureMessage(control: FormControl, meta: StringMap) {
- if (control.errors) {
- const errKeys = Object.keys(control.errors);
- console.log(errKeys);
- return meta.valFailureMessages[errKeys[0]];
- }
- }
- getValidationErrors() {
- if (!this.formGroup.valid) {
- const errorsFlat = SuperForm.getAllErrorsFlat(this.formGroup);
- return errorsFlat;
- }
- return 'No Errors';
- }
- handleCallback(fnId: string) {
- this.call.emit(fnId);
- }
- private getContolKeysCSVFromMetadata(metadata): string {
- // Return CSV of control keys in current nesting-level's metadata,
- // excluding metadata points that don't create FormControls, FromGroups or FormArrays
- // (identified by their 'noFormControsl' flag)
- // e.g. ButtonGroups, HTMLChunks, etc.
- return Object.entries(metadata)
- .filter(([key, val]) => !(val as StringMap).noFormControls)
- .reduce((acc, [key]) => [...acc, key], [])
- .join(',');
- }
- private displayDebuggingInConsole(): void {
- if (this.debug) {
- console.log('%c *** MetaData *** ', this.conGreen);
- console.dir(this.formMetaData);
- console.log('%c *** FormGroup *** ', this.conGreen);
- console.dir(this.formGroup);
- }
- }
- }
|