123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- import { Component, Input, Output, EventEmitter, TemplateRef, Optional, OnInit, OnChanges, ChangeDetectionStrategy } from '@angular/core';
- import { FormBuilder, FormControl, FormGroup, FormArray, FormGroupName, AbstractControl, ControlContainer } from '@angular/forms';
- import { SuperForm } from 'angular-super-validator';
- import { buildFormGroupFunctionFactory, buildFormControl } from './services/_formdata-utils';
- import { cloneDeep } from 'lodash/fp';
- export interface DynarowContext {
- control: AbstractControl;
- meta: StringMap<any>;
- }
- @Component({
- selector: 'app-dynaform',
- templateUrl: './dynaform.component.html',
- styleUrls: ['./dynaform.component.scss'],
- changeDetection: ChangeDetectionStrategy.OnPush // or ChangeDetectionStrategy.OnPush - should be more efficient. Experiment later.
- })
- export class DynaformComponent implements OnInit, OnChanges {
- /*
- * 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) {
- // console.log('Dynaform Set Meta');
- // this.formMetaData = this.formMetaData || data; // WHY? WHY? WHY? - leave in for now, just in case
- this.formMetaData = data;
- }
- @Input()
- template?: TemplateRef<any>;
- @Input()
- debug = false;
- @Output()
- call: EventEmitter<string> = new EventEmitter<string>();
- formMetaData: StringMap<any>; // 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;';
- conOlive = 'color: white; background-color: olive; font-weight: bold;';
- constructor(
- @Optional() private cc: ControlContainer
- ) {}
- ngOnInit() {
- // console.log('Dyanaform ngOnInit');
- }
- ngOnChanges() {
- // Triggered when inputs change
- console.log('%c *** DynaformChange *** ', this.conOlive);
- console.log(this.formMetaData);
- // 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 cant't find formGroup from formGroupName ${this.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;
- }
- getEmbeddedDynaformClasses(meta: StringMap<any>): StringMap<boolean> {
- let ngClassObj = {};
- if (Array.isArray(meta.class)) {
- ngClassObj = (meta.class as string[]).reduce((acc, className) => (acc[className] = true, acc), ngClassObj);
- } else if (typeof meta.class === 'string' && meta.class) {
- ngClassObj[meta.class] = true;
- }
- return ngClassObj;
- }
- isField(meta: StringMap<any>): boolean {
- return !meta.type.includes('Container') && meta.type !== 'RepeatingField';
- }
- getTemplateContext(controlName: string): DynarowContext {
- return {
- control: this.formGroup.get(controlName),
- meta: this.formMetaData[controlName]
- };
- }
- getRepeatingTemplateContext(name: string, index: number): DynarowContext {
- const rcFormArray = this.formGroup.get(name) as FormArray;
- const result = {
- control: rcFormArray.at(index),
- meta: this.formMetaData[name]['meta'][index]
- };
- return result;
- }
- getRowClass(control: FormControl, meta: StringMap<any>): string {
- const fieldTypeClass = meta.type ? meta.type.toLowerCase().replace('component', '') : '';
- const fieldClass = Array.isArray(meta.class) ? meta.class.join(' ') : meta.class;
- const containerClass = fieldClass ? (meta.type === 'Container' ? ` ${fieldClass}` : ` row-${fieldClass}`) : '';
- const errorClass = control && control.touched && control.invalid ? ' dyna-error' : '';
- return `row-${fieldTypeClass}${containerClass}${errorClass}`;
- }
- getRepeatingContainerLabel(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 rcFormArray = this.formGroup.get(repeatingContainerName) as FormArray;
- const formGroup = rcFormArray.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(name: string, index: number): void {
- // Show a particular member of a Repeating Container Group (used when only one is shown at a time i.e display = 'SINGLE')
- const rcMeta = this.formMetaData[name];
- rcMeta.meta = rcMeta.meta.map( (container, i) => ({ ...container, focussed: i === index }) );
- }
- addAllowed(name: string): boolean {
- const meta = this.formMetaData[name];
- return typeof meta.maxRepeat === 'number' && meta.maxRepeat > meta.meta.length;
- }
- // Maybe move the AddRF and deleteRF funtions to _formdata-utils.ts ?
- addRepeatingFieldMember(name: string): void {
- // (1) Check that we can still add controls
- if (!this.addAllowed(name)) {
- return;
- }
- // (2) Add metadata for new container member
- const rfMeta = this.formMetaData[name];
- const fieldTemplate = cloneDeep(rfMeta.__template);
- const i = this.formMetaData[name].meta.length;
- fieldTemplate.name = `group${i+1}`; // CHECK
- rfMeta.meta.push(fieldTemplate);
- // (3) Add FormControl to FormArray
- const newFormControl = buildFormControl(fieldTemplate);
- (this.formGroup.get(name) as FormArray).push(newFormControl);
- }
- // Maybe move the AddRC and deleteRC funtions to _formdata-utils.ts ?
- addRepeatingContainerMember(name: string): void {
- // (1) Check that we can still add controls
- if (!this.addAllowed(name)) {
- return;
- }
- // (2) Add metadata for new container member
- const rcMeta = this.formMetaData[name];
- const containerTemplate = cloneDeep(rcMeta.__template);
- const i = this.formMetaData[name].meta.length;
- containerTemplate.name = `group${i+1}`;
- rcMeta.meta.push(containerTemplate);
- // (3) Add FormGroup for new container member
- const buildFormGroup = buildFormGroupFunctionFactory(new FormBuilder());
- const newFormGroup = buildFormGroup(containerTemplate.meta);
- (this.formGroup.get(name) as FormArray).push(newFormGroup);
- // (4) Focus new container member if display = 'SINGLE' (i.e. we're only showing one at once)
- if (rcMeta.display === 'SINGLE') {
- this.focusContainer(name, i);
- }
- }
- deleteAllowed(name: string): boolean {
- const meta = this.formMetaData[name];
- return typeof meta.minRepeat === 'number' && meta.minRepeat < meta.meta.length;
- }
- // Maybe move the AddRC and deleteRC funtions to _formdata-utils.ts ?
- deleteRepeatingMember(name: string, index: number): void {
- // (1) Check that we can still delete controls
- if (!this.deleteAllowed(name)) {
- return;
- }
- // (2) Delete from the metadata, and rename the groups
- const rcMeta = this.formMetaData[name];
- const metaArr = rcMeta.meta;
- const newMetaArr = [ ...metaArr.slice(0, index), ...metaArr.slice(index + 1) ]
- .map((m, i) => { m.name = `group${i+1}`; return m; });
- rcMeta.meta = newMetaArr;
- // (3) Delete the corresponding FormGroup from the FormArray
- (this.formGroup.get(name) as FormArray).removeAt(index);
- // (4) Focus the last if display = 'SINGLE' (i.e. we're only showing one at once)
- if (rcMeta.display === 'SINGLE') {
- this.focusContainer(name, newMetaArr.length - 1);
- }
- }
- getValidationFailureMessage(control: FormControl, meta: StringMap<any>) {
- if (control.errors) {
- const errKeys = Object.keys(control.errors);
- console.log(errKeys);
- return meta.valFailureMsgs[errKeys[0]];
- }
- }
- getValidationErrors() {
- if (!this.formGroup.valid) {
- const errorsFlat = SuperForm.getAllErrorsFlat(this.formGroup);
- return errorsFlat;
- }
- return false;
- }
- handleCallback(fnId: string) {
- this.call.emit(fnId);
- }
- private getContolKeysCSVFromMetadata(metadata: StringMap<any>): 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 'noFormControl' flag)
- // e.g. ButtonGroups, HTMLChunks, etc.
- return Object.entries(metadata)
- .filter(([key, val]) => !(val as StringMap<any>).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);
- }
- }
- }
|