import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, EventEmitter, Input, OnDestroy, Output, QueryList, TemplateRef, } from '@angular/core'; import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; import { Observable, Subject, timer } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { PtDynamicFormsErrorTemplateDirective, PtDynamicSelectionOptionTemplateDirective, } from './dynamic-element.component'; import { IPtDynamicElementConfig, PtDynamicFormsService, PtDynamicElement } from './services/dynamic-forms.service'; import { DomSanitizer } from '@angular/platform-browser'; import { get, keys, each, isNil, some, reduce, includes } from 'lodash'; @Component({ selector: 'prutech-dynamic-forms', templateUrl: './dynamic-forms.component.html', styleUrls: ['./dynamic-forms.component.scss'], }) export class PtDynamicFormsComponent implements AfterContentInit, OnDestroy { private _renderedElements: IPtDynamicElementConfig[] = []; // tslint:disable-next-line:no-any private _errorTemplateMap: Map> = new Map>(); // tslint:disable-next-line:no-any private _optionTemplateMap: Map> = new Map>(); // tslint:disable-next-line:no-any private _destroy$: Subject = new Subject(); private _destroyControl$: Subject = new Subject(); private _elements: IPtDynamicElementConfig[]; readonly typeWithoutNameAsLabel: string[] = [PtDynamicElement.Radio]; keys: Function = keys; @Input() formControls: FormGroup; @Output() init: EventEmitter = new EventEmitter(); @ContentChildren(PtDynamicFormsErrorTemplateDirective, {descendants: true})_errorTemplates: QueryList; @ContentChildren(PtDynamicSelectionOptionTemplateDirective, {descendants: true}) _optionTemplates: QueryList; dynamicForm: FormGroup; constructor(private _formBuilder: FormBuilder, private _dynamicFormsService: PtDynamicFormsService, private _changeDetectorRef: ChangeDetectorRef, public sanitizer: DomSanitizer) { this.dynamicForm = this._formBuilder.group({}); console.log(sanitizer.bypassSecurityTrustStyle('-element-width: ' + 30 + '%')); } getLabel(label: string, name: string, type: string): string { if (includes(this.typeWithoutNameAsLabel, type)) { return label; } return label || name; } get elementsByRow(): IPtDynamicElementConfig[] { // tslint:disable-next-line:no-any return reduce(this._renderedElements, (acc: any, rec: IPtDynamicElementConfig) => { if (!acc[get(rec, 'row', 0)]) { acc[get(rec, 'row', 0)] = []; } acc[get(rec, 'row', 0)].push(rec); return acc; }, {}); } get elements(): IPtDynamicElementConfig[] { return this._renderedElements; } /** * elements: IPtDynamicElementConfig[] * JS Object that will render the elements depending on its config. * [name] property is required. */ @Input('elements') set elements(elements: IPtDynamicElementConfig[]) { if (elements) { this._elements = elements; } else { this._elements = []; } if (!this.formControls) { this._rerenderElements(); } else { this._renderedElements = this._elements.slice(); this._initialize(); } } get dynamicFormGroup(): FormGroup { return this.formControls || this.dynamicForm; } /** * Getter property for dynamic [FormGroup]. */ get form(): FormGroup { return this.dynamicForm; } /** * Getter property for [valid] of dynamic [FormGroup]. */ get valid(): boolean { if (this.dynamicForm) { return this.dynamicForm.valid; } return false; } /** * Getter property for [value] of dynamic [FormGroup]. */ // tslint:disable-next-line:no-any get value(): any { if (this.dynamicForm) { return this.dynamicForm.value; } return {}; } /** * Getter property for [errors] of dynamic [FormGroup]. */ // tslint:disable-next-line:no-any get errors(): { [name: string]: any } { if (this.dynamicForm) { // tslint:disable-next-line:no-any const errors: { [name: string]: any } = {}; for (const name of Object.keys(this.dynamicForm.controls)) { errors[name] = this.dynamicForm.controls[name].errors; } return errors; } return {}; } /** * Getter property for [controls] of dynamic [FormGroup]. */ get controls(): { [key: string]: AbstractControl } { if (this.dynamicForm) { return this.dynamicForm.controls; } return {}; } ngAfterContentInit(): void { this._updateErrorTemplates(); this._updateOptionTemplates(); } ngOnDestroy(): void { this._destroy$.next(); this._destroy$.complete(); this._destroyControl$.complete(); } /** * Refreshes the form and rerenders all validator/element modifications. */ refresh(): void { this._rerenderElements(); this._updateErrorTemplates(); this._updateOptionTemplates(); } /** * Getter method for error template references */ // tslint:disable-next-line:no-any getErrorTemplateRef(name: string): TemplateRef { return this._errorTemplateMap.get(name); } // tslint:disable-next-line:no-any getOptionTemplateRef(name: string): TemplateRef { return this._optionTemplateMap.get(name); } /** * Validate all from controls */ updateValueAndValidity(): void { each(keys(this.controls), (key: string) => { const control: AbstractControl = this.controls[key]; control.updateValueAndValidity(); }); } /** * Loads error templates and sets them in a map for faster access. */ private _updateErrorTemplates(): void { // tslint:disable-next-line:no-any this._errorTemplateMap = new Map>(); for (const errorTemplate of this._errorTemplates.toArray()) { this._errorTemplateMap.set(errorTemplate.tdDynamicFormsError, errorTemplate.templateRef); } } private _updateOptionTemplates(): void { // tslint:disable-next-line:no-any this._optionTemplateMap = new Map>(); for (const optionTemplate of this._optionTemplates.toArray()) { this._optionTemplateMap.set(optionTemplate.tdDynamicSelectOption, optionTemplate.templateRef); } } private _rerenderElements(): void { this._clearRemovedElements(); this._renderedElements = []; const duplicates: string[] = []; this._elements.forEach((elem: IPtDynamicElementConfig) => { this._dynamicFormsService.validateDynamicElementName(elem.name); if (duplicates.indexOf(elem.name) > -1) { throw new Error(`Dynamic element name: "${elem.name}" is duplicated`); } duplicates.push(elem.name); const dynamicElement: AbstractControl = this.dynamicForm.get(elem.name); if (!dynamicElement) { this.dynamicForm.addControl(elem.name, this._dynamicFormsService.createFormControl(elem)); this._subscribeToControlStatusChanges(elem.name); } else { dynamicElement.setValue(elem.default); dynamicElement.markAsPristine(); dynamicElement.markAsUntouched(); if (elem.disabled) { dynamicElement.disable(); } else { dynamicElement.enable(); } dynamicElement.setValidators(this._dynamicFormsService.createValidators(elem)); } // copy objects so they are only changes when calling this method this._renderedElements.push(Object.assign({}, elem)); }); this._initialize(); } private _initialize(): void { // call a change detection since the whole form might change this._changeDetectorRef.detectChanges(); timer() .toPromise() .then(() => { // call a markForCheck so elements are rendered correctly in OnPush this._changeDetectorRef.markForCheck(); this.init.emit(); }); } private _clearRemovedElements(): void { this._renderedElements = this._renderedElements.filter( (renderedElement: IPtDynamicElementConfig) => !this._elements.some((element: IPtDynamicElementConfig) => element.name === renderedElement.name), ); // remove elements that were removed from the array this._renderedElements.forEach((elem: IPtDynamicElementConfig) => { this._destroyControl$.next(elem.name); this.dynamicForm.removeControl(elem.name); }); } // Updates component when manually adding errors to controls private _subscribeToControlStatusChanges(elementName: string): void { const control: AbstractControl = this.controls[elementName]; // tslint:disable-next-line:no-any const controlDestroyed$: Observable = this._destroyControl$.pipe( filter((destroyedElementName: string) => destroyedElementName === elementName), ); control.statusChanges.pipe(takeUntil(this._destroy$), takeUntil(controlDestroyed$)).subscribe(() => { this._changeDetectorRef.markForCheck(); }); } }