UNPKG

@ngx-formly/core

Version:

Formly is a dynamic (JSON powered) form library for Angular that bring unmatched maintainability to your application's forms.

1 lines 221 kB
{"version":3,"file":"ngx-formly-core.mjs","sources":["../../../../src/core/src/lib/utils.ts","../../../../src/core/src/lib/extensions/field-expression-legacy/utils.ts","../../../../src/core/src/lib/extensions/field-form/utils.ts","../../../../src/core/src/lib/extensions/field-expression-legacy/field-expression.ts","../../../../src/core/src/lib/extensions/field-expression/utils.ts","../../../../src/core/src/lib/extensions/field-expression/field-expression.ts","../../../../src/core/src/lib/extensions/core/core.ts","../../../../src/core/src/lib/extensions/field-form/field-form.ts","../../../../src/core/src/lib/extensions/field-validation/field-validation.ts","../../../../src/core/src/lib/templates/field.type.ts","../../../../src/core/src/lib/templates/field-template.type.ts","../../../../src/core/src/lib/services/formly.config.ts","../../../../src/core/src/lib/components/formly.template.ts","../../../../src/core/src/lib/components/formly.field.ts","../../../../src/core/src/lib/templates/formly.group.ts","../../../../src/core/src/lib/core.config.ts","../../../../src/core/src/lib/services/formly.builder.ts","../../../../src/core/src/lib/components/formly.form.ts","../../../../src/core/src/lib/templates/formly.attributes.ts","../../../../src/core/src/lib/templates/formly.validation-message.ts","../../../../src/core/src/lib/templates/field-array.type.ts","../../../../src/core/src/lib/templates/field.wrapper.ts","../../../../src/core/src/lib/core.module.ts","../../../../src/core/src/public_api.ts","../../../../src/core/src/ngx-formly-core.ts"],"sourcesContent":["import { FormlyFieldConfig } from './models';\nimport { isObservable } from 'rxjs';\nimport { AbstractControl } from '@angular/forms';\nimport { FormlyFieldConfigCache } from './models';\nimport { ChangeDetectorRef, ComponentRef, NgZone, TemplateRef, Type, VERSION, ɵNoopNgZone } from '@angular/core';\n\nexport function disableTreeValidityCall(form: any, callback: () => void) {\n const _updateTreeValidity = form._updateTreeValidity.bind(form);\n form._updateTreeValidity = () => {};\n callback();\n form._updateTreeValidity = _updateTreeValidity;\n}\n\nexport function getFieldId(formId: string, field: FormlyFieldConfig, index: string | number) {\n if (field.id) {\n return field.id;\n }\n let type = field.type;\n if (!type && field.template) {\n type = 'template';\n }\n\n if (type instanceof Type) {\n type = type.prototype.constructor.name;\n }\n\n return [formId, type, field.key, index].join('_');\n}\n\nexport function hasKey(field: FormlyFieldConfig) {\n return !isNil(field.key) && field.key !== '' && (!Array.isArray(field.key) || field.key.length > 0);\n}\n\nexport function getKeyPath(field: FormlyFieldConfigCache): string[] {\n if (!hasKey(field)) {\n return [];\n }\n\n /* We store the keyPath in the field for performance reasons. This function will be called frequently. */\n if (field._keyPath?.key !== field.key) {\n let path: (string | number)[] = [];\n if (typeof field.key === 'string') {\n const key = field.key.indexOf('[') === -1 ? field.key : field.key.replace(/\\[(\\w+)\\]/g, '.$1');\n path = key.indexOf('.') !== -1 ? key.split('.') : [key];\n } else if (Array.isArray(field.key)) {\n path = field.key.slice(0);\n } else {\n path = [`${field.key}`];\n }\n\n defineHiddenProp(field, '_keyPath', { key: field.key, path });\n }\n\n return field._keyPath.path.slice(0);\n}\n\nexport const FORMLY_VALIDATORS = ['required', 'pattern', 'minLength', 'maxLength', 'min', 'max'];\n\nexport function assignFieldValue(field: FormlyFieldConfigCache, value: any) {\n let paths = getKeyPath(field);\n if (paths.length === 0) {\n return;\n }\n\n let root = field;\n while (root.parent) {\n root = root.parent;\n paths = [...getKeyPath(root), ...paths];\n }\n\n if (value === undefined && field.resetOnHide) {\n const k = paths.pop();\n const m = paths.reduce((model, path) => model[path] || {}, root.model);\n delete m[k];\n return;\n }\n\n assignModelValue(root.model, paths, value);\n}\n\nexport function assignModelValue(model: any, paths: string[], value: any) {\n for (let i = 0; i < paths.length - 1; i++) {\n const path = paths[i];\n if (!model[path] || !isObject(model[path])) {\n model[path] = /^\\d+$/.test(paths[i + 1]) ? [] : {};\n }\n\n model = model[path];\n }\n\n model[paths[paths.length - 1]] = clone(value);\n}\n\nexport function getFieldValue(field: FormlyFieldConfig): any {\n let model = field.parent ? field.parent.model : field.model;\n for (const path of getKeyPath(field)) {\n if (!model) {\n return model;\n }\n model = model[path];\n }\n\n return model;\n}\n\nexport function reverseDeepMerge(dest: any, ...args: any[]) {\n args.forEach((src) => {\n for (const srcArg in src) {\n if (isNil(dest[srcArg]) || isBlankString(dest[srcArg])) {\n dest[srcArg] = clone(src[srcArg]);\n } else if (objAndSameType(dest[srcArg], src[srcArg])) {\n reverseDeepMerge(dest[srcArg], src[srcArg]);\n }\n }\n });\n return dest;\n}\n\n// check a value is null or undefined\nexport function isNil(value: any) {\n return value == null;\n}\n\nexport function isUndefined(value: any) {\n return value === undefined;\n}\n\nexport function isBlankString(value: any) {\n return value === '';\n}\n\nexport function isFunction(value: any) {\n return typeof value === 'function';\n}\n\nexport function objAndSameType(obj1: any, obj2: any) {\n return (\n isObject(obj1) &&\n isObject(obj2) &&\n Object.getPrototypeOf(obj1) === Object.getPrototypeOf(obj2) &&\n !(Array.isArray(obj1) || Array.isArray(obj2))\n );\n}\n\nexport function isObject(x: any) {\n return x != null && typeof x === 'object';\n}\n\nexport function isPromise(obj: any): obj is Promise<any> {\n return !!obj && typeof obj.then === 'function';\n}\n\nexport function clone(value: any): any {\n if (\n !isObject(value) ||\n isObservable(value) ||\n isPromise(value) ||\n value instanceof TemplateRef ||\n /* instanceof SafeHtmlImpl */ value.changingThisBreaksApplicationSecurity ||\n ['RegExp', 'FileList', 'File', 'Blob'].indexOf(value.constructor?.name) !== -1\n ) {\n return value;\n }\n\n if (value instanceof Set) {\n return new Set(value);\n }\n\n if (value instanceof Map) {\n return new Map(value);\n }\n\n if (value instanceof Uint8Array) {\n return new Uint8Array(value);\n }\n\n if (value instanceof Uint16Array) {\n return new Uint16Array(value);\n }\n\n if (value instanceof Uint32Array) {\n return new Uint32Array(value);\n }\n\n // https://github.com/moment/moment/blob/master/moment.js#L252\n if (value._isAMomentObject && isFunction(value.clone)) {\n return value.clone();\n }\n\n if (value instanceof AbstractControl) {\n return null;\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime());\n }\n\n if (Array.isArray(value)) {\n return value.slice(0).map((v) => clone(v));\n }\n\n // best way to clone a js object maybe\n // https://stackoverflow.com/questions/41474986/how-to-clone-a-javascript-es6-class-instance\n const proto = Object.getPrototypeOf(value);\n let c = Object.create(proto);\n c = Object.setPrototypeOf(c, proto);\n // need to make a deep copy so we dont use Object.assign\n // also Object.assign wont copy property descriptor exactly\n return Object.keys(value).reduce((newVal, prop) => {\n const propDesc = Object.getOwnPropertyDescriptor(value, prop);\n if (propDesc.get) {\n Object.defineProperty(newVal, prop, propDesc);\n } else {\n newVal[prop] = clone(value[prop]);\n }\n\n return newVal;\n }, c);\n}\n\nexport function defineHiddenProp(field: any, prop: string, defaultValue: any) {\n Object.defineProperty(field, prop, { enumerable: false, writable: true, configurable: true });\n field[prop] = defaultValue;\n}\n\ntype IObserveFn<T> = (change: { currentValue: T; previousValue?: T; firstChange: boolean }) => void;\nexport interface IObserver<T> {\n setValue: (value: T, emitEvent?: boolean) => void;\n unsubscribe: () => void;\n}\ninterface IObserveTarget<T> {\n [prop: string]: any;\n _observers?: {\n [prop: string]: {\n value: T;\n onChange: IObserveFn<T>[];\n };\n };\n}\n\nexport function observeDeep<T = any>(source: IObserveTarget<T>, paths: string[], setFn: () => void): () => void {\n let observers: (() => void)[] = [];\n\n const unsubscribe = () => {\n observers.forEach((observer) => observer());\n observers = [];\n };\n const observer = observe(source, paths, ({ firstChange, currentValue }) => {\n !firstChange && setFn();\n\n unsubscribe();\n if (isObject(currentValue) && currentValue.constructor.name === 'Object') {\n Object.keys(currentValue).forEach((prop) => {\n observers.push(observeDeep(source, [...paths, prop], setFn));\n });\n }\n });\n\n return () => {\n observer.unsubscribe();\n unsubscribe();\n };\n}\n\nexport function observe<T = any>(o: IObserveTarget<T>, paths: string[], setFn?: IObserveFn<T>): IObserver<T> {\n if (!o._observers) {\n defineHiddenProp(o, '_observers', {});\n }\n\n let target = o;\n for (let i = 0; i < paths.length - 1; i++) {\n if (!target[paths[i]] || !isObject(target[paths[i]])) {\n target[paths[i]] = /^\\d+$/.test(paths[i + 1]) ? [] : {};\n }\n target = target[paths[i]];\n }\n\n const key = paths[paths.length - 1];\n const prop = paths.join('.');\n if (!o._observers[prop]) {\n o._observers[prop] = { value: target[key], onChange: [] };\n }\n\n const state = o._observers[prop];\n if (target[key] !== state.value) {\n state.value = target[key];\n }\n\n if (setFn && state.onChange.indexOf(setFn) === -1) {\n state.onChange.push(setFn);\n setFn({ currentValue: state.value, firstChange: true });\n if (state.onChange.length >= 1 && isObject(target)) {\n const { enumerable } = Object.getOwnPropertyDescriptor(target, key) || { enumerable: true };\n Object.defineProperty(target, key, {\n enumerable,\n configurable: true,\n get: () => state.value,\n set: (currentValue) => {\n if (currentValue !== state.value) {\n const previousValue = state.value;\n state.value = currentValue;\n state.onChange.forEach((changeFn) => changeFn({ previousValue, currentValue, firstChange: false }));\n }\n },\n });\n }\n }\n\n return {\n setValue(currentValue: T, emitEvent = true) {\n if (currentValue === state.value) {\n return;\n }\n\n const previousValue = state.value;\n state.value = currentValue;\n state.onChange.forEach((changeFn) => {\n if (changeFn !== setFn && emitEvent) {\n changeFn({ previousValue, currentValue, firstChange: false });\n }\n });\n },\n unsubscribe() {\n state.onChange = state.onChange.filter((changeFn) => changeFn !== setFn);\n if (state.onChange.length === 0) {\n delete o._observers[prop];\n }\n },\n };\n}\n\nexport function getField(f: FormlyFieldConfig, key: FormlyFieldConfig['key']): FormlyFieldConfig {\n key = (Array.isArray(key) ? key.join('.') : key) as string;\n if (!f.fieldGroup) {\n return undefined;\n }\n\n for (let i = 0, len = f.fieldGroup.length; i < len; i++) {\n const c = f.fieldGroup[i];\n const k = (Array.isArray(c.key) ? c.key.join('.') : c.key) as string;\n if (k === key) {\n return c;\n }\n\n if (c.fieldGroup && (isNil(k) || key.indexOf(`${k}.`) === 0)) {\n const field = getField(c, isNil(k) ? key : key.slice(k.length + 1));\n if (field) {\n return field;\n }\n }\n }\n\n return undefined;\n}\n\nexport function markFieldForCheck(field: FormlyFieldConfigCache) {\n field._componentRefs?.forEach((ref) => {\n // NOTE: we cannot use ref.changeDetectorRef, see https://github.com/ngx-formly/ngx-formly/issues/2191\n if (ref instanceof ComponentRef) {\n const changeDetectorRef = ref.injector.get(ChangeDetectorRef);\n changeDetectorRef.markForCheck();\n } else {\n ref.markForCheck();\n }\n });\n}\n\nexport function isNoopNgZone(ngZone: NgZone) {\n return ngZone instanceof ɵNoopNgZone;\n}\n\nexport function isHiddenField(field: FormlyFieldConfig) {\n const isHidden = (f: FormlyFieldConfig) => f.hide || f.expressions?.hide || f.hideExpression;\n let setDefaultValue = !field.resetOnHide || !isHidden(field);\n if (!isHidden(field) && field.resetOnHide) {\n let parent = field.parent;\n while (parent && !isHidden(parent)) {\n parent = parent.parent;\n }\n setDefaultValue = !parent || !isHidden(parent);\n }\n\n return !setDefaultValue;\n}\n\nexport function isSignalRequired() {\n return +VERSION.major > 18 || (+VERSION.major >= 18 && +VERSION.minor >= 1);\n}\n","/**\n * Legacy implementation\n */\nexport function evalStringExpressionLegacy(expression: string, argNames: string[]): any {\n try {\n return Function(...argNames, `return ${expression};`) as any;\n } catch (error) {\n console.error(error);\n }\n}\n\nexport function evalExpression(\n expression: string | ((...value: any[]) => any) | boolean,\n thisArg: any,\n argVal: any[],\n): any {\n if (typeof expression === 'function') {\n return expression.apply(thisArg, argVal);\n } else {\n return expression ? true : false;\n }\n}\n","import { EventEmitter } from '@angular/core';\nimport { UntypedFormArray, UntypedFormGroup, AbstractControl, FormControl } from '@angular/forms';\nimport { getKeyPath, getFieldValue, isNil, defineHiddenProp, observe, hasKey } from '../../utils';\nimport { FormlyFieldConfigCache } from '../../models';\n\nexport function unregisterControl(field: FormlyFieldConfigCache, emitEvent = false) {\n const control = field.formControl;\n const fieldIndex = control._fields ? control._fields.indexOf(field) : -1;\n if (fieldIndex !== -1) {\n control._fields.splice(fieldIndex, 1);\n }\n\n const form = control.parent as UntypedFormArray | UntypedFormGroup;\n if (!form) {\n return;\n }\n\n const opts = { emitEvent };\n if (form instanceof UntypedFormArray) {\n const key = form.controls.findIndex((c) => c === control);\n if (key !== -1) {\n form.removeAt(key, opts);\n }\n } else if (form instanceof UntypedFormGroup) {\n const paths = getKeyPath(field);\n const key = paths[paths.length - 1];\n if (form.get([key]) === control) {\n form.removeControl(key, opts);\n }\n }\n\n control.setParent(null);\n}\n\nexport function findControl(field: FormlyFieldConfigCache): AbstractControl {\n if (field.formControl) {\n return field.formControl;\n }\n\n if (field.shareFormControl === false) {\n return null;\n }\n\n return field.form?.get(getKeyPath(field));\n}\n\nexport function registerControl(\n field: FormlyFieldConfigCache,\n control?: FormlyFieldConfigCache['formControl'],\n emitEvent = false,\n) {\n control = control || field.formControl;\n\n if (!control._fields) {\n defineHiddenProp(control, '_fields', []);\n }\n if (control._fields.indexOf(field) === -1) {\n control._fields.push(field);\n }\n\n if (!field.formControl && control) {\n defineHiddenProp(field, 'formControl', control);\n control.setValidators(null);\n control.setAsyncValidators(null);\n\n field.props.disabled = !!field.props.disabled;\n const disabledObserver = observe(field, ['props', 'disabled'], ({ firstChange, currentValue }) => {\n if (!firstChange) {\n currentValue ? field.formControl.disable() : field.formControl.enable();\n }\n });\n if (control instanceof FormControl) {\n control.registerOnDisabledChange(disabledObserver.setValue);\n }\n }\n\n if (!field.form || !hasKey(field)) {\n return;\n }\n\n let form = field.form;\n const paths = getKeyPath(field);\n const value = getFieldValue(field);\n if (!(isNil(control.value) && isNil(value)) && control.value !== value && control instanceof FormControl) {\n control.patchValue(value);\n }\n\n for (let i = 0; i < paths.length - 1; i++) {\n const path = paths[i];\n if (!form.get([path])) {\n (form as UntypedFormGroup).setControl(path, new UntypedFormGroup({}), { emitEvent });\n }\n\n form = <UntypedFormGroup>form.get([path]);\n }\n\n const key = paths[paths.length - 1];\n if (!field._hide && form.get([key]) !== control) {\n (form as UntypedFormGroup).setControl(key, control, { emitEvent });\n }\n}\n\nexport function updateValidity(c: AbstractControl, onlySelf = false) {\n const status = c.status;\n const value = c.value;\n c.updateValueAndValidity({ emitEvent: false, onlySelf });\n if (status !== c.status) {\n (c.statusChanges as EventEmitter<string>).emit(c.status);\n }\n\n if (value !== c.value) {\n (c.valueChanges as EventEmitter<any>).emit(c.value);\n }\n}\n\nexport function clearControl(form: FormlyFieldConfigCache['formControl']) {\n delete form?._fields;\n form.setValidators(null);\n form.setAsyncValidators(null);\n if (form instanceof UntypedFormGroup || form instanceof UntypedFormArray) {\n Object.values(form.controls).forEach((c) => clearControl(c));\n }\n}\n","import { FormlyValueChangeEvent, FormlyFieldConfigCache } from '../../models';\nimport {\n isObject,\n isNil,\n isUndefined,\n isFunction,\n defineHiddenProp,\n observe,\n getFieldValue,\n assignFieldValue,\n hasKey,\n} from '../../utils';\nimport { evalExpression, evalStringExpressionLegacy } from './utils';\nimport { isObservable, Observable } from 'rxjs';\nimport { tap } from 'rxjs/operators';\nimport { FormlyExtension } from '../../models';\nimport { unregisterControl, registerControl, updateValidity } from '../field-form/utils';\nimport { UntypedFormArray } from '@angular/forms';\n\nexport class FieldExpressionExtension implements FormlyExtension {\n onPopulate(field: FormlyFieldConfigCache) {\n if (field._expressions) {\n return;\n }\n\n // cache built expression\n defineHiddenProp(field, '_expressions', {});\n\n observe(field, ['hide'], ({ currentValue, firstChange }) => {\n defineHiddenProp(field, '_hide', !!currentValue);\n if (!firstChange || (firstChange && currentValue === true)) {\n field.props.hidden = currentValue;\n field.options._hiddenFieldsForCheck.push({ field });\n }\n });\n\n if (field.hideExpression) {\n observe(field, ['hideExpression'], ({ currentValue: expr }) => {\n field._expressions.hide = this.parseExpressions(field, 'hide', typeof expr === 'boolean' ? () => expr : expr);\n });\n }\n\n const evalExpr = (key: string, expr: any) => {\n if (typeof expr === 'string' || isFunction(expr)) {\n field._expressions[key] = this.parseExpressions(field, key, expr);\n } else if (expr instanceof Observable) {\n field._expressions[key] = {\n value$: (expr as Observable<any>).pipe(\n tap((v) => {\n this.evalExpr(field, key, v);\n field.options._detectChanges(field);\n }),\n ),\n };\n }\n };\n\n field.expressions = field.expressions || {};\n for (const key of Object.keys(field.expressions)) {\n observe(field, ['expressions', key], ({ currentValue: expr }) => {\n evalExpr(key, isFunction(expr) ? (...args: any) => expr(field, args[3]) : expr);\n });\n }\n\n field.expressionProperties = field.expressionProperties || {};\n for (const key of Object.keys(field.expressionProperties)) {\n observe(field, ['expressionProperties', key], ({ currentValue }) => evalExpr(key, currentValue));\n }\n }\n\n postPopulate(field: FormlyFieldConfigCache) {\n if (field.parent) {\n return;\n }\n\n if (!field.options.checkExpressions) {\n let checkLocked = false;\n field.options.checkExpressions = (f, ignoreCache) => {\n if (checkLocked) {\n return;\n }\n\n checkLocked = true;\n const fieldChanged = this.checkExpressions(f, ignoreCache);\n const options = field.options;\n options._hiddenFieldsForCheck\n .sort((f) => (f.field.hide ? -1 : 1))\n .forEach((f) => this.changeHideState(f.field, f.field.hide ?? f.default, !ignoreCache));\n options._hiddenFieldsForCheck = [];\n if (fieldChanged) {\n this.checkExpressions(field);\n }\n checkLocked = false;\n };\n }\n }\n\n protected parseExpressions(field: FormlyFieldConfigCache, path: string, expr: any) {\n let parentExpression: any;\n if (field.parent && ['hide', 'props.disabled'].includes(path)) {\n const rootValue = (f: FormlyFieldConfigCache) => {\n return path === 'hide' ? f.hide : f.props.disabled;\n };\n\n parentExpression = () => {\n let root = field.parent;\n while (root.parent && !rootValue(root)) {\n root = root.parent;\n }\n\n return rootValue(root);\n };\n }\n\n expr = expr || (() => false);\n if (typeof expr === 'string') {\n expr = this._evalStringExpression(expr, ['model', 'formState', 'field']);\n }\n\n let currentValue: any;\n\n return {\n callback: (ignoreCache?: boolean) => {\n try {\n const exprValue = evalExpression(\n parentExpression ? (...args: any) => parentExpression(field) || expr(...args) : expr,\n { field },\n [field.model, field.options.formState, field, ignoreCache],\n );\n\n if (\n ignoreCache ||\n (currentValue !== exprValue &&\n (!isObject(exprValue) ||\n isObservable(exprValue) ||\n JSON.stringify(exprValue) !== JSON.stringify(currentValue)))\n ) {\n currentValue = exprValue;\n this.evalExpr(field, path, exprValue);\n\n return true;\n }\n\n return false;\n } catch (error: any) {\n error.message = `[Formly Error] [Expression \"${path}\"] ${error.message}`;\n throw error;\n }\n },\n };\n }\n\n protected _evalStringExpression(expression: string, argNames: string[]) {\n return evalStringExpressionLegacy(expression, argNames);\n }\n\n private checkExpressions(field: FormlyFieldConfigCache, ignoreCache = false) {\n if (!field) {\n return false;\n }\n\n let fieldChanged = false;\n if (field._expressions) {\n for (const key of Object.keys(field._expressions)) {\n field._expressions[key].callback?.(ignoreCache) && (fieldChanged = true);\n }\n }\n field.fieldGroup?.forEach((f) => this.checkExpressions(f, ignoreCache) && (fieldChanged = true));\n\n return fieldChanged;\n }\n\n private changeDisabledState(field: FormlyFieldConfigCache, value: boolean) {\n if (field.fieldGroup) {\n field.fieldGroup\n .filter((f: FormlyFieldConfigCache) => !f._expressions.hasOwnProperty('props.disabled'))\n .forEach((f) => this.changeDisabledState(f, value));\n }\n\n if (hasKey(field) && field.props.disabled !== value) {\n field.props.disabled = value;\n }\n }\n\n private changeHideState(field: FormlyFieldConfigCache, hide: boolean, resetOnHide: boolean) {\n if (field.fieldGroup) {\n field.fieldGroup\n .filter((f: FormlyFieldConfigCache) => f && !f._expressions.hide)\n .forEach((f) => this.changeHideState(f, hide, resetOnHide));\n }\n\n if (field.formControl && hasKey(field)) {\n defineHiddenProp(field, '_hide', !!(hide || field.hide));\n const c = field.formControl;\n if (c._fields?.length > 1) {\n updateValidity(c);\n }\n\n if (hide === true && (!c._fields || c._fields.every((f) => !!f._hide))) {\n unregisterControl(field, true);\n if (resetOnHide && field.resetOnHide) {\n assignFieldValue(field, undefined);\n field.formControl.reset({ value: undefined, disabled: field.formControl.disabled });\n field.options.fieldChanges.next({ value: undefined, field, type: 'valueChanges' });\n if (field.fieldGroup && field.formControl instanceof UntypedFormArray) {\n field.fieldGroup.length = 0;\n }\n }\n } else if (hide === false) {\n if (field.resetOnHide && !isUndefined(field.defaultValue) && isUndefined(getFieldValue(field))) {\n assignFieldValue(field, field.defaultValue);\n }\n registerControl(field, undefined, true);\n if (field.resetOnHide && field.fieldArray && field.fieldGroup?.length !== field.model?.length) {\n field.options.build(field);\n }\n }\n }\n\n if (field.options.fieldChanges) {\n field.options.fieldChanges.next(<FormlyValueChangeEvent>{ field, type: 'hidden', value: hide });\n }\n }\n\n private evalExpr(field: FormlyFieldConfigCache, prop: string, value: any) {\n if (prop.indexOf('model.') === 0) {\n const key = prop.replace(/^model\\./, ''),\n parent = field.fieldGroup ? field : field.parent;\n\n let control = field?.key === key ? field.formControl : field.form.get(key);\n if (!control && field.get(key)) {\n control = field.get(key).formControl;\n }\n assignFieldValue({ key, parent, model: field.model }, value);\n if (control && !(isNil(control.value) && isNil(value)) && control.value !== value) {\n control.patchValue(value);\n }\n } else {\n try {\n let target: any = field;\n const paths = this._evalExpressionPath(field, prop);\n const lastIndex = paths.length - 1;\n for (let i = 0; i < lastIndex; i++) {\n target = target[paths[i]];\n }\n\n target[paths[lastIndex]] = value;\n } catch (error: any) {\n error.message = `[Formly Error] [Expression \"${prop}\"] ${error.message}`;\n throw error;\n }\n\n if (['templateOptions.disabled', 'props.disabled'].includes(prop) && hasKey(field)) {\n this.changeDisabledState(field, value);\n }\n }\n\n this.emitExpressionChanges(field, prop, value);\n }\n\n private emitExpressionChanges(field: FormlyFieldConfigCache, property: string, value: any) {\n if (!field.options.fieldChanges) {\n return;\n }\n\n field.options.fieldChanges.next({\n field,\n type: 'expressionChanges',\n property,\n value,\n });\n }\n\n private _evalExpressionPath(field: FormlyFieldConfigCache, prop: string) {\n if (field._expressions[prop] && field._expressions[prop].paths) {\n return field._expressions[prop].paths;\n }\n\n let paths: string[] = [];\n if (prop.indexOf('[') === -1) {\n paths = prop.split('.');\n } else {\n prop\n .split(/[[\\]]{1,2}/) // https://stackoverflow.com/a/20198206\n .filter((p) => p)\n .forEach((path) => {\n const arrayPath = path.match(/['|\"](.*?)['|\"]/);\n if (arrayPath) {\n paths.push(arrayPath[1]);\n } else {\n paths.push(...path.split('.').filter((p) => p));\n }\n });\n }\n\n if (field._expressions[prop]) {\n field._expressions[prop].paths = paths;\n }\n\n return paths;\n }\n}\n","// CSP-compliant expression parser for Formly expressions\n// Supports: model.path, formState.path, field.path, comparisons, logical operators, and negation\n\ntype ExpressionContext = {\n model: any;\n formState: any;\n field: any;\n};\n\n// Tokenizer\nenum TokenType {\n IDENTIFIER = 'IDENTIFIER',\n DOT = 'DOT',\n BRACKET_OPEN = 'BRACKET_OPEN',\n BRACKET_CLOSE = 'BRACKET_CLOSE',\n STRING = 'STRING',\n NUMBER = 'NUMBER',\n BOOLEAN = 'BOOLEAN',\n NULL = 'NULL',\n UNDEFINED = 'UNDEFINED',\n OPERATOR = 'OPERATOR',\n LOGICAL = 'LOGICAL',\n NOT = 'NOT',\n ARITHMETIC = 'ARITHMETIC',\n PAREN_OPEN = 'PAREN_OPEN',\n PAREN_CLOSE = 'PAREN_CLOSE',\n TERNARY_QUESTION = 'TERNARY_QUESTION',\n TERNARY_COLON = 'TERNARY_COLON',\n EOF = 'EOF',\n}\n\ninterface Token {\n type: TokenType;\n value: any;\n}\n\nclass Tokenizer {\n private pos = 0;\n private input: string;\n\n constructor(input: string) {\n this.input = input.trim();\n }\n\n tokenize(): Token[] {\n const tokens: Token[] = [];\n\n while (this.pos < this.input.length) {\n this.skipWhitespace();\n\n if (this.pos >= this.input.length) break;\n\n const char = this.input[this.pos];\n\n // String literals\n if (char === '\"' || char === \"'\") {\n tokens.push(this.readString());\n }\n // Numbers\n else if (/\\d/.test(char)) {\n tokens.push(this.readNumber());\n }\n // Identifiers and keywords\n else if (/[a-zA-Z_$]/.test(char)) {\n tokens.push(this.readIdentifier());\n }\n // Operators and symbols\n else if (char === '.') {\n tokens.push({ type: TokenType.DOT, value: '.' });\n this.pos++;\n } else if (char === '[') {\n tokens.push({ type: TokenType.BRACKET_OPEN, value: '[' });\n this.pos++;\n } else if (char === ']') {\n tokens.push({ type: TokenType.BRACKET_CLOSE, value: ']' });\n this.pos++;\n } else if (char === '(') {\n tokens.push({ type: TokenType.PAREN_OPEN, value: '(' });\n this.pos++;\n } else if (char === ')') {\n tokens.push({ type: TokenType.PAREN_CLOSE, value: ')' });\n this.pos++;\n } else if (char === '?') {\n tokens.push({ type: TokenType.TERNARY_QUESTION, value: '?' });\n this.pos++;\n } else if (char === ':') {\n tokens.push({ type: TokenType.TERNARY_COLON, value: ':' });\n this.pos++;\n } else if (char === '!') {\n if (this.peek() === '=') {\n if (this.input[this.pos + 2] === '=') {\n tokens.push({ type: TokenType.OPERATOR, value: '!==' });\n this.pos += 3;\n } else {\n tokens.push({ type: TokenType.OPERATOR, value: '!=' });\n this.pos += 2;\n }\n } else {\n tokens.push({ type: TokenType.NOT, value: '!' });\n this.pos++;\n }\n } else if (char === '=' || char === '<' || char === '>') {\n tokens.push(this.readOperator());\n } else if (char === '+' || char === '*' || char === '/' || char === '%') {\n tokens.push({ type: TokenType.ARITHMETIC, value: char });\n this.pos++;\n } else if (char === '-') {\n // Check if this is a negative number or subtraction\n // It's a negative number if preceded by an operator, opening paren, or at the start\n const lastToken = tokens[tokens.length - 1];\n const isNegativeNumber =\n tokens.length === 0 ||\n lastToken?.type === TokenType.OPERATOR ||\n lastToken?.type === TokenType.LOGICAL ||\n lastToken?.type === TokenType.ARITHMETIC ||\n lastToken?.type === TokenType.PAREN_OPEN ||\n lastToken?.type === TokenType.TERNARY_QUESTION ||\n lastToken?.type === TokenType.TERNARY_COLON;\n\n if (isNegativeNumber && this.peek() && /\\d/.test(this.peek())) {\n tokens.push(this.readNumber());\n } else {\n tokens.push({ type: TokenType.ARITHMETIC, value: '-' });\n this.pos++;\n }\n } else if (char === '&' && this.peek() === '&') {\n tokens.push({ type: TokenType.LOGICAL, value: '&&' });\n this.pos += 2;\n } else if (char === '|' && this.peek() === '|') {\n tokens.push({ type: TokenType.LOGICAL, value: '||' });\n this.pos += 2;\n } else {\n throw new Error(`Unexpected character: ${char} at position ${this.pos}`);\n }\n }\n\n tokens.push({ type: TokenType.EOF, value: null });\n return tokens;\n }\n\n private skipWhitespace(): void {\n while (this.pos < this.input.length && /\\s/.test(this.input[this.pos])) {\n this.pos++;\n }\n }\n\n private peek(offset = 1): string {\n return this.input[this.pos + offset] || '';\n }\n\n private readString(): Token {\n const quote = this.input[this.pos];\n this.pos++;\n let value = '';\n\n while (this.pos < this.input.length && this.input[this.pos] !== quote) {\n if (this.input[this.pos] === '\\\\') {\n this.pos++;\n if (this.pos < this.input.length) {\n const escaped = this.input[this.pos];\n switch (escaped) {\n case 'n':\n value += '\\n';\n break;\n case 't':\n value += '\\t';\n break;\n case 'r':\n value += '\\r';\n break;\n default:\n value += escaped;\n }\n }\n } else {\n value += this.input[this.pos];\n }\n this.pos++;\n }\n\n this.pos++; // skip closing quote\n return { type: TokenType.STRING, value };\n }\n\n private readNumber(): Token {\n let value = '';\n\n // Handle negatives\n if (this.input[this.pos] === '-') {\n value += '-';\n this.pos++;\n }\n\n let hasDecimal = false;\n while (this.pos < this.input.length && /[\\d.]/.test(this.input[this.pos])) {\n if (this.input[this.pos] === '.') {\n if (hasDecimal) {\n throw new Error(`Invalid number format: multiple decimal points at position ${this.pos}`);\n }\n hasDecimal = true;\n }\n value += this.input[this.pos];\n this.pos++;\n }\n\n const parsed = parseFloat(value);\n if (isNaN(parsed)) {\n throw new Error(`Invalid number format: ${value}`);\n }\n\n return { type: TokenType.NUMBER, value: parsed };\n }\n\n private readIdentifier(): Token {\n let value = '';\n\n while (this.pos < this.input.length && /[a-zA-Z0-9_$]/.test(this.input[this.pos])) {\n value += this.input[this.pos];\n this.pos++;\n }\n\n // Check for keywords\n if (value === 'true' || value === 'false') {\n return { type: TokenType.BOOLEAN, value: value === 'true' };\n }\n if (value === 'null') {\n return { type: TokenType.NULL, value: null };\n }\n if (value === 'undefined') {\n return { type: TokenType.UNDEFINED, value: undefined };\n }\n\n return { type: TokenType.IDENTIFIER, value };\n }\n\n private readOperator(): Token {\n let op = this.input[this.pos];\n this.pos++;\n\n if (this.pos < this.input.length) {\n const next = this.input[this.pos];\n if (\n (op === '=' && next === '=') ||\n (op === '!' && next === '=') ||\n (op === '<' && next === '=') ||\n (op === '>' && next === '=')\n ) {\n op += next;\n this.pos++;\n\n // Check for === or !==\n if (this.pos < this.input.length && this.input[this.pos] === '=') {\n op += '=';\n this.pos++;\n }\n }\n }\n\n return { type: TokenType.OPERATOR, value: op };\n }\n}\n\n// Parser and Evaluator\nclass ExpressionParser {\n private tokens: Token[];\n private pos = 0;\n\n constructor(tokens: Token[]) {\n this.tokens = tokens;\n }\n\n parse(): (context: ExpressionContext) => any {\n const expr = this.parseTernary();\n return (context: ExpressionContext) => expr(context);\n }\n\n private parseTernary(): (context: ExpressionContext) => any {\n const expr = this.parseLogicalOr();\n\n if (this.current().type === TokenType.TERNARY_QUESTION) {\n this.consume(TokenType.TERNARY_QUESTION);\n const trueExpr = this.parseLogicalOr();\n this.consume(TokenType.TERNARY_COLON);\n const falseExpr = this.parseTernary();\n\n return (context: ExpressionContext) => {\n return expr(context) ? trueExpr(context) : falseExpr(context);\n };\n }\n\n return expr;\n }\n\n private parseLogicalOr(): (context: ExpressionContext) => any {\n let left = this.parseLogicalAnd();\n\n while (this.current().type === TokenType.LOGICAL && this.current().value === '||') {\n this.consume(TokenType.LOGICAL);\n const right = this.parseLogicalAnd();\n const prevLeft = left;\n left = (context: ExpressionContext) => prevLeft(context) || right(context);\n }\n\n return left;\n }\n\n private parseLogicalAnd(): (context: ExpressionContext) => any {\n let left = this.parseComparison();\n\n while (this.current().type === TokenType.LOGICAL && this.current().value === '&&') {\n this.consume(TokenType.LOGICAL);\n const right = this.parseComparison();\n const prevLeft = left;\n left = (context: ExpressionContext) => prevLeft(context) && right(context);\n }\n\n return left;\n }\n\n private parseComparison(): (context: ExpressionContext) => any {\n const left = this.parseAdditive();\n\n if (this.current().type === TokenType.OPERATOR) {\n const op = this.consume(TokenType.OPERATOR).value;\n const right = this.parseAdditive();\n\n return (context: ExpressionContext) => {\n const leftVal = left(context);\n const rightVal = right(context);\n\n switch (op) {\n case '===':\n return leftVal === rightVal;\n case '!==':\n return leftVal !== rightVal;\n case '==':\n return leftVal == rightVal;\n case '!=':\n return leftVal != rightVal;\n case '<':\n return leftVal < rightVal;\n case '<=':\n return leftVal <= rightVal;\n case '>':\n return leftVal > rightVal;\n case '>=':\n return leftVal >= rightVal;\n default:\n throw new Error(`Unknown operator: ${op}`);\n }\n };\n }\n\n return left;\n }\n\n private parseAdditive(): (context: ExpressionContext) => any {\n let left = this.parseMultiplicative();\n\n while (\n this.current().type === TokenType.ARITHMETIC &&\n (this.current().value === '+' || this.current().value === '-')\n ) {\n const op = this.consume(TokenType.ARITHMETIC).value;\n const right = this.parseMultiplicative();\n const prevLeft = left;\n\n if (op === '+') {\n left = (context: ExpressionContext) => prevLeft(context) + right(context);\n } else {\n left = (context: ExpressionContext) => prevLeft(context) - right(context);\n }\n }\n\n return left;\n }\n\n private parseMultiplicative(): (context: ExpressionContext) => any {\n let left = this.parseUnary();\n\n while (\n this.current().type === TokenType.ARITHMETIC &&\n (this.current().value === '*' || this.current().value === '/' || this.current().value === '%')\n ) {\n const op = this.consume(TokenType.ARITHMETIC).value;\n const right = this.parseUnary();\n const prevLeft = left;\n\n if (op === '*') {\n left = (context: ExpressionContext) => prevLeft(context) * right(context);\n } else if (op === '/') {\n left = (context: ExpressionContext) => prevLeft(context) / right(context);\n } else {\n left = (context: ExpressionContext) => prevLeft(context) % right(context);\n }\n }\n\n return left;\n }\n\n private parseUnary(): (context: ExpressionContext) => any {\n if (this.current().type === TokenType.NOT) {\n this.consume(TokenType.NOT);\n const expr = this.parseUnary();\n return (context: ExpressionContext) => !expr(context);\n }\n\n return this.parsePrimary();\n }\n\n private parsePrimary(): (context: ExpressionContext) => any {\n const token = this.current();\n\n // Parentheses\n if (token.type === TokenType.PAREN_OPEN) {\n this.consume(TokenType.PAREN_OPEN);\n const expr = this.parseTernary();\n this.consume(TokenType.PAREN_CLOSE);\n return expr;\n }\n\n // Literals\n if (\n token.type === TokenType.STRING ||\n token.type === TokenType.NUMBER ||\n token.type === TokenType.BOOLEAN ||\n token.type === TokenType.NULL ||\n token.type === TokenType.UNDEFINED\n ) {\n const value = token.value;\n this.pos++;\n return () => value;\n }\n\n // Property access (model.field, formState.prop, etc)\n if (token.type === TokenType.IDENTIFIER) {\n return this.parsePropertyAccess();\n }\n\n throw new Error(`Unexpected token: ${JSON.stringify(token)}`);\n }\n\n private parsePropertyAccess(): (context: ExpressionContext) => any {\n const path: Array<string | ((context: ExpressionContext) => any)> = [];\n\n // Read first identifier\n path.push(this.consume(TokenType.IDENTIFIER).value);\n\n // Read rest of path (dots and brackets)\n while (this.current().type === TokenType.DOT || this.current().type === TokenType.BRACKET_OPEN) {\n if (this.current().type === TokenType.DOT) {\n this.consume(TokenType.DOT);\n path.push(this.consume(TokenType.IDENTIFIER).value);\n } else {\n this.consume(TokenType.BRACKET_OPEN);\n\n // bracket notation can contain expressions\n if (this.current().type === TokenType.STRING) {\n path.push(this.consume(TokenType.STRING).value);\n } else if (this.current().type === TokenType.NUMBER) {\n path.push(String(this.consume(TokenType.NUMBER).value));\n } else {\n // dynamic property access\n const expr = this.parseTernary();\n path.push(expr);\n }\n\n this.consume(TokenType.BRACKET_CLOSE);\n }\n }\n\n return (context: ExpressionContext) => {\n let value: any = context;\n\n for (const segment of path) {\n if (value === null || value === undefined) {\n return undefined;\n }\n\n if (typeof segment === 'function') {\n value = value[segment(context)];\n } else {\n value = value[segment];\n }\n }\n\n return value;\n };\n }\n\n private current(): Token {\n return this.tokens[this.pos];\n }\n\n private consume(expectedType: TokenType): Token {\n const token = this.current();\n\n if (token.type !== expectedType) {\n throw new Error(`Expected ${expectedType}, got ${token.type}`);\n }\n\n this.pos++;\n return token;\n }\n}\n\n/**\n * Uses CSP-safe implementation\n */\nexport function parseExpression(expression: string, argNames: string[]): any {\n try {\n const tokenizer = new Tokenizer(expression);\n const tokens = tokenizer.tokenize();\n const parser = new ExpressionParser(tokens);\n const evaluator = parser.parse();\n\n // Return a function that maps the args to the context\n return (...args: any[]) => {\n const context: any = {};\n argNames.forEach((name, i) => {\n context[name] = args[i];\n });\n return evaluator(context);\n };\n } catch (error) {\n console.error('Expression parse error:', error);\n return undefined;\n }\n}\n","import { FieldExpressionExtension as FieldExpressionLegacyExtension } from '../field-expression-legacy/field-expression';\nimport { parseExpression } from './utils';\n\nexport class FieldExpressionExtension extends FieldExpressionLegacyExtension {\n protected override _evalStringExpression(expression: string, argNames: string[]) {\n return parseExpression(expression, argNames);\n }\n}\n","import { ComponentRef } from '@angular/core';\nimport { FormlyConfig } from '../../services/formly.config';\nimport { FormlyFieldConfigCache, FormlyValueChangeEvent, FormlyExtension, FormlyFieldConfig } from '../../models';\nimport {\n getFieldId,\n assignFieldValue,\n isUndefined,\n getFieldValue,\n reverseDeepMerge,\n defineHiddenProp,\n clone,\n getField,\n markFieldForCheck,\n hasKey,\n observe,\n isHiddenField,\n isSignalRequired,\n} from '../../utils';\nimport { Subject } from 'rxjs';\n\nexport class CoreExtension implements FormlyExtension {\n private formId = 0;\n constructor(private config: FormlyConfig) {}\n\n prePopulate(field: FormlyFieldConfigCache) {\n const root = field.parent;\n this.initRootOptions(field);\n this.initFieldProps(field);\n if (root) {\n Object.defineProperty(field, 'options', { get: () => root.options, configurable: true });\n Object.defineProperty(field, 'model', {\n get: () => (hasKey(field) && field.fieldGroup ? getFieldValue(field) : root.model),\n configurable: true,\n });\n }\n\n Object.defineProperty(field, 'get', {\n value: (key: FormlyFieldConfig['key']) => getField(field, key),\n configurable: true,\n });\n\n this.getFieldComponentInstance(field).prePopulate?.(field);\n }\n\n onPopulate(field: FormlyFieldConfigCache) {\n this.initFieldOptions(field);\n this.getFieldComponentInstance(field).onPopulate?.(field);\n if (field.fieldGroup) {\n field.fieldGroup.forEach((f, index) => {\n if (f) {\n Object.defineProperty(f, 'parent', { get: () => field, configurable: true });\n Object.defineProperty(f, 'index', { get: () => index, configurable: true });\n }\n this.formId++;\n });\n }\n }\n\n postPopulate(field: FormlyFieldConfigCache) {\n this.getFieldComponentInstance(field).postPopulate?.(field);\n }\n\n private initFieldProps(field: FormlyFieldConfigCache) {\n field.props ??= field.templateOptions;\n Object.defineProperty(field, 'templateOptions', {\n get: () => field.props,\n set: (props) => (field.props = props),\n configurable: true,\n });\n }\n\n private initRootOptions(field: FormlyFieldConfigCache) {\n if (field.parent) {\n return;\n }\n\n const options = field.options;\n field.options.formState = field.options.formState || {};\n if (!options.showError) {\n options.showError = this.config.extras.showError;\n }\n\n if (!options.fieldChanges) {\n defineHiddenProp(options, 'fieldChanges', new Subject<FormlyValueChangeEvent>());\n }\n\n if (!options._hiddenFieldsForCheck) {\n options._hiddenFieldsForCheck = [];\n }\n\n options._detectChanges = (f: FormlyFieldConfigCache) => {\n if (f._componentRefs) {\n markFieldForCheck(f);\n }\n\n f.fieldGroup?.forEach((f) => f && options._detectChanges(f));\n };\n\n options.detectChanges = (f: FormlyFieldConfigCache) => {\n f.options.checkExpressions?.(f);\n options._detectChanges(f);\n };\n\n options.resetModel = (model?: any) => {\n model = clone(model ?? options._initialModel);\n if (field.model) {\n Object.keys(field.model).forEach((k) => delete field.model[k]);\n Object.assign(field.model, model || {});\n }\n\n if (!isSignalRequired()) {\n observe(options, ['parentForm', 'submitted']).setValue(false, false);\n }\n options.build(field);\n field.form.reset(field.model);\n };\n\n options.updateInitialValue = (model?: any) => (options._initialModel = clone(model ?? field.model));\n field.options.updateInitialValue();\n }\n\n private initFieldOptions(field: FormlyFieldConfigCache) {\n reverseDeepMerge(field, {\n id: getFieldId(`formly_${this.formId}`, field, field.index),\n hooks: {},\n modelOptions: {},\n validation: { messages: {} },\n props:\n !field.type || !hasKey(field)\n ? {}\n : {\n label: '',\n placeholder: '',\n disabled: false,\n },\n });\n\n if (this.config.extras.resetFieldOnHide && field.resetOnHide !== false) {\n field.resetOnHide = true;\n }\n\n if (\n field.type !== 'formly-template' &&\n (field.template || field.expressions?.template || field.expressionProperties?.template)\n ) {\n field.type = 'formly-template';\n }\n\n if (!field.type && field.fieldGroup) {\n field.type = 'formly-group';\n }\n\n if (field.type) {\n this.config.getMergedField(field);\n }\n\n if (\n hasKey(field) &&\n !isUndefined(field.defaultValue) &&\n isUndefined(getFieldValue(field)) &&\n !isHiddenField(field)\n ) {\n assignFieldValue(field, field.defaultValue);\n }\n\n field.wrappers = field.wrappers || [];\n }\n\n private getFieldComponentInstance(field: FormlyFieldConfigCache) {\n const componentRefInstance = () => {\n let componentRef = this.config.resolveFieldTypeRef(field);\n\n const fieldComponentRef = field._componentRefs?.slice(-1)[0];\n if (\n fieldComponentRef instanceof ComponentRef &&\n fieldComponentRef?.componentType === componentRef?.componentType\n ) {\n componentRef = fieldComponentRef as any;\n }\n\n return componentRef?.instance as any;\n };\n\n if (!field._proxyInstance) {\n defineHiddenProp(\n field,\n '_proxyInstance',\n new Proxy({} as FormlyExtension, {\n get: (_, prop) => componentRefInstance()?.[prop],\n set: (_, prop, value) => (componentRefInstance()[prop] = value),\n }),\n );\n }\n\n return field._proxyInstance;\n }\n}\n","import { FormlyExtension, FormlyFieldConfigCache } from '../../models';\nimport {\n UntypedFormGroup,\n UntypedFormControl,\n AbstractControlOptions,\n Validators,\n ValidatorFn,\n AsyncValidatorFn,\n FormControl,\n} from '@angular/forms';\nimport { getFieldValue, defineHiddenProp, hasKey, getKeyPath } from '../../utils';\nimport { registerControl, findControl, updateValidity } from './utils';\nimport { of } from 'rxjs';\n\nexport class FieldFormExtension implements FormlyExtension {\n private root: FormlyFieldConfigCache;\n prePopulate(field: FormlyFieldConfigCache) {\n if (!this.root) {\n this.root = field;\n }\n\n if (field.parent) {\n Object.defineProperty(field, 'form', {\n get: () => field.parent!.formControl,\n configurable: true,\n });\n }\n }\n\n onP