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