import { Injectable, Type } from '@angular/core'; import { FormControl, ValidatorFn, Validators } from '@angular/forms'; import { concat, find, flatMap, get, isArray, isEqual, isNil, uniq } from 'lodash'; import { BehaviorSubject } from 'rxjs'; import { PtDynamicAutocompleteComponent } from '../dynamic-elements/dynamic-autocomplete/dynamic-autocomplete.component'; import { PtDynamicCheckboxComponent } from '../dynamic-elements/dynamic-checkbox/dynamic-checkbox.component'; import { PtDynamicDatepickerComponent } from '../dynamic-elements/dynamic-datepicker/dynamic-datepicker.component'; import { PtDynamicFileInputComponent } from '../dynamic-elements/dynamic-file-input/dynamic-file-input.component'; import { PtDynamicInputComponent } from '../dynamic-elements/dynamic-input/dynamic-input.component'; import { PtDynamicSelectComponent } from '../dynamic-elements/dynamic-select/dynamic-select.component'; import { PtDynamicSlideToggleComponent } from '../dynamic-elements/dynamic-slide-toggle/dynamic-slide-toggle.component'; import { PtDynamicSliderComponent } from '../dynamic-elements/dynamic-slider/dynamic-slider.component'; import { PtDynamicTextareaComponent } from '../dynamic-elements/dynamic-textarea/dynamic-textarea.component'; import { PtDynamicRadioButtonComponent } from '../dynamic-elements/dynamic-radio-button/dynamic-radio-button.component'; import * as momentNs from 'moment'; import { phoneValidator } from '../../shared/validators/phone-validator'; import { zipCodeValidator } from '../../shared/validators/zip-code-validator'; import { ssnValidator } from '../../shared/validators/ssn-validator'; import { ReferenceData } from '../../shared/models/reference-data'; import { autocompleteValidator } from '../../shared/validators/autocomplete-validator'; import { Entity } from '../../shared/models/entity'; const moment: typeof momentNs = momentNs; export enum PtDynamicType { Text = 'text', Boolean = 'boolean', Number = 'number', Array = 'array', Date = 'date', } export enum PtFilterType { StartsWith = 'startsWith', Contains = 'contains', } export enum PtDynamicElement { Input = 'input', Datepicker = 'datepicker', Password = 'password', Textarea = 'textarea', Slider = 'slider', SlideToggle = 'slide-toggle', Checkbox = 'checkbox', Select = 'select', FileInput = 'file-input', Autocomplete = 'autocomplete', Radio = 'radio', PhoneNumber = 'phone-number', } export interface IPtDynamicElementValidator { validator: ValidatorFn; } export const defaultDateFormat: string = 'MM/DD/YYYY'; export interface IPtDynamicElementConfig { label?: string; name: string; hint?: string; placeholder?: string; row?: number; // tslint:disable-next-line:no-any type: PtDynamicType | PtDynamicElement | Type; required?: boolean; disabled?: boolean; hidden?: boolean; readonly?: boolean; showClear?: boolean; // tslint:disable-next-line:no-any min?: any; // tslint:disable-next-line:no-any max?: any; appearance?: string; referenceData?: string; // tslint:disable-next-line:no-any minLength?: any; // tslint:disable-next-line:no-any maxLength?: any; // tslint:disable-next-line:no-any selections?: string[] | { value: any; label: string; displayText: string }[] | BehaviorSubject; // tslint:disable-next-line:no-any concatToSelection?: any; filter?: boolean; filterType?: PtFilterType; matchFunction?: Function; loading?: BehaviorSubject; multiple?: boolean; multiselectAll?: boolean; // tslint:disable-next-line:no-any default?: any; flex?: number; dateFormat?: string; showLabel?: boolean; showLabelAbove?: boolean; displayWith?: Function; lookupKey?: string; validators?: IPtDynamicElementValidator[]; } export const DYNAMIC_ELEMENT_NAME_REGEX: RegExp = /^[^0-9][^\@]*$/; @Injectable({ providedIn: 'root', }) export class PtDynamicFormsService { readonly customValidators: { [validatorName: string]: ValidatorFn } = { phone: phoneValidator, zipCode: zipCodeValidator, email: Validators.email, ssn: ssnValidator, }; /** * Method to validate if the [name] is a proper element name. * Throws error if name is not valid. */ validateDynamicElementName(name: string): void { if (!DYNAMIC_ELEMENT_NAME_REGEX.test(name)) { throw new Error(`Dynamic element name: '${name} is not valid.`); } } /** * Gets component to be rendered depending on [PtDynamicElement | PtDynamicType] * Throws error if it does not exists or not supported. */ // tslint:disable-next-line:no-any getDynamicElement(element: PtDynamicElement | PtDynamicType | Type, readonly: boolean = false): any { if (readonly) { // tslint:disable-next-line:triple-equals if (element == PtDynamicElement.Textarea) { return PtDynamicTextareaComponent; } // tslint:disable-next-line:triple-equals if (element == PtDynamicElement.Autocomplete) { return PtDynamicAutocompleteComponent; } // tslint:disable-next-line:triple-equals if (element == PtDynamicElement.Select) { return PtDynamicSelectComponent; } return PtDynamicInputComponent; } switch (element) { case PtDynamicType.Text: case PtDynamicType.Number: case PtDynamicElement.Input: case PtDynamicElement.Password: case PtDynamicElement.PhoneNumber: return PtDynamicInputComponent; case PtDynamicElement.Textarea: return PtDynamicTextareaComponent; case PtDynamicType.Boolean: case PtDynamicElement.SlideToggle: return PtDynamicSlideToggleComponent; case PtDynamicElement.Checkbox: return PtDynamicCheckboxComponent; case PtDynamicElement.Slider: return PtDynamicSliderComponent; case PtDynamicType.Array: case PtDynamicElement.Select: return PtDynamicSelectComponent; case PtDynamicElement.Autocomplete: return PtDynamicAutocompleteComponent; case PtDynamicElement.FileInput: return PtDynamicFileInputComponent; case PtDynamicElement.Datepicker: case PtDynamicType.Date: return PtDynamicDatepickerComponent; case PtDynamicElement.Radio: return PtDynamicRadioButtonComponent; default: throw new Error(`Error: type ${element} does not exist or not supported.`); } } /** * Creates form control for element depending [IPtDynamicElementConfig] properties. */ createFormControl(config: IPtDynamicElementConfig): FormControl { const validator: ValidatorFn = this.createValidators(config); return new FormControl({ value: config.default, disabled: config.disabled }, validator); } /** * Creates form validationdepending [IPtDynamicElementConfig] properties. */ createValidators(config: IPtDynamicElementConfig): ValidatorFn { let validator: ValidatorFn; if (config.required) { validator = Validators.required; } if (config.max || config.max === 0) { validator = Validators.compose([validator, Validators.max(parseFloat(config.max))]); } if (config.min || config.min === 0) { validator = Validators.compose([validator, Validators.min(parseFloat(config.min))]); } if (config.maxLength || config.maxLength === 0) { validator = Validators.compose([validator, Validators.maxLength(parseFloat(config.maxLength))]); } if (config.minLength || config.minLength === 0) { validator = Validators.compose([validator, Validators.minLength(parseFloat(config.minLength))]); } // Add provided custom validators to the validator function if (config.validators) { config.validators.forEach((validatorConfig: IPtDynamicElementValidator) => { validator = Validators.compose([validator, validatorConfig.validator]); }); } return validator; } getReferenceDataValue(referenceData: ReferenceData[], value: { correlation: string } | string): ReferenceData { if (!value) { return undefined; } return find(referenceData, (rd: ReferenceData) => { const referenceDataCorrelationId: string = get(rd, 'correlationId.correlation'); const correlation: string = get(value, 'correlation'); if (!!correlation && !!referenceDataCorrelationId) { return isEqual(correlation, referenceDataCorrelationId); } return false; }); } // tslint:disable-next-line:no-any mapFormData(entity: Entity, elements: IPtDynamicElementConfig[], referenceData: { [type: string]: ReferenceData[] }): any { return flatMap(elements, (element: IPtDynamicElementConfig) => this.buildElement(element, entity, referenceData)); } private isSelectionElement(element: IPtDynamicElementConfig): boolean { return element.type === PtDynamicElement.Autocomplete || element.type === PtDynamicElement.Select; } private isDatePicker(element: IPtDynamicElementConfig): boolean { return element.type === PtDynamicElement.Datepicker; } private buildNonSelectElement(element: IPtDynamicElementConfig, entity: Entity): IPtDynamicElementConfig { const initialValue: string = get(entity, element.name); return { ...element, default: isNil(initialValue) ? element.default : initialValue, } as IPtDynamicElementConfig; } private buildDatePickerElement(element: IPtDynamicElementConfig, entity: Entity): IPtDynamicElementConfig { const initialValue: string = get(entity, element.name); const value: string = isNil(initialValue) ? element.default : initialValue; return { ...element, default: isNil(value) ? value : moment(value).toDate(), } as IPtDynamicElementConfig; } private selections(referenceDataType: string, referenceData: { [type: string]: ReferenceData[] }, // tslint:disable-next-line:no-any concatToSelection: any): BehaviorSubject { const subject: BehaviorSubject = new BehaviorSubject(undefined); if (!!referenceData) { if (!!concatToSelection) { subject.next(uniq(concat(isArray(concatToSelection) ? concatToSelection : [concatToSelection], get(referenceData, referenceDataType)))); } else { subject.next(get(referenceData, referenceDataType)); } } return subject; } private buildSelectionElement(element: IPtDynamicElementConfig, entity: Entity, referenceData: { [type: string]: ReferenceData[] }): IPtDynamicElementConfig { const selectionsSubject: BehaviorSubject = this.selections(element.referenceData, referenceData, element.concatToSelection); // tslint:disable-next-line:no-any const formValue: any = get(entity, element.name); // tslint:disable-next-line:no-any const initialValue: any = get(formValue, 'correlation') ? this.getReferenceDataValue(get(element, 'selections.value') || get(referenceData, element.referenceData), formValue) : formValue; return { ...element, selections: element.selections ? element.selections : selectionsSubject, default: isNil(initialValue) ? element.default : initialValue, validators: element.type === PtDynamicElement.Autocomplete ? [{ validator: autocompleteValidator(selectionsSubject) }] : undefined, } as IPtDynamicElementConfig; } private buildElement(element: IPtDynamicElementConfig, entity: Entity, referenceData: { [type: string]: ReferenceData[] }): IPtDynamicElementConfig { if (this.isSelectionElement(element)) { return this.buildSelectionElement(element, entity, referenceData); } if (this.isDatePicker(element)) { return this.buildDatePickerElement(element, entity); } return this.buildNonSelectElement(element, entity); } }