@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
JavaScript
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 });