Expression Engine Reference

Expression Engine Reference

The expression engine evaluates simple expressions against form values at runtime. It is used by the computedValue property on IFieldConfig to create reactive computed fields that automatically update when their dependencies change.

Import:

import { evaluateExpression, extractExpressionDependencies } from "@formosaic/core";

Syntax

Field References

Use $values.fieldName to reference form field values in expressions.

$values.quantity
$values.unitPrice
$values.nested.path

Field references support dotted path traversal for nested objects (e.g., $values.address.city).

Arithmetic Operators

OperatorDescriptionExample
+Addition / String concatenation$values.a + $values.b
-Subtraction$values.total - $values.discount
*Multiplication$values.quantity * $values.unitPrice
/Division$values.total / $values.count

Comparison Operators

OperatorDescriptionExample
>Greater than$values.score > 80
<Less than$values.startDate < $values.endDate
>=Greater than or equal$values.age >= 18
<=Less than or equal$values.quantity <= $values.maxQuantity
===Strict equality$values.status === "Active"
!==Strict inequality$values.type !== "Archived"

Logical Operators

OperatorDescriptionExample
&&Logical AND$values.enabled && $values.verified
||Logical OR$values.primary || $values.secondary

Ternary Operator

The conditional (ternary) operator ? : is supported:

$values.quantity > 0 ? $values.quantity * $values.unitPrice : 0

String Concatenation

The + operator also handles string concatenation:

$values.firstName + " " + $values.lastName

Math Functions

The following Math functions are available in the execution context:

FunctionDescriptionExample
Math.round()Round to nearest integerMath.round($values.total)
Math.floor()Round downMath.floor($values.score)
Math.ceil()Round upMath.ceil($values.hours)
Math.abs()Absolute valueMath.abs($values.delta)
Math.min()Minimum of argumentsMath.min($values.a, $values.b)
Math.max()Maximum of argumentsMath.max($values.a, 0)

Examples

Common Expressions

ExpressionResult TypeUse Case
$values.quantity * $values.unitPricenumberCalculate total
$values.firstName + " " + $values.lastNamestringFull name
$values.startDate < $values.endDatebooleanDate comparison
Math.round($values.total * 100) / 100numberRound to 2 decimal places
$values.subtotal + $values.taxnumberSum fields
$values.price * (1 - $values.discountPercent / 100)numberApply percentage discount
$values.quantity > 0 ? $values.quantity * $values.unitPrice : 0numberConditional calculation

Usage in Field Config

const fieldConfigs = {
  quantity: {
    type: "Number",
    label: "Quantity",
    required: true,
  },
  unitPrice: {
    type: "Number",
    label: "Unit Price",
    required: true,
  },
  subtotal: {
    type: "Number",
    label: "Subtotal",
    readOnly: true,
    computedValue: "$values.quantity * $values.unitPrice",
  },
  tax: {
    type: "Number",
    label: "Tax",
    readOnly: true,
    computedValue: "Math.round($values.subtotal * 0.08 * 100) / 100",
  },
  total: {
    type: "Number",
    label: "Total",
    readOnly: true,
    computedValue: "$values.subtotal + $values.tax",
  },
  fullName: {
    type: "ReadOnly",
    label: "Full Name",
    computedValue: "$values.firstName + \" \" + $values.lastName",
  },
};

Dependency Extraction

The extractExpressionDependencies() function returns the field names referenced in an expression. This is used internally to auto-detect which fields trigger re-evaluation of computed values.

import { extractExpressionDependencies } from "@formosaic/core";

const deps = extractExpressionDependencies("$values.quantity * $values.unitPrice");
// Returns: ["quantity", "unitPrice"]

Note: The extraction uses the regex pattern $values.([a-zA-Z_][a-zA-Z0-9_]*) which captures the top-level field name only. For nested paths like $values.address.city, only "address" is extracted as the dependency.


Programmatic Evaluation

You can evaluate expressions directly using evaluateExpression():

import { evaluateExpression } from "@formosaic/core";

const result = evaluateExpression(
  "$values.quantity * $values.unitPrice",
  { quantity: 5, unitPrice: 19.99 }
);
// Returns: 99.95

Safety

  • No eval(): The engine uses new Function() with a restricted scope. Only the Math object is available in the execution context.
  • Strict mode: Expressions are evaluated inside "use strict" to prevent accidental global variable access.
  • Null/undefined handling: Field values that are null or undefined resolve to the literal undefined in the expression. This may cause arithmetic expressions to return NaN.
  • Error suppression: Invalid expressions return undefined instead of throwing errors.
  • String values: String field values are automatically JSON-stringified (quoted) in the resolved expression to prevent injection.

Template Expressions

Template expressions use a different syntax from runtime expressions. They are resolved at template expansion time (before the form renders), not at runtime.

Syntax: {{...}}

Template expressions are wrapped in double curly braces {{...}} and appear in template field definitions:

registerFormTemplate('address', {
  params: {
    country: { type: 'string', default: 'US' },
    required: { type: 'boolean', default: true },
  },
  fields: {
    street: { type: 'Textbox', label: 'Street', required: '{{params.required}}' },
    state: {
      type: 'Dropdown',
      label: "{{params.country == 'CA' ? 'Province' : 'State'}}",
      options: '{{$lookup.stateOptions[params.country]}}',
    },
  },
});

Template Expression Contexts

ContextSyntaxDescription
params.*{{params.country}}Access template parameter values
$lookup.*{{$lookup.stateOptions[params.country]}}Access registered lookup tables
Ternary{{params.x == 'y' ? 'a' : 'b'}}Conditional expressions

How They Differ from Runtime Expressions

Template ExpressionsRuntime Expressions
Syntax{{params.country}}$values.fieldName
EvaluatedTemplate expansion time (before render)Runtime (on every form value change)
Contextparams, $lookup$values, $fn, $parent, $root
Used inTemplate field definitionscomputedValue property
ResultStatic config values baked into IFieldConfigDynamic values updated reactively

Template expressions produce static configuration. Once a template is expanded, the resulting IFieldConfig objects are indistinguishable from hand-written configs. Runtime $values expressions continue to work normally inside expanded template fields.

Scoping in Templates

Inside template computedValue expressions, $values references are local by default:

  • $values.street inside a shipping fragment resolves to $values.shipping.street
  • Use $root.fieldName to reference root-level form fields: $root.globalDiscount

See the Templates & Composition guide for full documentation.


Limitations

LimitationWorkaround
No custom function calls (only Math.*)Use value functions (computedValue: "$fn.name()") for complex logic
No variable declarations (let, const, var)Keep expressions as single return expressions
No loops (for, while)Use value functions for iterative calculations
No if/else statementsUse the ternary operator ? : instead
No regex operationsUse validation functions for pattern matching
No Date operations (new Date(), .getTime())Use value functions for date calculations
No array methods (.map(), .filter(), etc.)Use value functions for array operations
No template literals (`${...}`)Use string concatenation with +