ng-dynamic-json-form
Version:
Generate Angular reactive form dynamically using JSON. Support conditional rendering and toggle of validators.
1 lines • 396 kB
Source Map (JSON)
{"version":3,"file":"ng-dynamic-json-form.mjs","sources":["../../../lib/core/utilities/get-control-errors.ts","../../../lib/core/components/custom-control/custom-control.component.ts","../../../lib/core/components/custom-error-message/custom-error-message.abstract.ts","../../../lib/core/components/custom-form-label/custom-form-label.abstract.ts","../../../lib/core/utilities/get-class-list-from-string.ts","../../../lib/core/utilities/get-style-list-from-string.ts","../../../lib/core/directives/control-layout.directive.ts","../../../lib/core/directives/host-id.directive.ts","../../../lib/core/directives/imask-value-patch.directive.ts","../../../lib/core/providers/props-binding.provider.ts","../../../lib/core/directives/props-binding.directive.ts","../../../lib/core/directives/textarea-auto-height.directive.ts","../../../lib/core/models/validators.enum.ts","../../../lib/core/models/conditions-action.enum.ts","../../../lib/core/pipes/control-type-by-config.pipe.ts","../../../lib/core/services/global-variable.service.ts","../../../lib/core/pipes/is-control-required.pipe.ts","../../../lib/core/services/host-event.service.ts","../../../lib/core/services/form-validation.service.ts","../../../lib/core/components/error-message/error-message.component.ts","../../../lib/core/components/error-message/error-message.component.html","../../../lib/core/components/form-label/form-label.component.ts","../../../lib/core/components/form-label/form-label.component.html","../../../lib/core/components/content-wrapper/content-wrapper.component.ts","../../../lib/core/components/content-wrapper/content-wrapper.component.html","../../../lib/core/services/config-mapping.service.ts","../../../lib/core/utilities/get-boolean-operation-result.ts","../../../lib/core/utilities/get-value-in-object.ts","../../../lib/core/utilities/schema-validator.js","../../../lib/core/services/config-validation.service.ts","../../../lib/core/utilities/evaluate-conditions-statements.ts","../../../lib/core/utilities/get-control-and-value-path.ts","../../../lib/core/services/form-conditions.service.ts","../../../lib/core/services/form-generator.service.ts","../../../lib/core/services/form-ready-state.service.ts","../../../lib/core/services/form-value.service.ts","../../../lib/core/services/http-request-cache.service.ts","../../../lib/core/utilities/trim-object-by-keys.ts","../../../lib/core/services/options-data.service.ts","../../../lib/core/ui-basic/ui-basic-checkbox/ui-basic-checkbox.component.ts","../../../lib/core/ui-basic/ui-basic-checkbox/ui-basic-checkbox.component.html","../../../lib/core/ui-basic/ui-basic-date/ui-basic-date.component.ts","../../../lib/core/ui-basic/ui-basic-date/ui-basic-date.component.html","../../../lib/core/ui-basic/ui-basic-input-mask/ui-basic-input-mask.component.ts","../../../lib/core/ui-basic/ui-basic-input-mask/ui-basic-input-mask.component.html","../../../lib/core/ui-basic/ui-basic-input/ui-basic-input.component.ts","../../../lib/core/ui-basic/ui-basic-input/ui-basic-input.component.html","../../../lib/core/ui-basic/ui-basic-radio/ui-basic-radio.component.ts","../../../lib/core/ui-basic/ui-basic-radio/ui-basic-radio.component.html","../../../lib/core/ui-basic/ui-basic-range/ui-basic-range.component.ts","../../../lib/core/ui-basic/ui-basic-range/ui-basic-range.component.html","../../../lib/core/ui-basic/ui-basic-select/ui-basic-select.component.ts","../../../lib/core/ui-basic/ui-basic-select/ui-basic-select.component.html","../../../lib/core/ui-basic/ui-basic-switch/ui-basic-switch.component.ts","../../../lib/core/ui-basic/ui-basic-switch/ui-basic-switch.component.html","../../../lib/core/ui-basic/ui-basic-textarea/ui-basic-textarea.component.ts","../../../lib/core/ui-basic/ui-basic-textarea/ui-basic-textarea.component.html","../../../lib/core/ui-basic/ui-basic-components.constant.ts","../../../lib/core/components/form-control/form-control.component.ts","../../../lib/core/components/form-control/form-control.component.html","../../../lib/core/components/form-group/form-group.component.ts","../../../lib/core/components/form-group/form-group.component.html","../../../lib/core/providers/ng-dynamic-json-form.provider.ts","../../../lib/core/ng-dynamic-json-form.component.ts","../../../lib/core/ng-dynamic-json-form.component.html","../../../lib/public-api.ts","../../../lib/ng-dynamic-json-form.ts"],"sourcesContent":["import {\r\n AbstractControl,\r\n ValidationErrors,\r\n isFormControl,\r\n isFormGroup,\r\n} from '@angular/forms';\r\n\r\n/**Get all the errors in the AbstractControl recursively\r\n * @example\r\n * root: {\r\n * control1: ValidationErrors,\r\n * control2: {\r\n * childA: ValidationErrors\r\n * }\r\n * }\r\n */\r\nexport function getControlErrors(\r\n control: AbstractControl | undefined\r\n): ValidationErrors | null {\r\n if (!control) {\r\n return null;\r\n }\r\n\r\n if (isFormControl(control)) {\r\n return control.errors;\r\n }\r\n\r\n if (isFormGroup(control)) {\r\n const result = Object.keys(control.controls).reduce((acc, key) => {\r\n const childControl = control.controls[key];\r\n const err = getControlErrors(childControl);\r\n\r\n return err ? { ...acc, [key]: err } : acc;\r\n }, {});\r\n\r\n return !Object.keys(result).length ? null : result;\r\n }\r\n\r\n return null;\r\n}\r\n","import { Component, signal } from '@angular/core';\r\nimport {\r\n AbstractControl,\r\n ControlValueAccessor,\r\n UntypedFormGroup,\r\n ValidationErrors,\r\n Validator,\r\n} from '@angular/forms';\r\nimport { FormControlConfig, OptionItem } from '../../models';\r\nimport { getControlErrors } from '../../utilities/get-control-errors';\r\n\r\n@Component({\r\n selector: 'custom-control',\r\n template: ``,\r\n standalone: true,\r\n})\r\nexport class CustomControlComponent implements ControlValueAccessor, Validator {\r\n /**\r\n * This control can be use to bind the UI of the custom component with the parent control.\r\n * Use this if you don't want to rewrite/override every methods in the CVA manually.\r\n *\r\n * Assign it with instance of `AbstractControl`\r\n *\r\n * ```\r\n * // FormControl\r\n * override control = new FormControl('');\r\n *\r\n * // FormGroup\r\n * override control = new FormGroup({\r\n * controlA: new FormControl(...),\r\n * controlB: new FormControl(...)\r\n * });\r\n * ```\r\n */\r\n control?: AbstractControl;\r\n\r\n hostForm = signal<UntypedFormGroup | undefined>(undefined);\r\n data = signal<FormControlConfig | undefined>(undefined);\r\n hideErrorMessage = signal<boolean | undefined>(undefined);\r\n\r\n writeValue(obj: any): void {\r\n this.control?.patchValue(obj);\r\n }\r\n\r\n registerOnChange(fn: any): void {}\r\n\r\n registerOnTouched(fn: any): void {}\r\n\r\n setDisabledState(isDisabled: boolean): void {\r\n isDisabled ? this.control?.disable() : this.control?.enable();\r\n }\r\n\r\n validate(control: AbstractControl<any, any>): ValidationErrors | null {\r\n return getControlErrors(this.control);\r\n }\r\n\r\n markAsDirty(): void {}\r\n\r\n markAsPristine(): void {}\r\n\r\n markAsTouched(): void {}\r\n\r\n markAsUntouched(): void {}\r\n\r\n setErrors(errors: ValidationErrors | null): void {}\r\n\r\n onOptionsGet(options: OptionItem[]): void {\r\n const data = this.data();\r\n\r\n if (!data || !data.options) {\r\n return;\r\n }\r\n\r\n this.data.update((x) => {\r\n if (!x?.options) {\r\n return x;\r\n }\r\n\r\n x.options.data = [...options];\r\n\r\n return { ...x };\r\n });\r\n }\r\n}\r\n","import { signal } from '@angular/core';\r\nimport { AbstractControl, FormControl } from '@angular/forms';\r\n\r\nexport abstract class CustomErrorMessage {\r\n control: AbstractControl = new FormControl();\r\n errorMessages = signal<string[]>([]);\r\n}\r\n","import { signal } from '@angular/core';\r\nimport { FormLayout } from '../../models/form-layout.interface';\r\n\r\nexport abstract class CustomFormLabel {\r\n collapsible = signal<boolean>(false);\r\n expand = signal<boolean>(false);\r\n label = signal<string | undefined>(undefined);\r\n layout = signal<FormLayout | undefined>(undefined);\r\n props = signal<any | undefined>(undefined);\r\n}\r\n","export function getClassListFromString(\r\n classNames: string | undefined\r\n): string[] {\r\n if (!classNames) {\r\n return [];\r\n }\r\n\r\n try {\r\n return classNames\r\n .split(/\\s{1,}/)\r\n .map((x) => x.trim())\r\n .filter(Boolean);\r\n } catch {\r\n return [];\r\n }\r\n}\r\n","export function getStyleListFromString(styles: string | undefined): string[] {\r\n if (!styles) {\r\n return [];\r\n }\r\n\r\n try {\r\n return styles\r\n .split(';')\r\n .map((x) => x.trim())\r\n .filter(Boolean);\r\n } catch {\r\n return [];\r\n }\r\n}\r\n","import {\r\n Directive,\r\n ElementRef,\r\n computed,\r\n effect,\r\n inject,\r\n input,\r\n} from '@angular/core';\r\nimport { FormControlConfig } from '../models';\r\nimport { getClassListFromString } from '../utilities/get-class-list-from-string';\r\nimport { getStyleListFromString } from '../utilities/get-style-list-from-string';\r\n\r\n@Directive({\r\n selector: '[controlLayout]',\r\n standalone: true,\r\n})\r\nexport class ControlLayoutDirective {\r\n private el = inject(ElementRef);\r\n\r\n controlLayout = input<{\r\n type?:\r\n | 'host'\r\n | 'label'\r\n | 'content'\r\n | 'formGroup'\r\n | 'description'\r\n | 'inputArea'\r\n | 'error';\r\n layout?: FormControlConfig['layout'];\r\n }>();\r\n\r\n classNames = computed(() => {\r\n const data = this.controlLayout();\r\n if (!data) {\r\n return [];\r\n }\r\n\r\n const { type, layout } = data;\r\n const classString = layout?.[`${type ?? 'host'}Class`] ?? '';\r\n const result = getClassListFromString(classString);\r\n\r\n return result;\r\n });\r\n\r\n styleList = computed(() => {\r\n const data = this.controlLayout();\r\n if (!data) {\r\n return [];\r\n }\r\n\r\n const { type, layout } = data;\r\n const styleString = layout?.[`${type ?? 'host'}Styles`] ?? '';\r\n const result = getStyleListFromString(styleString);\r\n\r\n return result;\r\n });\r\n\r\n updateClass = effect(() => {\r\n const host = this.el.nativeElement as HTMLElement;\r\n const classNames = this.classNames();\r\n\r\n if (!classNames.length) {\r\n return;\r\n }\r\n\r\n host.classList.add(...classNames);\r\n });\r\n\r\n updateStyles = effect(() => {\r\n const host = this.el.nativeElement as HTMLElement;\r\n const styleList = this.styleList();\r\n\r\n if (!styleList.length) {\r\n return;\r\n }\r\n\r\n for (const item of styleList) {\r\n const [name, value] = item.split(':').map((x) => x.trim());\r\n host.style.setProperty(name, value);\r\n }\r\n });\r\n}\r\n","import {\r\n Directive,\r\n ElementRef,\r\n computed,\r\n effect,\r\n inject,\r\n input,\r\n} from '@angular/core';\r\n\r\n@Directive({\r\n selector: '[hostId]',\r\n standalone: true,\r\n})\r\nexport class HostIdDirective {\r\n private el = inject(ElementRef);\r\n\r\n hostId = input<{ parentId?: string; controlName?: string }>();\r\n\r\n computedId = computed(() => {\r\n const hostId = this.hostId();\r\n\r\n if (!hostId) {\r\n return;\r\n }\r\n\r\n const { parentId, controlName } = hostId;\r\n return parentId ? `${parentId}.${controlName}` : controlName;\r\n });\r\n\r\n updateAttribute = effect(() => {\r\n const id = this.computedId();\r\n const host = this.el.nativeElement as HTMLElement;\r\n\r\n if (id) {\r\n host.setAttribute('id', id);\r\n }\r\n });\r\n}\r\n","import { Directive, inject } from '@angular/core';\r\nimport { IMaskDirective } from 'angular-imask';\r\n\r\n@Directive({\r\n selector: '[imaskValuePatch]',\r\n standalone: true,\r\n})\r\nexport class ImaskValuePatchDirective {\r\n private imask = inject(IMaskDirective);\r\n private isNumber = false;\r\n\r\n constructor() {\r\n const iMask = this.imask;\r\n\r\n iMask.writeValue = (value: string | number) => {\r\n // ----- Modified area -----\r\n if (!this.isNumber) {\r\n this.isNumber = typeof value === 'number';\r\n }\r\n\r\n value = value == null && iMask.unmask !== 'typed' ? '' : `${value}`;\r\n // ----- Modified area -----\r\n\r\n if (iMask.maskRef) {\r\n iMask.beginWrite(value);\r\n iMask.maskValue = value;\r\n iMask.endWrite();\r\n } else {\r\n iMask['_renderer'].setProperty(iMask.element, 'value', value);\r\n iMask['_initialValue'] = value;\r\n }\r\n };\r\n\r\n iMask['_onAccept'] = () => {\r\n // ----- Modified area -----\r\n const valueParsed = this.isNumber\r\n ? parseFloat(iMask.maskValue)\r\n : iMask.maskValue;\r\n\r\n const value = isNaN(valueParsed) ? null : valueParsed;\r\n // ----- Modified area -----\r\n\r\n // if value was not changed during writing don't fire events\r\n // for details see https://github.com/uNmAnNeR/imaskjs/issues/136\r\n if (iMask['_writing'] && value === iMask.endWrite()) return;\r\n iMask.onChange(value);\r\n iMask.accept.emit(value);\r\n };\r\n }\r\n}\r\n","import { InjectionToken, Provider, ProviderToken } from '@angular/core';\r\n\r\ninterface PropsBindingItem {\r\n key: string;\r\n token: ProviderToken<any>;\r\n}\r\n\r\nexport const PROPS_BINDING_INJECTORS = new InjectionToken<PropsBindingItem[]>(\r\n 'property-binding-injector'\r\n);\r\n\r\nexport function providePropsBinding(value: PropsBindingItem[]): Provider {\r\n return {\r\n provide: PROPS_BINDING_INJECTORS,\r\n useValue: value,\r\n };\r\n}\r\n","import {\r\n ChangeDetectorRef,\r\n Directive,\r\n ElementRef,\r\n Injector,\r\n SimpleChange,\r\n WritableSignal,\r\n computed,\r\n effect,\r\n inject,\r\n input,\r\n isSignal,\r\n} from '@angular/core';\r\nimport { PROPS_BINDING_INJECTORS } from '../providers/props-binding.provider';\r\n@Directive({\r\n selector: '[propsBinding]',\r\n standalone: true,\r\n})\r\nexport class PropsBindingDirective {\r\n private injectionTokens = inject(PROPS_BINDING_INJECTORS, {\r\n optional: true,\r\n });\r\n private injector = inject(Injector);\r\n private cd = inject(ChangeDetectorRef);\r\n private el = inject(ElementRef);\r\n\r\n propsBinding = input<{ props: any; key?: string; omit?: string[] }[]>([]);\r\n\r\n validPropsData = computed(() =>\r\n this.propsBinding().filter(\r\n (x) =>\r\n Boolean(x) &&\r\n typeof x.props === 'object' &&\r\n Object.keys(x.props).length > 0,\r\n ),\r\n );\r\n\r\n handlePropsGet = effect(() => {\r\n const propsData = this.validPropsData();\r\n\r\n if (!propsData.length) {\r\n return;\r\n }\r\n\r\n const hostEl = this.el.nativeElement as HTMLElement | undefined;\r\n\r\n for (const item of propsData) {\r\n const { props, key, omit = [] } = item;\r\n const providerToken = this.injectionTokens?.find(\r\n (x) => x.key === key,\r\n )?.token;\r\n\r\n const component = !providerToken\r\n ? null\r\n : this.injector.get(providerToken);\r\n\r\n for (const key in props) {\r\n const value = props[key];\r\n\r\n if (value == undefined || omit.includes(key)) {\r\n continue;\r\n }\r\n\r\n if (component) {\r\n this.updateComponentProperty({ component, key, value });\r\n }\r\n\r\n if (hostEl) {\r\n // Only set CSS custom properties (starting with --) or valid HTML attributes\r\n if (key.startsWith('--')) {\r\n hostEl.style.setProperty(key, value);\r\n } else if (this.isValidHtmlAttribute(key)) {\r\n hostEl.setAttribute(key, value);\r\n }\r\n }\r\n }\r\n }\r\n\r\n this.cd.markForCheck();\r\n this.cd.detectChanges();\r\n });\r\n\r\n private updateComponentProperty(data: {\r\n component: any;\r\n key: any;\r\n value: any;\r\n }): void {\r\n const { component, key, value } = data;\r\n const hasProperty = this.hasProperty(component, key);\r\n\r\n if (!hasProperty) {\r\n return;\r\n }\r\n\r\n const property = component[key];\r\n\r\n if (isSignal(property)) {\r\n if (typeof (property as any).set === 'function') {\r\n (property as WritableSignal<any>).set(value);\r\n }\r\n } else {\r\n component[key] = value;\r\n }\r\n\r\n // For compatibility\r\n if (component['ngOnChanges']) {\r\n const simpleChange = new SimpleChange(undefined, value, true);\r\n component.ngOnChanges({ [key]: simpleChange });\r\n }\r\n }\r\n\r\n private hasProperty(obj: any, key: string): boolean {\r\n return Object.hasOwn(obj, key) || key in obj;\r\n }\r\n\r\n private isValidHtmlAttribute(attributeName: string): boolean {\r\n // Common HTML attributes - this is not exhaustive but covers most use cases\r\n const validAttributes = new Set([\r\n 'id',\r\n 'class',\r\n 'style',\r\n 'title',\r\n 'lang',\r\n 'dir',\r\n 'hidden',\r\n 'tabindex',\r\n 'accesskey',\r\n 'contenteditable',\r\n 'draggable',\r\n 'spellcheck',\r\n 'translate',\r\n 'role',\r\n 'aria-label',\r\n 'aria-labelledby',\r\n 'aria-describedby',\r\n 'aria-hidden',\r\n 'aria-expanded',\r\n 'aria-selected',\r\n 'aria-checked',\r\n 'aria-disabled',\r\n 'data-testid',\r\n 'disabled',\r\n 'readonly',\r\n 'required',\r\n 'placeholder',\r\n 'value',\r\n 'checked',\r\n 'selected',\r\n 'multiple',\r\n 'size',\r\n 'rows',\r\n 'cols',\r\n 'min',\r\n 'max',\r\n 'step',\r\n 'pattern',\r\n 'minlength',\r\n 'maxlength',\r\n 'src',\r\n 'alt',\r\n 'href',\r\n 'target',\r\n 'rel',\r\n 'type',\r\n 'name',\r\n 'for',\r\n ]);\r\n\r\n // Allow data-* and aria-* attributes\r\n return (\r\n validAttributes.has(attributeName.toLowerCase()) ||\r\n attributeName.startsWith('data-') ||\r\n attributeName.startsWith('aria-')\r\n );\r\n }\r\n}\r\n","import {\r\n Directive,\r\n ElementRef,\r\n HostListener,\r\n inject,\r\n input,\r\n OnInit,\r\n} from '@angular/core';\r\n\r\n@Directive({\r\n selector: '[textareaAutoHeight]',\r\n standalone: true,\r\n})\r\nexport class TextareaAutHeightDirective implements OnInit {\r\n private el = inject(ElementRef);\r\n\r\n autoResize = input<boolean>(true);\r\n\r\n @HostListener('input', ['$event'])\r\n onInput(): void {\r\n this.setHeight();\r\n }\r\n\r\n ngOnInit(): void {\r\n this.removeResizeProperty();\r\n }\r\n\r\n private removeResizeProperty(): void {\r\n const hostEl = this.el.nativeElement as HTMLElement;\r\n hostEl.style.setProperty('resize', 'none');\r\n }\r\n\r\n private setHeight(): void {\r\n const autoResize = this.autoResize();\r\n const hostEl = this.el.nativeElement as HTMLElement;\r\n\r\n if (!hostEl || !autoResize) {\r\n return;\r\n }\r\n\r\n const borderWidth = Math.ceil(\r\n parseFloat(window.getComputedStyle(hostEl).borderWidth),\r\n );\r\n\r\n hostEl.style.removeProperty('height');\r\n hostEl.style.setProperty(\r\n 'height',\r\n `${hostEl.scrollHeight + borderWidth * 2}px`,\r\n );\r\n }\r\n}\r\n","export enum ValidatorsEnum {\r\n required = 'required',\r\n requiredTrue = 'requiredTrue',\r\n min = 'min',\r\n max = 'max',\r\n minLength = 'minLength',\r\n maxLength = 'maxLength',\r\n email = 'email',\r\n pattern = 'pattern',\r\n}\r\n","import { ValidatorsEnum } from './validators.enum';\r\n\r\nexport enum ConditionsActionEnum {\r\n 'control.hidden' = 'control.hidden',\r\n 'control.disabled' = 'control.disabled',\r\n 'validator.required' = `validator.${ValidatorsEnum.required}`,\r\n 'validator.requiredTrue' = `validator.${ValidatorsEnum.requiredTrue}`,\r\n 'validator.min' = `validator.${ValidatorsEnum.min}`,\r\n 'validator.max' = `validator.${ValidatorsEnum.max}`,\r\n 'validator.minLength' = `validator.${ValidatorsEnum.minLength}`,\r\n 'validator.maxLength' = `validator.${ValidatorsEnum.maxLength}`,\r\n 'validator.email' = `validator.${ValidatorsEnum.email}`,\r\n 'validator.pattern' = `validator.${ValidatorsEnum.pattern}`,\r\n}\r\n","import { Pipe, PipeTransform } from '@angular/core';\r\nimport { FormControlConfig } from '../models';\r\n\r\n@Pipe({\r\n name: 'controlTypeByConfig',\r\n standalone: true,\r\n})\r\nexport class ControlTypeByConfigPipe implements PipeTransform {\r\n transform(config: FormControlConfig): 'FormControl' | 'FormGroup' {\r\n if (!config.children) {\r\n return 'FormControl';\r\n }\r\n\r\n return 'FormGroup';\r\n }\r\n}\r\n","import { Injectable, TemplateRef, Type } from '@angular/core';\r\nimport { UntypedFormGroup } from '@angular/forms';\r\nimport { CustomErrorMessage } from '../components/custom-error-message/custom-error-message.abstract';\r\nimport { CustomFormLabel } from '../components/custom-form-label/custom-form-label.abstract';\r\nimport { BehaviorSubject, Observable } from 'rxjs';\r\nimport {\r\n ConditionsActionFunctions,\r\n CustomComponents,\r\n CustomErrorComponents,\r\n CustomLabelComponents,\r\n CustomTemplates,\r\n FormControlConfig,\r\n FormLayout,\r\n OptionItem,\r\n UiComponents,\r\n} from '../models';\r\nimport { CustomAsyncValidators } from '../models/custom-async-validators.type';\r\nimport { CustomValidators } from '../models/custom-validators.type';\r\nimport { FormConfig } from '../providers/ng-dynamic-json-form.provider';\r\ninterface GlobalVariables\r\n extends Omit<\r\n GlobalVariableService,\r\n 'setup' | 'rootConfigs' | 'rootForm' | 'hideErrorMessage$'\r\n > {}\r\n@Injectable()\r\nexport class GlobalVariableService {\r\n descriptionPosition?: FormLayout['descriptionPosition'];\r\n hideErrorMessage$ = new BehaviorSubject<boolean | undefined>(undefined);\r\n rootConfigs: FormControlConfig[] = [];\r\n rootForm?: UntypedFormGroup;\r\n showErrorsOnTouched = true;\r\n\r\n // =============== The variables that must be initialized ===============\r\n hostElement?: HTMLElement;\r\n conditionsActionFunctions: ConditionsActionFunctions | undefined;\r\n optionsSources:\r\n | {\r\n [key: string]: Observable<OptionItem[]>;\r\n }\r\n | undefined;\r\n uiComponents: UiComponents | undefined;\r\n customAsyncValidators: CustomAsyncValidators | undefined;\r\n customValidators: CustomValidators | undefined;\r\n customComponents: CustomComponents | undefined;\r\n customTemplates: CustomTemplates | undefined;\r\n\r\n // Custom error\r\n errorComponents: CustomErrorComponents | undefined;\r\n errorTemplates: CustomTemplates | undefined;\r\n errorTemplateDefault: TemplateRef<any> | undefined;\r\n errorComponentDefault: Type<CustomErrorMessage> | undefined;\r\n\r\n // Custom label\r\n labelComponents: CustomLabelComponents | undefined;\r\n labelTemplates: CustomTemplates | undefined;\r\n labelTemplateDefault: TemplateRef<any> | undefined;\r\n\r\n // Custom loading\r\n labelComponentDefault: Type<CustomFormLabel> | undefined;\r\n loadingComponent: Type<any> | undefined;\r\n loadingTemplate: TemplateRef<any> | undefined;\r\n\r\n // Hide error message\r\n hideErrorsForTypes: FormConfig['hideErrorsForTypes'];\r\n\r\n // Global validation messages\r\n validationMessages: FormConfig['validationMessages'];\r\n // ======================================================================\r\n\r\n setup(variables: GlobalVariables): void {\r\n for (const key in variables) {\r\n (this as any)[key] = (variables as any)[key];\r\n }\r\n }\r\n}\r\n","import { Pipe, PipeTransform } from '@angular/core';\r\nimport { AbstractControl, Validators } from '@angular/forms';\r\n\r\n@Pipe({\r\n name: 'isControlRequired',\r\n standalone: true,\r\n})\r\nexport class IsControlRequiredPipe implements PipeTransform {\r\n transform(value: AbstractControl | undefined): boolean {\r\n if (!value) {\r\n return false;\r\n }\r\n\r\n return value.hasValidator(Validators.required);\r\n }\r\n}\r\n","import { isPlatformServer } from '@angular/common';\r\nimport { inject, Injectable, PLATFORM_ID } from '@angular/core';\r\nimport { EMPTY, fromEvent, merge, Observable, Subject, tap } from 'rxjs';\r\n\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class HostEventService {\r\n private platformId = inject(PLATFORM_ID);\r\n\r\n readonly focusOut$ = new Subject<FocusEvent>();\r\n readonly keyUp$ = new Subject<KeyboardEvent>();\r\n readonly keyDown$ = new Subject<KeyboardEvent>();\r\n readonly pointerUp$ = new Subject<PointerEvent>();\r\n readonly pointerDown$ = new Subject<PointerEvent>();\r\n\r\n start$(hostElement: HTMLElement): Observable<any> {\r\n if (this.isServer) {\r\n return EMPTY;\r\n }\r\n\r\n return merge(\r\n this.event$(hostElement, 'focusout', this.focusOut$),\r\n this.event$(hostElement, 'keyup', this.keyUp$),\r\n this.event$(hostElement, 'keydown', this.keyDown$),\r\n this.event$(hostElement, 'pointerup', this.pointerUp$),\r\n this.event$(hostElement, 'pointerdown', this.pointerDown$),\r\n );\r\n }\r\n\r\n private event$(\r\n target: HTMLElement,\r\n name: string,\r\n subject: Subject<any>,\r\n ): Observable<any> {\r\n if (this.isServer) {\r\n return EMPTY;\r\n }\r\n\r\n return fromEvent(target, name, { passive: true }).pipe(\r\n tap((x) => subject.next(x)),\r\n );\r\n }\r\n\r\n private get isServer(): boolean {\r\n return isPlatformServer(this.platformId);\r\n }\r\n}\r\n","import { Injectable, inject } from '@angular/core';\r\nimport {\r\n AbstractControl,\r\n AsyncValidatorFn,\r\n ValidationErrors,\r\n ValidatorFn,\r\n Validators,\r\n} from '@angular/forms';\r\nimport { Observable, map, of, startWith } from 'rxjs';\r\nimport { CustomValidators, ValidatorConfig, ValidatorsEnum } from '../models';\r\nimport { CustomAsyncValidators } from '../models/custom-async-validators.type';\r\nimport { GlobalVariableService } from './global-variable.service';\r\n\r\nfunction emailValidator(control: AbstractControl): ValidationErrors | null {\r\n const emailValid = RegExp(/^[^@\\s!(){}<>]+@[\\w-]+(\\.[A-Za-z]+)+$/).test(\r\n control.value,\r\n );\r\n\r\n if (!control.value) {\r\n return null;\r\n }\r\n\r\n return emailValid ? null : { email: 'Invalid email format' };\r\n}\r\n\r\nconst builtInValidators = (\r\n value?: any,\r\n): {\r\n [key in ValidatorsEnum]?: ValidatorFn;\r\n} =>\r\n ({\r\n [ValidatorsEnum.required]: Validators.required,\r\n [ValidatorsEnum.requiredTrue]: Validators.requiredTrue,\r\n [ValidatorsEnum.email]: emailValidator,\r\n [ValidatorsEnum.pattern]: Validators.pattern(value),\r\n [ValidatorsEnum.min]: Validators.min(value),\r\n [ValidatorsEnum.max]: Validators.max(value),\r\n [ValidatorsEnum.minLength]: Validators.minLength(value),\r\n [ValidatorsEnum.maxLength]: Validators.maxLength(value),\r\n }) as const;\r\n\r\n@Injectable()\r\nexport class FormValidationService {\r\n private globalVariableService = inject(GlobalVariableService);\r\n\r\n getErrorMessages$(\r\n control: AbstractControl | null | undefined,\r\n validators?: ValidatorConfig[],\r\n ): Observable<string[]> {\r\n if (!control || !validators?.length) {\r\n return of([]);\r\n }\r\n\r\n return control.statusChanges.pipe(\r\n startWith(control.status),\r\n map(() =>\r\n this.getErrorMessages(control.errors, control.value, validators),\r\n ),\r\n );\r\n }\r\n\r\n getValidators(input: ValidatorConfig[] | undefined): ValidatorFn[] {\r\n if (!input || !input.length) {\r\n return [];\r\n }\r\n\r\n // Remove duplicates\r\n const filteredConfigs = [\r\n ...new Map(input.map((v) => [v.name, v])).values(),\r\n ];\r\n\r\n const customValidators = this.globalVariableService.customValidators;\r\n const validatorFns = filteredConfigs.map((item) => {\r\n const { name } = item;\r\n const value = this.getValidatorValue(item);\r\n const builtInValidator = builtInValidators(value)[name as ValidatorsEnum];\r\n const customValidator = this.getValidatorFn(\r\n item,\r\n customValidators?.[name],\r\n ) as ValidatorFn | null;\r\n\r\n const result = customValidator ?? builtInValidator;\r\n\r\n return result;\r\n });\r\n\r\n return validatorFns.filter(Boolean) as ValidatorFn[];\r\n }\r\n\r\n getAsyncValidators(input: ValidatorConfig[] | undefined): AsyncValidatorFn[] {\r\n if (!input || !input.length) {\r\n return [];\r\n }\r\n\r\n // Remove duplicates\r\n const filteredConfigs = [\r\n ...new Map(input.map((v) => [v.name, v])).values(),\r\n ];\r\n\r\n const customAsyncValidators =\r\n this.globalVariableService.customAsyncValidators;\r\n\r\n const validatorFns = filteredConfigs.map((item) => {\r\n const validatorFn = customAsyncValidators?.[item.name];\r\n return this.getValidatorFn(item, validatorFn) as AsyncValidatorFn | null;\r\n });\r\n\r\n return validatorFns.filter(Boolean) as AsyncValidatorFn[];\r\n }\r\n\r\n /**Get the error messages of the control\r\n *\r\n * @description\r\n * Try to get the custom error message specified in the config first,\r\n * else use the error message in the `ValidationErrors`.\r\n *\r\n * When using custom validator, the custom message most likely will not working,\r\n * it's because we are using the key in the errors to find the config message.\r\n * Since user can define the error object, it becomes very difficult to match the config name\r\n * with the keys in the error object.\r\n */\r\n private getErrorMessages(\r\n controlErrors: ValidationErrors | null,\r\n controlValue: any,\r\n validatorConfigs: ValidatorConfig[],\r\n ): string[] {\r\n if (!controlErrors) {\r\n return [];\r\n }\r\n\r\n const errorMessage = (error: any) => {\r\n return typeof error === 'string' ? error : JSON.stringify(error);\r\n };\r\n\r\n return Object.keys(controlErrors).reduce((acc, key) => {\r\n const error = controlErrors[key];\r\n const config = this.getConfigFromErrorKey(\r\n { [key]: error },\r\n validatorConfigs,\r\n );\r\n\r\n const configMessage = config?.message;\r\n const defaultMessage =\r\n this.globalVariableService.validationMessages?.[config?.name ?? ''];\r\n\r\n const customMessage = (configMessage || defaultMessage)\r\n ?.replace(/{{value}}/g, controlValue || '')\r\n .replace(/{{validatorValue}}/g, config?.value);\r\n\r\n acc.push(customMessage || errorMessage(error));\r\n\r\n return acc;\r\n }, [] as string[]);\r\n }\r\n\r\n private getConfigFromErrorKey(\r\n error: { [key: string]: any },\r\n configs: ValidatorConfig[],\r\n ): ValidatorConfig | undefined {\r\n // The key mapping of the `ValidationErrors` with the `ValidatorConfig`,\r\n // to let us get the correct message by using `name` of `ValidatorConfig`.\r\n const valueKeyMapping: { [key in ValidatorsEnum]?: string } = {\r\n pattern: 'requiredPattern',\r\n min: 'min',\r\n max: 'max',\r\n minLength: 'requiredLength',\r\n maxLength: 'requiredLength',\r\n };\r\n\r\n const getValidatorValue = (v: any) => {\r\n return typeof v !== 'number' && !isNaN(v) ? parseFloat(v) : v;\r\n };\r\n\r\n const [errorKey, errorValue] = Object.entries(error)[0];\r\n\r\n const result = configs.find((item) => {\r\n const { name, value } = item;\r\n\r\n if (errorKey !== name.toLowerCase()) {\r\n return false;\r\n }\r\n\r\n if (value === undefined || value === null || value === '') {\r\n return true;\r\n }\r\n\r\n const targetKey = valueKeyMapping[name as ValidatorsEnum] ?? '';\r\n const requiredValue = errorValue[targetKey];\r\n const validatorValue = getValidatorValue(value);\r\n\r\n return requiredValue && name === 'pattern'\r\n ? requiredValue.includes(validatorValue)\r\n : requiredValue === validatorValue;\r\n });\r\n\r\n return result;\r\n }\r\n\r\n private getValidatorValue(validatorConfig: ValidatorConfig) {\r\n const { name, value, flags } = validatorConfig;\r\n\r\n switch (name) {\r\n case ValidatorsEnum.pattern:\r\n return value instanceof RegExp ? value : new RegExp(value, flags);\r\n\r\n case ValidatorsEnum.min:\r\n case ValidatorsEnum.max:\r\n case ValidatorsEnum.minLength:\r\n case ValidatorsEnum.maxLength:\r\n try {\r\n return typeof value !== 'number' ? parseFloat(value) : value;\r\n } catch {\r\n break;\r\n }\r\n\r\n default:\r\n return value;\r\n }\r\n }\r\n\r\n /**\r\n * Get validatorFn from either validatorFn or factory function that return a validatorFn.\r\n * If it's a factory function, return the validatorFn instead.\r\n *\r\n * @param validatorConfig\r\n * @param validatorFn\r\n */\r\n private getValidatorFn(\r\n validatorConfig: ValidatorConfig,\r\n validatorFn:\r\n | CustomValidators[string]\r\n | CustomAsyncValidators[string]\r\n | undefined,\r\n ): ValidatorFn | AsyncValidatorFn | null {\r\n const { value } = validatorConfig;\r\n\r\n if (!validatorFn) {\r\n return null;\r\n }\r\n\r\n const result =\r\n typeof validatorFn({} as any) !== 'function'\r\n ? validatorFn\r\n : validatorFn(value);\r\n\r\n return result as ValidatorFn | AsyncValidatorFn;\r\n }\r\n}\r\n","import { CommonModule } from '@angular/common';\r\nimport {\r\n Component,\r\n TemplateRef,\r\n Type,\r\n ViewContainerRef,\r\n computed,\r\n effect,\r\n inject,\r\n input,\r\n signal,\r\n untracked,\r\n viewChild,\r\n} from '@angular/core';\r\nimport { rxResource } from '@angular/core/rxjs-interop';\r\nimport { AbstractControl } from '@angular/forms';\r\nimport { ValidatorConfig } from '../../models';\r\nimport { FormValidationService } from '../../services/form-validation.service';\r\nimport { CustomErrorMessage } from '../custom-error-message/custom-error-message.abstract';\r\n\r\n@Component({\r\n selector: 'error-message',\r\n imports: [CommonModule],\r\n templateUrl: './error-message.component.html',\r\n host: {\r\n class: 'error-message',\r\n },\r\n})\r\nexport class ErrorMessageComponent {\r\n private internal_formValidationService = inject(FormValidationService);\r\n private customComponentInstance = signal<CustomErrorMessage | null>(null);\r\n\r\n control = input<AbstractControl>();\r\n validators = input<ValidatorConfig[]>();\r\n customComponent = input<Type<CustomErrorMessage>>();\r\n customTemplate = input<TemplateRef<any> | null>(null);\r\n\r\n useDefaultTemplate = computed(\r\n () => !this.customComponent() && !this.customTemplate(),\r\n );\r\n\r\n componentAnchor = viewChild.required('componentAnchor', {\r\n read: ViewContainerRef,\r\n });\r\n\r\n errorMessages = rxResource({\r\n params: () => {\r\n const control = this.control();\r\n const validators = this.validators();\r\n\r\n return { control, validators };\r\n },\r\n stream: ({ params }) => {\r\n return this.internal_formValidationService.getErrorMessages$(\r\n params.control,\r\n params.validators,\r\n );\r\n },\r\n defaultValue: [],\r\n });\r\n\r\n updateControlErrors = effect(() => {\r\n const messages = this.errorMessages.value();\r\n const customComponent = this.customComponentInstance();\r\n\r\n if (!customComponent) {\r\n return;\r\n }\r\n\r\n customComponent.errorMessages.set([...messages]);\r\n });\r\n\r\n injectComponent = effect(() => {\r\n const control = this.control();\r\n const customComponent = this.customComponent();\r\n const anchor = this.componentAnchor();\r\n\r\n if (!customComponent || !anchor || !control) {\r\n return;\r\n }\r\n\r\n untracked(() => {\r\n anchor.clear();\r\n\r\n const componentRef = anchor.createComponent(customComponent);\r\n\r\n componentRef.instance.control = control;\r\n this.customComponentInstance.set(componentRef.instance);\r\n\r\n this.injectComponent.destroy();\r\n });\r\n });\r\n}\r\n","<!-- Custom error message component -->\r\n<ng-container #componentAnchor></ng-container>\r\n<ng-container\r\n [ngTemplateOutlet]=\"customTemplate()\"\r\n [ngTemplateOutletContext]=\"{\r\n control: control(),\r\n messages: errorMessages.value(),\r\n }\"\r\n></ng-container>\r\n\r\n<!-- Default error message component -->\r\n@if (useDefaultTemplate()) {\r\n @for (error of errorMessages.value(); track $index) {\r\n <div>{{ error }}</div>\r\n }\r\n}\r\n","import { CommonModule } from '@angular/common';\r\nimport {\r\n afterNextRender,\r\n Component,\r\n computed,\r\n DestroyRef,\r\n effect,\r\n ElementRef,\r\n inject,\r\n input,\r\n signal,\r\n TemplateRef,\r\n Type,\r\n untracked,\r\n viewChild,\r\n ViewContainerRef,\r\n} from '@angular/core';\r\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\r\nimport { filter, fromEvent, tap } from 'rxjs';\r\nimport { FormControlConfig } from '../../models';\r\nimport { FormLayout } from '../../models/form-layout.interface';\r\nimport { CustomFormLabel } from '../custom-form-label/custom-form-label.abstract';\r\n\r\n@Component({\r\n selector: 'form-label',\r\n imports: [CommonModule],\r\n templateUrl: './form-label.component.html',\r\n styleUrls: ['./form-label.component.scss'],\r\n host: {\r\n class: 'form-label',\r\n },\r\n})\r\nexport class FormLabelComponent {\r\n private el = inject(ElementRef);\r\n private destroyRef = inject(DestroyRef);\r\n private collapsibleElCssText = '';\r\n private componentRef?: CustomFormLabel;\r\n\r\n label = input<string>();\r\n layout = input<FormControlConfig['layout']>();\r\n props = input<FormControlConfig['props']>();\r\n collapsibleEl = input<HTMLElement>();\r\n /**\r\n * State that comes from the root component.\r\n * This will overwrite the current collapsible state\r\n */\r\n state = input<FormLayout['contentCollapsible']>();\r\n customComponent = input<Type<CustomFormLabel>>();\r\n customTemplate = input<TemplateRef<any>>();\r\n\r\n componentAnchor = viewChild.required('componentAnchor', {\r\n read: ViewContainerRef,\r\n });\r\n\r\n expand = signal<boolean>(false);\r\n\r\n useDefaultTemplate = computed(\r\n () => !this.customComponent() && !this.customTemplate(),\r\n );\r\n\r\n isCollapsible = computed(() => {\r\n const layout = this.layout();\r\n if (!layout) {\r\n return false;\r\n }\r\n\r\n return (\r\n layout.contentCollapsible === 'collapse' ||\r\n layout.contentCollapsible === 'expand'\r\n );\r\n });\r\n\r\n injectCustomComponent = effect(() => {\r\n const anchor = this.componentAnchor();\r\n const customComponent = this.customComponent();\r\n\r\n if (!anchor || !customComponent) {\r\n return;\r\n }\r\n\r\n anchor.clear();\r\n\r\n untracked(() => {\r\n const componentRef = anchor.createComponent(customComponent);\r\n\r\n componentRef.instance.label.set(this.label());\r\n componentRef.instance.layout.set(this.layout());\r\n componentRef.instance.props.set(this.props());\r\n componentRef.instance.collapsible.set(this.isCollapsible());\r\n componentRef.instance.expand.set(this.expand());\r\n this.componentRef = componentRef.instance;\r\n\r\n this.injectCustomComponent.destroy();\r\n });\r\n });\r\n\r\n handleStateChange = effect(() => {\r\n const isCollapsible = this.isCollapsible();\r\n const state = this.state();\r\n\r\n if (!isCollapsible || !state) {\r\n return;\r\n }\r\n\r\n this.toggle(state === 'expand');\r\n });\r\n\r\n setExpandInitialState = effect(() => {\r\n const state = this.state();\r\n\r\n if (state) {\r\n this.expand.set(state === 'expand');\r\n } else {\r\n this.expand.set(this.layout()?.contentCollapsible === 'expand');\r\n }\r\n\r\n this.setExpandInitialState.destroy();\r\n });\r\n\r\n setHostStyle = effect(() => {\r\n const host = this.el.nativeElement as HTMLElement;\r\n const label = this.label();\r\n const customComponent = this.customComponent();\r\n const isCollapsible = this.isCollapsible();\r\n\r\n if (!label || !!customComponent) {\r\n return;\r\n }\r\n\r\n if (isCollapsible) {\r\n host.style.setProperty('display', 'flex');\r\n host.style.setProperty('cursor', 'pointer');\r\n } else {\r\n host.style.setProperty('display', 'inline-block');\r\n host.style.removeProperty('cursor');\r\n }\r\n });\r\n\r\n constructor() {\r\n afterNextRender(() => {\r\n this.initCollapsibleEl();\r\n this.listenClickEvent();\r\n });\r\n }\r\n\r\n toggle = (expand?: boolean) => {\r\n const collapsible = this.isCollapsible();\r\n\r\n if (!collapsible) {\r\n return;\r\n }\r\n\r\n this.expand.update((x) => expand ?? !x);\r\n this.setElementHeight();\r\n\r\n if (this.componentRef) {\r\n this.componentRef.expand.set(this.expand());\r\n }\r\n };\r\n\r\n private listenTransition(): void {\r\n const el = this.collapsibleEl();\r\n\r\n if (!el) {\r\n return;\r\n }\r\n\r\n const transitionEnd$ = fromEvent(el, 'transitionend', {\r\n passive: true,\r\n }).pipe(\r\n filter(() => this.expand()),\r\n tap(() => {\r\n el?.classList.remove(...['height', 'overflow']);\r\n }),\r\n );\r\n\r\n transitionEnd$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();\r\n }\r\n\r\n private setElementHeight(): void {\r\n this.setExpandStyle();\r\n\r\n if (!this.expand()) {\r\n requestAnimationFrame(() => this.setCollapseStyle());\r\n }\r\n }\r\n\r\n private initCollapsibleEl(): void {\r\n const el = this.collapsibleEl();\r\n\r\n if (!el || !this.isCollapsible()) {\r\n return;\r\n }\r\n\r\n this.collapsibleElCssText = el.style.cssText || '';\r\n el.classList.add('collapsible-container');\r\n this.listenTransition();\r\n\r\n if (!this.expand()) {\r\n this.setCollapseStyle();\r\n }\r\n }\r\n\r\n private setCollapseStyle(): void {\r\n const el = this.collapsibleEl();\r\n const stylesToRemove = ['border', 'padding', 'margin'];\r\n\r\n if (!el) {\r\n return;\r\n }\r\n\r\n stylesToRemove.forEach((style) => {\r\n if (!this.collapsibleElCssText.includes(style)) return;\r\n el.style.removeProperty(style);\r\n });\r\n\r\n el.style.setProperty('overflow', 'hidden');\r\n el.style.setProperty('height', '0px');\r\n }\r\n\r\n private setExpandStyle(): void {\r\n const el = this.collapsibleEl();\r\n\r\n const height = !el ? 0 : el.scrollHeight + 1;\r\n\r\n // Set existing styles from collapsible element first\r\n if (this.collapsibleElCssText) {\r\n el?.setAttribute('style', this.collapsibleElCssText);\r\n }\r\n\r\n // Then set height later to overwrite height style\r\n el?.style.setProperty('height', `${height}px`);\r\n }\r\n\r\n private listenClickEvent(): void {\r\n const host = this.el.nativeElement as HTMLElement;\r\n const collapsible = this.isCollapsible();\r\n\r\n if (!collapsible) {\r\n return;\r\n }\r\n\r\n fromEvent(host, 'click', { passive: true })\r\n .pipe(\r\n tap(() => this.toggle()),\r\n takeUntilDestroyed(this.destroyRef),\r\n )\r\n .subscribe();\r\n }\r\n}\r\n","@if (useDefaultTemplate()) {\r\n <span class=\"text\">{{ label() }}</span>\r\n @if (isCollapsible()) {\r\n <span\r\n style=\"margin-left: auto\"\r\n [ngStyle]=\"{\r\n transform: !expand() ? 'rotate(-180deg)' : 'rotate(0deg)',\r\n }\"\r\n >\r\n <!-- prettier-ignore -->\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" fill=\"currentColor\" class=\"bi bi-chevron-up\" viewBox=\"0 0 16 16\">\r\n <path fill-rule=\"evenodd\" d=\"M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708z\"/>\r\n </svg>\r\n </span>\r\n }\r\n}\r\n\r\n<ng-container #componentAnchor></ng-container>\r\n\r\n@if (customTemplate(); as template) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"template\"\r\n [ngTemplateOutletContext]=\"{\r\n label: label(),\r\n layout: layout(),\r\n toggle,\r\n collapsible: isCollapsible(),\r\n expand: expand(),\r\n props: props(),\r\n }\"\r\n ></ng-container>\r\n}\r\n","import { CommonModule } from '@angular/common';\r\nimport {\r\n Component,\r\n computed,\r\n DestroyRef,\r\n inject,\r\n input,\r\n OnInit,\r\n signal,\r\n} from '@angular/core';\r\nimport {\r\n rxResource,\r\n takeUntilDestroyed,\r\n toSignal,\r\n} from '@angular/core/rxjs-interop';\r\nimport { AbstractControl } from '@angular/forms';\r\nimport { map, merge, startWith, takeWhile, tap } from 'rxjs';\r\nimport { ControlLayoutDirective } from '../../directives/control-layout.directive';\r\nimport { FormControlConfig } from '../../models';\r\nimport { FormLayout } from '../../models/form-layout.interface';\r\nimport { IsControlRequiredPipe } from '../../pipes/is-control-required.pipe';\r\nimport { GlobalVariableService } from '../../services/global-variable.service';\r\nimport { HostEventService } from '../../services/host-event.service';\r\nimport { ErrorMessageComponent } from '../error-message/error-message.component';\r\nimport { FormLabelComponent } from '../form-label/form-label.component';\r\n\r\n@Component({\r\n selector: 'content-wrapper',\r\n imports: [\r\n CommonModule,\r\n FormLabelComponent,\r\n ErrorMessageComponent,\r\n ControlLayoutDirective,\r\n IsControlRequiredPipe,\r\n ],\r\n templateUrl: './content-wrapper.component.html',\r\n styles: [],\r\n})\r\nexport class ContentWrapperComponent implements OnInit {\r\n private destroyRef = inject(DestroyRef);\r\n private global = inject(GlobalVariableService);\r\n private hostEventService = inject(HostEventService);\r\n\r\n readonly showErrorsOnTouched = this.global.showErrorsOnTouched;\r\n\r\n config = input<FormControlConfig>();\r\n control = input<AbstractControl>();\r\n collapsibleState = input<FormLayout['contentCollapsible']>();\r\n\r\n isDirty = signal<boolean>(false);\r\n isTouched = signal<boolean>(false);\r\n\r\n controlErrors = rxResource({\r\n params: () => this.control(),\r\n stream: ({ params }) =>\r\n merge(params.valueChanges, params.statusChanges).pipe(\r\n startWith(params.status),\r\n map(() => params.errors),\r\n ),\r\n defaultValue: null,\r\n });\r\n\r\n hideErrors = toSignal(\r\n this.global.hideErrorMessage$.pipe(tap(() => this.updateControlStatus())),\r\n );\r\n\r\n formControlName = computed(() => this.config()?.formControlName ?? '');\r\n\r\n description = computed(() => this.config()?.description);\r\n descriptionPosition = computed(() => {\r\n const position =\r\n this.layout()?.descriptionPosition ?? this.global.descriptionPosition;\r\n\r\n return position;\r\n });\r\n\r\n label = computed(() => this.config()?.label);\r\n layout = computed(() => this.config()?.layout);\r\n props = computed(() => this.config()?.props);\r\n\r\n validators = computed(() => {\r\n const { validators, asyncValidators } = this.config() ?? {};\r\n return [...(validators || []), ...(asyncValidators || [])];\r\n });\r\n\r\n showLabel = computed(() => this.label() && this.layout()?.hideLabel !== true);\r\n\r\n customErrorComponent = computed(() => {\r\n const name = this.formControlName();\r\n const components = this.global.errorComponents;\r\n const defaultComponent = this.global.errorComponentDefault;\r\n\r\n return components?.[name] ?? defaultComponent;\r\n });\r\n\r\n customErrorTemplate = computed(() => {\r\n const name = this.formControlName();\r\n const templates = this.global.errorTemplates;\r\n const defaultTemplate = this.global.errorTemplateDefault;\r\n\r\n return templates?.[name] ?? defaultTemplate ?? null;\r\n });\r\n\r\n customLabelComponent = computed(() => {\r\n const name = this.formControlName();\r\n const components = this.global.labelComponents;\r\n const defaultComponent = this.global.labelComponentDefault;\r\n\r\n return components?.[name] ?? defaultComponent;\r\n });\r\n\r\n customLabelTemplate = computed(() => {\r\n const name = this.formControlName();\r\n const templates = this.global.labelTemplates;\r\n const defaultTemplate = this.global.labelTemplateDefault;\r\n\r\n return templates?.[name] ?? defaultTemplate;\r\n });\r\n\r\n renderErrorSection = computed(() => {\r\n const type = this.config()?.type ?? 'text';\r\n const typesToHide = this.global.hideErrorsForTypes ?? [];\r\n\r\n return typesToHide.filter(Boolean).every((x) => x !== type);\r\n });\r\n\r\n showErrors = computed(() => {\r\n const errors = this.controlErrors.value();\r\n const hideErrors = this.hideErrors();\r\n const isDirty = this.isDirty();\r\n const isTouched = this.isTouched();\r\n\r\n if (!errors || hideErrors) {\r\n return false;\r\n }\r\n\r\n if (this.showErrorsOnTouched) {\r\n return isDirty || isTouched || false;\r\n }\r\n\r\n return isDirty ?? false;\r\n });\r\n\r\n ngOnInit(): void {\r\n merge(this.hostEventService.keyUp$, this.hostEventService.pointerUp$)\r\n .pipe(\r\n tap(() => this.updateControlStatus()),\r\n