import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { FormGroup, FormGroupDirective } from '@angular/forms'; import { BaseApplication } from '@core/typings/application.typing'; import { FormComponentValidChange, FormDefinitionComponent, FormValueChange } from '@features/configure-forms/form.typing'; import { FormBuilderService } from '@features/formio/form-builder/services/form-builder/form-builder.service'; import { GcFormRendererComponent } from '@features/formio/form-renderer/gc-form-renderer/gc-form-renderer.component'; import { ComponentHelperService } from '@features/formio/services/component-helper/component-helper.service'; import { SimpleStringMap, TypeSafeFormBuilder, TypeSafeFormGroup } from '@yourcause/common'; import { isEqual, isUndefined } from 'lodash'; import { Subscription } from 'rxjs'; export interface FormRendererFormGroup { formGroup: FormGroup; compKey: string; } @Component({ template: '' }) export class BaseFormioComponent implements OnInit, OnChanges, OnDestroy { @Input() label: string; @Input() placeholder: string; @Input() description: string; @Input() tooltipText: string; @Input() translations: SimpleStringMap; @Input() defaultVal: string; @Input() hideLabel: boolean; @Input() tabIndex: number; protected _data: T; protected saveDisabled = false; protected _dirty = false; @Input() public get data (): T { return this._data; } public set data (value: T) { if (!isEqual(this._data, value)) { this._data = value; if (this.compKey) { this.patchValue(); } } } @Input() parentFields: Partial; @Input() isForSetValue: boolean; @Input() hasCustomValidationError: boolean; @Input() emitInitialValidity: boolean; @Input() showError: boolean; @Input() disabledOverride: boolean; @Input() disabled: boolean; @Output() dataChange = new EventEmitter(); // To display this component outside of formio, pass in formioComponent @Input() formioComponent: FormDefinitionComponent; @Output() onValueChange = new EventEmitter(); @Output() onValidChange = new EventEmitter(); sub = new Subscription(); get dirty (): boolean { return this._dirty; } set dirty (dirty: boolean) { this._dirty = dirty; } compKey: string; formGroup: TypeSafeFormGroup; afterInit = false; constructor ( public renderer: GcFormRendererComponent, public formGroupDir: FormGroupDirective, public formBuilder: TypeSafeFormBuilder, public formBuilderService: FormBuilderService, public componentHelper: ComponentHelperService ) { } get comp () { return this.formioComponent; } get control () { return this.formGroup?.get(this.compKey); } get inFormBuilder () { return this.formBuilderService.inFormBuilder; } get isDisabled () { if ( !isUndefined(this.disabledOverride) && this.disabledOverride !== null ) { return this.disabledOverride; } else { return this.disabled || this.componentHelper.isCompDisabled(this.comp); } } get isRequired () { if (this.comp) { return this.comp.validate.required; } return false; } ngOnInit () { this.setCompKey(); this.setDefaultTranslations(); this.afterInit = true; } ngOnChanges (changes: SimpleChanges) { if (this.afterInit) { if (!this.inFormBuilder) { if (changes.hasCustomValidationError) { this.control?.updateValueAndValidity(); } if ( changes.showError && this.showError && (this.control?.invalid || this.formGroup?.invalid) ) { this.control?.markAsTouched(); this.control?.markAsDirty(); this.control?.updateValueAndValidity(); } } } } setDefaultTranslations () { if (Object.keys(this.translations || {}).length === 0) { this.translations = { [this.label]: this.label, [this.description]: this.description, [this.placeholder]: this.placeholder, [this.tooltipText]: this.tooltipText }; } this.label = this.translations[this.label] || this.label; this.description = this.translations[this.description] || this.description; this.placeholder = this.translations[this.placeholder] || this.translations.placeholder || this.placeholder; this.tooltipText = this.translations[this.tooltipText] || this.tooltipText; } /** * Adapt the key for default value, * so the controls in form builder have different names / are not bound to eachother */ getAdaptedKeyFromComponentKey ( componentKey: string, isForSetValue: boolean ) { return isForSetValue ? `defaultValue_${componentKey}` : componentKey; } setCompKey () { this.compKey = this.getAdaptedKeyFromComponentKey( this.comp.key, this.isForSetValue ); } setFormGroup (controlValue: any, validators: any[]) { if ( ( !this.inFormBuilder && ( this.comp.customValidation || !!this.comp.validate.custom ) ) ) { validators = [ ...validators, this.customValidator() ]; } const existingGroup = this.formGroupDir?.form.get(this.compKey); if (!existingGroup) { this.generateFormGroup(controlValue, validators); } else { this.formGroup = existingGroup as FormGroup; this.formGroup.get(this.compKey).setValidators(validators); // if we've viewed the group and it's invalid if (existingGroup.dirty && existingGroup.invalid) { setTimeout(() => { // update the value and validity (to show errors) // after the template renders existingGroup.updateValueAndValidity(); }); } } this.handleFormGroupReady(!existingGroup); } generateFormGroup ( controlValue: any, validators: any[] ) { this.formGroup = this.formBuilder.group({ [this.compKey]: [ controlValue, validators ] }); } handleFormGroupReady (shouldEmitFormGroupReady: boolean) { this.sub.add(this.control.statusChanges.subscribe(() => { setTimeout(() => { this.emitValidity(); }); })); if (shouldEmitFormGroupReady) { this.renderer?.onFormGroupReady({ formGroup: this.formGroup, compKey: this.compKey }); } if (this.emitInitialValidity) { this.emitValidity(); } } emitValidity () { this.onValidChange.emit({ isValid: this.control.valid, key: this.compKey }); } patchValue () { } emitData (dataToEmit: T) { if (!isEqual(this.data, dataToEmit)) { this._data = dataToEmit; if (!this.saveDisabled) { this.dataChange.emit(dataToEmit); } } } getValueOnShow () { return this.data; } customValidator () { return () => { const customValidationResult = this.comp?.validate.validationResult; const customValidationMessage = this.comp?.validate.customMessage; if (this.hasCustomValidationError) { if (customValidationResult) { return { customValidationError: { errorMessage: customValidationMessage || customValidationResult } }; } else { return { customValidationError: { errorMessage: this.comp?.customValidation?.result ?? customValidationMessage } }; } } return null; }; } ngOnDestroy () { this.sub.unsubscribe(); } }