UNPKG

ng-dynamic-json-form

Version:

Generate Angular reactive form dynamically using JSON. Support conditional rendering and toggle of validators.

999 lines (976 loc) 319 kB
import * as i0 from '@angular/core'; import { signal, Component, inject, ElementRef, input, computed, effect, Directive, InjectionToken, Injector, ChangeDetectorRef, isSignal, SimpleChange, HostListener, Pipe, Injectable, PLATFORM_ID, viewChild, ViewContainerRef, untracked, DestroyRef, afterNextRender, isDevMode, LOCALE_ID, forwardRef, viewChildren, output, ChangeDetectionStrategy } from '@angular/core'; import * as i2 from '@angular/forms'; import { isFormControl, isFormGroup, FormControl, Validators, UntypedFormGroup, FormArray, ReactiveFormsModule, FormGroup, UntypedFormControl, NG_VALUE_ACCESSOR, NG_VALIDATORS, NgControl, NG_ASYNC_VALIDATORS } from '@angular/forms'; import { IMaskDirective } from 'angular-imask'; import * as i1 from '@angular/common'; import { isPlatformServer, CommonModule, formatDate } from '@angular/common'; import { rxResource, takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop'; import { BehaviorSubject, Subject, EMPTY, merge, fromEvent, tap, of, startWith, map, filter, takeWhile, from, mergeMap, Observable, finalize, catchError, takeUntil, combineLatest, switchMap, distinctUntilChanged, debounceTime, take } from 'rxjs'; import IMask from 'imask/esm/index'; import { HttpClient } from '@angular/common/http'; /**Get all the errors in the AbstractControl recursively * @example * root: { * control1: ValidationErrors, * control2: { * childA: ValidationErrors * } * } */ function getControlErrors(control) { if (!control) { return null; } if (isFormControl(control)) { return control.errors; } if (isFormGroup(control)) { const result = Object.keys(control.controls).reduce((acc, key) => { const childControl = control.controls[key]; const err = getControlErrors(childControl); return err ? { ...acc, [key]: err } : acc; }, {}); return !Object.keys(result).length ? null : result; } return null; } class CustomControlComponent { constructor() { this.hostForm = signal(undefined, ...(ngDevMode ? [{ debugName: "hostForm" }] : [])); this.data = signal(undefined, ...(ngDevMode ? [{ debugName: "data" }] : [])); this.hideErrorMessage = signal(undefined, ...(ngDevMode ? [{ debugName: "hideErrorMessage" }] : [])); } writeValue(obj) { this.control?.patchValue(obj); } registerOnChange(fn) { } registerOnTouched(fn) { } setDisabledState(isDisabled) { isDisabled ? this.control?.disable() : this.control?.enable(); } validate(control) { return getControlErrors(this.control); } markAsDirty() { } markAsPristine() { } markAsTouched() { } markAsUntouched() { } setErrors(errors) { } onOptionsGet(options) { const data = this.data(); if (!data || !data.options) { return; } this.data.update((x) => { if (!x?.options) { return x; } x.options.data = [...options]; return { ...x }; }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CustomControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.3", type: CustomControlComponent, isStandalone: true, selector: "custom-control", ngImport: i0, template: ``, isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: CustomControlComponent, decorators: [{ type: Component, args: [{ selector: 'custom-control', template: ``, standalone: true, }] }] }); class CustomErrorMessage { constructor() { this.control = new FormControl(); this.errorMessages = signal([], ...(ngDevMode ? [{ debugName: "errorMessages" }] : [])); } } class CustomFormLabel { constructor() { this.collapsible = signal(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : [])); this.expand = signal(false, ...(ngDevMode ? [{ debugName: "expand" }] : [])); this.label = signal(undefined, ...(ngDevMode ? [{ debugName: "label" }] : [])); this.layout = signal(undefined, ...(ngDevMode ? [{ debugName: "layout" }] : [])); this.props = signal(undefined, ...(ngDevMode ? [{ debugName: "props" }] : [])); } } function getClassListFromString(classNames) { if (!classNames) { return []; } try { return classNames .split(/\s{1,}/) .map((x) => x.trim()) .filter(Boolean); } catch { return []; } } function getStyleListFromString(styles) { if (!styles) { return []; } try { return styles .split(';') .map((x) => x.trim()) .filter(Boolean); } catch { return []; } } class ControlLayoutDirective { constructor() { this.el = inject(ElementRef); this.controlLayout = input(...(ngDevMode ? [undefined, { debugName: "controlLayout" }] : [])); this.classNames = computed(() => { const data = this.controlLayout(); if (!data) { return []; } const { type, layout } = data; const classString = layout?.[`${type ?? 'host'}Class`] ?? ''; const result = getClassListFromString(classString); return result; }, ...(ngDevMode ? [{ debugName: "classNames" }] : [])); this.styleList = computed(() => { const data = this.controlLayout(); if (!data) { return []; } const { type, layout } = data; const styleString = layout?.[`${type ?? 'host'}Styles`] ?? ''; const result = getStyleListFromString(styleString); return result; }, ...(ngDevMode ? [{ debugName: "styleList" }] : [])); this.updateClass = effect(() => { const host = this.el.nativeElement; const classNames = this.classNames(); if (!classNames.length) { return; } host.classList.add(...classNames); }, ...(ngDevMode ? [{ debugName: "updateClass" }] : [])); this.updateStyles = effect(() => { const host = this.el.nativeElement; const styleList = this.styleList(); if (!styleList.length) { return; } for (const item of styleList) { const [name, value] = item.split(':').map((x) => x.trim()); host.style.setProperty(name, value); } }, ...(ngDevMode ? [{ debugName: "updateStyles" }] : [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlLayoutDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: ControlLayoutDirective, isStandalone: true, selector: "[controlLayout]", inputs: { controlLayout: { classPropertyName: "controlLayout", publicName: "controlLayout", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlLayoutDirective, decorators: [{ type: Directive, args: [{ selector: '[controlLayout]', standalone: true, }] }] }); class HostIdDirective { constructor() { this.el = inject(ElementRef); this.hostId = input(...(ngDevMode ? [undefined, { debugName: "hostId" }] : [])); this.computedId = computed(() => { const hostId = this.hostId(); if (!hostId) { return; } const { parentId, controlName } = hostId; return parentId ? `${parentId}.${controlName}` : controlName; }, ...(ngDevMode ? [{ debugName: "computedId" }] : [])); this.updateAttribute = effect(() => { const id = this.computedId(); const host = this.el.nativeElement; if (id) { host.setAttribute('id', id); } }, ...(ngDevMode ? [{ debugName: "updateAttribute" }] : [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostIdDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: HostIdDirective, isStandalone: true, selector: "[hostId]", inputs: { hostId: { classPropertyName: "hostId", publicName: "hostId", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostIdDirective, decorators: [{ type: Directive, args: [{ selector: '[hostId]', standalone: true, }] }] }); class ImaskValuePatchDirective { constructor() { this.imask = inject(IMaskDirective); this.isNumber = false; const iMask = this.imask; iMask.writeValue = (value) => { // ----- Modified area ----- if (!this.isNumber) { this.isNumber = typeof value === 'number'; } value = value == null && iMask.unmask !== 'typed' ? '' : `${value}`; // ----- Modified area ----- if (iMask.maskRef) { iMask.beginWrite(value); iMask.maskValue = value; iMask.endWrite(); } else { iMask['_renderer'].setProperty(iMask.element, 'value', value); iMask['_initialValue'] = value; } }; iMask['_onAccept'] = () => { // ----- Modified area ----- const valueParsed = this.isNumber ? parseFloat(iMask.maskValue) : iMask.maskValue; const value = isNaN(valueParsed) ? null : valueParsed; // ----- Modified area ----- // if value was not changed during writing don't fire events // for details see https://github.com/uNmAnNeR/imaskjs/issues/136 if (iMask['_writing'] && value === iMask.endWrite()) return; iMask.onChange(value); iMask.accept.emit(value); }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImaskValuePatchDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.3", type: ImaskValuePatchDirective, isStandalone: true, selector: "[imaskValuePatch]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImaskValuePatchDirective, decorators: [{ type: Directive, args: [{ selector: '[imaskValuePatch]', standalone: true, }] }], ctorParameters: () => [] }); const PROPS_BINDING_INJECTORS = new InjectionToken('property-binding-injector'); function providePropsBinding(value) { return { provide: PROPS_BINDING_INJECTORS, useValue: value, }; } class PropsBindingDirective { constructor() { this.injectionTokens = inject(PROPS_BINDING_INJECTORS, { optional: true, }); this.injector = inject(Injector); this.cd = inject(ChangeDetectorRef); this.el = inject(ElementRef); this.propsBinding = input([], ...(ngDevMode ? [{ debugName: "propsBinding" }] : [])); this.validPropsData = computed(() => this.propsBinding().filter((x) => Boolean(x) && typeof x.props === 'object' && Object.keys(x.props).length > 0), ...(ngDevMode ? [{ debugName: "validPropsData" }] : [])); this.handlePropsGet = effect(() => { const propsData = this.validPropsData(); if (!propsData.length) { return; } const hostEl = this.el.nativeElement; for (const item of propsData) { const { props, key, omit = [] } = item; const providerToken = this.injectionTokens?.find((x) => x.key === key)?.token; const component = !providerToken ? null : this.injector.get(providerToken); for (const key in props) { const value = props[key]; if (value == undefined || omit.includes(key)) { continue; } if (component) { this.updateComponentProperty({ component, key, value }); } if (hostEl) { // Only set CSS custom properties (starting with --) or valid HTML attributes if (key.startsWith('--')) { hostEl.style.setProperty(key, value); } else if (this.isValidHtmlAttribute(key)) { hostEl.setAttribute(key, value); } } } } this.cd.markForCheck(); this.cd.detectChanges(); }, ...(ngDevMode ? [{ debugName: "handlePropsGet" }] : [])); } updateComponentProperty(data) { const { component, key, value } = data; const hasProperty = this.hasProperty(component, key); if (!hasProperty) { return; } const property = component[key]; if (isSignal(property)) { if (typeof property.set === 'function') { property.set(value); } } else { component[key] = value; } // For compatibility if (component['ngOnChanges']) { const simpleChange = new SimpleChange(undefined, value, true); component.ngOnChanges({ [key]: simpleChange }); } } hasProperty(obj, key) { return Object.hasOwn(obj, key) || key in obj; } isValidHtmlAttribute(attributeName) { // Common HTML attributes - this is not exhaustive but covers most use cases const validAttributes = new Set([ 'id', 'class', 'style', 'title', 'lang', 'dir', 'hidden', 'tabindex', 'accesskey', 'contenteditable', 'draggable', 'spellcheck', 'translate', 'role', 'aria-label', 'aria-labelledby', 'aria-describedby', 'aria-hidden', 'aria-expanded', 'aria-selected', 'aria-checked', 'aria-disabled', 'data-testid', 'disabled', 'readonly', 'required', 'placeholder', 'value', 'checked', 'selected', 'multiple', 'size', 'rows', 'cols', 'min', 'max', 'step', 'pattern', 'minlength', 'maxlength', 'src', 'alt', 'href', 'target', 'rel', 'type', 'name', 'for', ]); // Allow data-* and aria-* attributes return (validAttributes.has(attributeName.toLowerCase()) || attributeName.startsWith('data-') || attributeName.startsWith('aria-')); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: PropsBindingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: PropsBindingDirective, isStandalone: true, selector: "[propsBinding]", inputs: { propsBinding: { classPropertyName: "propsBinding", publicName: "propsBinding", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: PropsBindingDirective, decorators: [{ type: Directive, args: [{ selector: '[propsBinding]', standalone: true, }] }] }); class TextareaAutHeightDirective { constructor() { this.el = inject(ElementRef); this.autoResize = input(true, ...(ngDevMode ? [{ debugName: "autoResize" }] : [])); } onInput() { this.setHeight(); } ngOnInit() { this.removeResizeProperty(); } removeResizeProperty() { const hostEl = this.el.nativeElement; hostEl.style.setProperty('resize', 'none'); } setHeight() { const autoResize = this.autoResize(); const hostEl = this.el.nativeElement; if (!hostEl || !autoResize) { return; } const borderWidth = Math.ceil(parseFloat(window.getComputedStyle(hostEl).borderWidth)); hostEl.style.removeProperty('height'); hostEl.style.setProperty('height', `${hostEl.scrollHeight + borderWidth * 2}px`); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: TextareaAutHeightDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.1.3", type: TextareaAutHeightDirective, isStandalone: true, selector: "[textareaAutoHeight]", inputs: { autoResize: { classPropertyName: "autoResize", publicName: "autoResize", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "input": "onInput($event)" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: TextareaAutHeightDirective, decorators: [{ type: Directive, args: [{ selector: '[textareaAutoHeight]', standalone: true, }] }], propDecorators: { onInput: [{ type: HostListener, args: ['input', ['$event']] }] } }); var ValidatorsEnum; (function (ValidatorsEnum) { ValidatorsEnum["required"] = "required"; ValidatorsEnum["requiredTrue"] = "requiredTrue"; ValidatorsEnum["min"] = "min"; ValidatorsEnum["max"] = "max"; ValidatorsEnum["minLength"] = "minLength"; ValidatorsEnum["maxLength"] = "maxLength"; ValidatorsEnum["email"] = "email"; ValidatorsEnum["pattern"] = "pattern"; })(ValidatorsEnum || (ValidatorsEnum = {})); var ConditionsActionEnum; (function (ConditionsActionEnum) { ConditionsActionEnum["control.hidden"] = "control.hidden"; ConditionsActionEnum["control.disabled"] = "control.disabled"; ConditionsActionEnum["validator.required"] = "validator.required"; ConditionsActionEnum["validator.requiredTrue"] = "validator.requiredTrue"; ConditionsActionEnum["validator.min"] = "validator.min"; ConditionsActionEnum["validator.max"] = "validator.max"; ConditionsActionEnum["validator.minLength"] = "validator.minLength"; ConditionsActionEnum["validator.maxLength"] = "validator.maxLength"; ConditionsActionEnum["validator.email"] = "validator.email"; ConditionsActionEnum["validator.pattern"] = "validator.pattern"; })(ConditionsActionEnum || (ConditionsActionEnum = {})); class ControlTypeByConfigPipe { transform(config) { if (!config.children) { return 'FormControl'; } return 'FormGroup'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlTypeByConfigPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.1.3", ngImport: i0, type: ControlTypeByConfigPipe, isStandalone: true, name: "controlTypeByConfig" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ControlTypeByConfigPipe, decorators: [{ type: Pipe, args: [{ name: 'controlTypeByConfig', standalone: true, }] }] }); class GlobalVariableService { constructor() { this.hideErrorMessage$ = new BehaviorSubject(undefined); this.rootConfigs = []; this.showErrorsOnTouched = true; } // ====================================================================== setup(variables) { for (const key in variables) { this[key] = variables[key]; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: GlobalVariableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: GlobalVariableService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: GlobalVariableService, decorators: [{ type: Injectable }] }); class IsControlRequiredPipe { transform(value) { if (!value) { return false; } return value.hasValidator(Validators.required); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IsControlRequiredPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.1.3", ngImport: i0, type: IsControlRequiredPipe, isStandalone: true, name: "isControlRequired" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: IsControlRequiredPipe, decorators: [{ type: Pipe, args: [{ name: 'isControlRequired', standalone: true, }] }] }); class HostEventService { constructor() { this.platformId = inject(PLATFORM_ID); this.focusOut$ = new Subject(); this.keyUp$ = new Subject(); this.keyDown$ = new Subject(); this.pointerUp$ = new Subject(); this.pointerDown$ = new Subject(); } start$(hostElement) { if (this.isServer) { return EMPTY; } return merge(this.event$(hostElement, 'focusout', this.focusOut$), this.event$(hostElement, 'keyup', this.keyUp$), this.event$(hostElement, 'keydown', this.keyDown$), this.event$(hostElement, 'pointerup', this.pointerUp$), this.event$(hostElement, 'pointerdown', this.pointerDown$)); } event$(target, name, subject) { if (this.isServer) { return EMPTY; } return fromEvent(target, name, { passive: true }).pipe(tap((x) => subject.next(x))); } get isServer() { return isPlatformServer(this.platformId); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostEventService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostEventService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: HostEventService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); function emailValidator(control) { const emailValid = RegExp(/^[^@\s!(){}<>]+@[\w-]+(\.[A-Za-z]+)+$/).test(control.value); if (!control.value) { return null; } return emailValid ? null : { email: 'Invalid email format' }; } const builtInValidators = (value) => ({ [ValidatorsEnum.required]: Validators.required, [ValidatorsEnum.requiredTrue]: Validators.requiredTrue, [ValidatorsEnum.email]: emailValidator, [ValidatorsEnum.pattern]: Validators.pattern(value), [ValidatorsEnum.min]: Validators.min(value), [ValidatorsEnum.max]: Validators.max(value), [ValidatorsEnum.minLength]: Validators.minLength(value), [ValidatorsEnum.maxLength]: Validators.maxLength(value), }); class FormValidationService { constructor() { this.globalVariableService = inject(GlobalVariableService); } getErrorMessages$(control, validators) { if (!control || !validators?.length) { return of([]); } return control.statusChanges.pipe(startWith(control.status), map(() => this.getErrorMessages(control.errors, control.value, validators))); } getValidators(input) { if (!input || !input.length) { return []; } // Remove duplicates const filteredConfigs = [ ...new Map(input.map((v) => [v.name, v])).values(), ]; const customValidators = this.globalVariableService.customValidators; const validatorFns = filteredConfigs.map((item) => { const { name } = item; const value = this.getValidatorValue(item); const builtInValidator = builtInValidators(value)[name]; const customValidator = this.getValidatorFn(item, customValidators?.[name]); const result = customValidator ?? builtInValidator; return result; }); return validatorFns.filter(Boolean); } getAsyncValidators(input) { if (!input || !input.length) { return []; } // Remove duplicates const filteredConfigs = [ ...new Map(input.map((v) => [v.name, v])).values(), ]; const customAsyncValidators = this.globalVariableService.customAsyncValidators; const validatorFns = filteredConfigs.map((item) => { const validatorFn = customAsyncValidators?.[item.name]; return this.getValidatorFn(item, validatorFn); }); return validatorFns.filter(Boolean); } /**Get the error messages of the control * * @description * Try to get the custom error message specified in the config first, * else use the error message in the `ValidationErrors`. * * When using custom validator, the custom message most likely will not working, * it's because we are using the key in the errors to find the config message. * Since user can define the error object, it becomes very difficult to match the config name * with the keys in the error object. */ getErrorMessages(controlErrors, controlValue, validatorConfigs) { if (!controlErrors) { return []; } const errorMessage = (error) => { return typeof error === 'string' ? error : JSON.stringify(error); }; return Object.keys(controlErrors).reduce((acc, key) => { const error = controlErrors[key]; const config = this.getConfigFromErrorKey({ [key]: error }, validatorConfigs); const configMessage = config?.message; const defaultMessage = this.globalVariableService.validationMessages?.[config?.name ?? '']; const customMessage = (configMessage || defaultMessage) ?.replace(/{{value}}/g, controlValue || '') .replace(/{{validatorValue}}/g, config?.value); acc.push(customMessage || errorMessage(error)); return acc; }, []); } getConfigFromErrorKey(error, configs) { // The key mapping of the `ValidationErrors` with the `ValidatorConfig`, // to let us get the correct message by using `name` of `ValidatorConfig`. const valueKeyMapping = { pattern: 'requiredPattern', min: 'min', max: 'max', minLength: 'requiredLength', maxLength: 'requiredLength', }; const getValidatorValue = (v) => { return typeof v !== 'number' && !isNaN(v) ? parseFloat(v) : v; }; const [errorKey, errorValue] = Object.entries(error)[0]; const result = configs.find((item) => { const { name, value } = item; if (errorKey !== name.toLowerCase()) { return false; } if (value === undefined || value === null || value === '') { return true; } const targetKey = valueKeyMapping[name] ?? ''; const requiredValue = errorValue[targetKey]; const validatorValue = getValidatorValue(value); return requiredValue && name === 'pattern' ? requiredValue.includes(validatorValue) : requiredValue === validatorValue; }); return result; } getValidatorValue(validatorConfig) { const { name, value, flags } = validatorConfig; switch (name) { case ValidatorsEnum.pattern: return value instanceof RegExp ? value : new RegExp(value, flags); case ValidatorsEnum.min: case ValidatorsEnum.max: case ValidatorsEnum.minLength: case ValidatorsEnum.maxLength: try { return typeof value !== 'number' ? parseFloat(value) : value; } catch { break; } default: return value; } } /** * Get validatorFn from either validatorFn or factory function that return a validatorFn. * If it's a factory function, return the validatorFn instead. * * @param validatorConfig * @param validatorFn */ getValidatorFn(validatorConfig, validatorFn) { const { value } = validatorConfig; if (!validatorFn) { return null; } const result = typeof validatorFn({}) !== 'function' ? validatorFn : validatorFn(value); return result; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormValidationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormValidationService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormValidationService, decorators: [{ type: Injectable }] }); class ErrorMessageComponent { constructor() { this.internal_formValidationService = inject(FormValidationService); this.customComponentInstance = signal(null, ...(ngDevMode ? [{ debugName: "customComponentInstance" }] : [])); this.control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : [])); this.validators = input(...(ngDevMode ? [undefined, { debugName: "validators" }] : [])); this.customComponent = input(...(ngDevMode ? [undefined, { debugName: "customComponent" }] : [])); this.customTemplate = input(null, ...(ngDevMode ? [{ debugName: "customTemplate" }] : [])); this.useDefaultTemplate = computed(() => !this.customComponent() && !this.customTemplate(), ...(ngDevMode ? [{ debugName: "useDefaultTemplate" }] : [])); this.componentAnchor = viewChild.required('componentAnchor', { read: ViewContainerRef, }); this.errorMessages = rxResource({ params: () => { const control = this.control(); const validators = this.validators(); return { control, validators }; }, stream: ({ params }) => { return this.internal_formValidationService.getErrorMessages$(params.control, params.validators); }, defaultValue: [], }); this.updateControlErrors = effect(() => { const messages = this.errorMessages.value(); const customComponent = this.customComponentInstance(); if (!customComponent) { return; } customComponent.errorMessages.set([...messages]); }, ...(ngDevMode ? [{ debugName: "updateControlErrors" }] : [])); this.injectComponent = effect(() => { const control = this.control(); const customComponent = this.customComponent(); const anchor = this.componentAnchor(); if (!customComponent || !anchor || !control) { return; } untracked(() => { anchor.clear(); const componentRef = anchor.createComponent(customComponent); componentRef.instance.control = control; this.customComponentInstance.set(componentRef.instance); this.injectComponent.destroy(); }); }, ...(ngDevMode ? [{ debugName: "injectComponent" }] : [])); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ErrorMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: ErrorMessageComponent, isStandalone: true, selector: "error-message", inputs: { control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, validators: { classPropertyName: "validators", publicName: "validators", isSignal: true, isRequired: false, transformFunction: null }, customComponent: { classPropertyName: "customComponent", publicName: "customComponent", isSignal: true, isRequired: false, transformFunction: null }, customTemplate: { classPropertyName: "customTemplate", publicName: "customTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "error-message" }, viewQueries: [{ propertyName: "componentAnchor", first: true, predicate: ["componentAnchor"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- 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", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ErrorMessageComponent, decorators: [{ type: Component, args: [{ selector: 'error-message', imports: [CommonModule], host: { class: 'error-message', }, template: "<!-- 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" }] }] }); class FormLabelComponent { constructor() { this.el = inject(ElementRef); this.destroyRef = inject(DestroyRef); this.collapsibleElCssText = ''; this.label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : [])); this.layout = input(...(ngDevMode ? [undefined, { debugName: "layout" }] : [])); this.props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : [])); this.collapsibleEl = input(...(ngDevMode ? [undefined, { debugName: "collapsibleEl" }] : [])); /** * State that comes from the root component. * This will overwrite the current collapsible state */ this.state = input(...(ngDevMode ? [undefined, { debugName: "state" }] : [])); this.customComponent = input(...(ngDevMode ? [undefined, { debugName: "customComponent" }] : [])); this.customTemplate = input(...(ngDevMode ? [undefined, { debugName: "customTemplate" }] : [])); this.componentAnchor = viewChild.required('componentAnchor', { read: ViewContainerRef, }); this.expand = signal(false, ...(ngDevMode ? [{ debugName: "expand" }] : [])); this.useDefaultTemplate = computed(() => !this.customComponent() && !this.customTemplate(), ...(ngDevMode ? [{ debugName: "useDefaultTemplate" }] : [])); this.isCollapsible = computed(() => { const layout = this.layout(); if (!layout) { return false; } return (layout.contentCollapsible === 'collapse' || layout.contentCollapsible === 'expand'); }, ...(ngDevMode ? [{ debugName: "isCollapsible" }] : [])); this.injectCustomComponent = effect(() => { const anchor = this.componentAnchor(); const customComponent = this.customComponent(); if (!anchor || !customComponent) { return; } anchor.clear(); untracked(() => { const componentRef = anchor.createComponent(customComponent); componentRef.instance.label.set(this.label()); componentRef.instance.layout.set(this.layout()); componentRef.instance.props.set(this.props()); componentRef.instance.collapsible.set(this.isCollapsible()); componentRef.instance.expand.set(this.expand()); this.componentRef = componentRef.instance; this.injectCustomComponent.destroy(); }); }, ...(ngDevMode ? [{ debugName: "injectCustomComponent" }] : [])); this.handleStateChange = effect(() => { const isCollapsible = this.isCollapsible(); const state = this.state(); if (!isCollapsible || !state) { return; } this.toggle(state === 'expand'); }, ...(ngDevMode ? [{ debugName: "handleStateChange" }] : [])); this.setExpandInitialState = effect(() => { const state = this.state(); if (state) { this.expand.set(state === 'expand'); } else { this.expand.set(this.layout()?.contentCollapsible === 'expand'); } this.setExpandInitialState.destroy(); }, ...(ngDevMode ? [{ debugName: "setExpandInitialState" }] : [])); this.setHostStyle = effect(() => { const host = this.el.nativeElement; const label = this.label(); const customComponent = this.customComponent(); const isCollapsible = this.isCollapsible(); if (!label || !!customComponent) { return; } if (isCollapsible) { host.style.setProperty('display', 'flex'); host.style.setProperty('cursor', 'pointer'); } else { host.style.setProperty('display', 'inline-block'); host.style.removeProperty('cursor'); } }, ...(ngDevMode ? [{ debugName: "setHostStyle" }] : [])); this.toggle = (expand) => { const collapsible = this.isCollapsible(); if (!collapsible) { return; } this.expand.update((x) => expand ?? !x); this.setElementHeight(); if (this.componentRef) { this.componentRef.expand.set(this.expand()); } }; afterNextRender(() => { this.initCollapsibleEl(); this.listenClickEvent(); }); } listenTransition() { const el = this.collapsibleEl(); if (!el) { return; } const transitionEnd$ = fromEvent(el, 'transitionend', { passive: true, }).pipe(filter(() => this.expand()), tap(() => { el?.classList.remove(...['height', 'overflow']); })); transitionEnd$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); } setElementHeight() { this.setExpandStyle(); if (!this.expand()) { requestAnimationFrame(() => this.setCollapseStyle()); } } initCollapsibleEl() { const el = this.collapsibleEl(); if (!el || !this.isCollapsible()) { return; } this.collapsibleElCssText = el.style.cssText || ''; el.classList.add('collapsible-container'); this.listenTransition(); if (!this.expand()) { this.setCollapseStyle(); } } setCollapseStyle() { const el = this.collapsibleEl(); const stylesToRemove = ['border', 'padding', 'margin']; if (!el) { return; } stylesToRemove.forEach((style) => { if (!this.collapsibleElCssText.includes(style)) return; el.style.removeProperty(style); }); el.style.setProperty('overflow', 'hidden'); el.style.setProperty('height', '0px'); } setExpandStyle() { const el = this.collapsibleEl(); const height = !el ? 0 : el.scrollHeight + 1; // Set existing styles from collapsible element first if (this.collapsibleElCssText) { el?.setAttribute('style', this.collapsibleElCssText); } // Then set height later to overwrite height style el?.style.setProperty('height', `${height}px`); } listenClickEvent() { const host = this.el.nativeElement; const collapsible = this.isCollapsible(); if (!collapsible) { return; } fromEvent(host, 'click', { passive: true }) .pipe(tap(() => this.toggle()), takeUntilDestroyed(this.destroyRef)) .subscribe(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormLabelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: FormLabelComponent, isStandalone: true, selector: "form-label", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, collapsibleEl: { classPropertyName: "collapsibleEl", publicName: "collapsibleEl", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, customComponent: { classPropertyName: "customComponent", publicName: "customComponent", isSignal: true, isRequired: false, transformFunction: null }, customTemplate: { classPropertyName: "customTemplate", publicName: "customTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "form-label" }, viewQueries: [{ propertyName: "componentAnchor", first: true, predicate: ["componentAnchor"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "@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", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: FormLabelComponent, decorators: [{ type: Component, args: [{ selector: 'form-label', imports: [CommonModule], host: { class: 'form-label', }, template: "@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" }] }], ctorParameters: () => [] }); class ContentWrapperComponent { constructor() { this.destroyRef = inject(DestroyRef); this.global = inject(GlobalVariableService); this.hostEventService = inject(HostEventService); this.showErrorsOnTouched = this.global.showErrorsOnTouched; this.config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : [])); this.control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : [])); this.collapsibleState = input(...(ngDevMode ? [undefined, { debugName: "collapsibleState" }] : [])); this.isDirty = signal(false, ...(ngDevMode ? [{ debugName: "isDirty" }] : [])); this.isTouched = signal(false, ...(ngDevMode ? [{ debugName: "isTouched" }] : [])); this.controlErrors = rxResource({ params: () => this.control(), stream: ({ params }) => merge(params.valueChanges, params.statusChanges).pipe(startWith(params.status), map(() => params.errors)), defaultValue: null, }); this.hideErrors = toSignal(this.global.hideErrorMessage$.pipe(tap(() => this.updateControlStatus()))); this.formControlName = computed(() => this.config()?.formControlName ?? '', ...(ngDevMode ? [{ debugName: "formControlName" }] : [])); this.description = computed(() => this.config()?.description, ...(ngDevMode ? [{ debugName: "description" }] : [])); this.descriptionPosition = computed(() => { const position = this.layout()?.descriptionPosition ?? this.global.descriptionPosition; return position; }, ...(ngDevMode ? [{ debugName: "descriptionPosition" }] : [])); this.label = computed(() => this.config()?.label, ...(ngDevMode ? [{ debugName: "label" }] : [])); this.layout = computed(() => this.config()?.layout, ...(ngDevMode ? [{ debugName: "layout" }] : [])); this.props = computed(() => this.config()?.props, ...(ngDevMode ? [{ debugName: "props" }] : [])); this.validators = computed(() => { const { validators, asyncValidators } = this.config() ?? {}; return [...(validators || []), ...(asyncValidators || [])]; }, ...(ngDevMode ? [{ debugName: "validators" }] : [])); this.showLabel = computed(() => this.label() && this.layout()?.hideLabel !== true, ...(ngDevMode ? [{ debugName: "showLabel" }] : [])); this.customErrorComponent = computed(() => { const name = this.formControlName(); const components = this.global.errorComponents; const defaultComponent = this.global.errorComponentDefault; return components?.[name] ?? defaultComponent;