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
| Operator | Description | Example |
|---|---|---|
+ | Addition / String concatenation | $values.a + $values.b |
- | Subtraction | $values.total - $values.discount |
* | Multiplication | $values.quantity * $values.unitPrice |
/ | Division | $values.total / $values.count |
Comparison Operators
| Operator | Description | Example |
|---|---|---|
> | 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
| Operator | Description | Example |
|---|---|---|
&& | 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:
| Function | Description | Example |
|---|---|---|
Math.round() | Round to nearest integer | Math.round($values.total) |
Math.floor() | Round down | Math.floor($values.score) |
Math.ceil() | Round up | Math.ceil($values.hours) |
Math.abs() | Absolute value | Math.abs($values.delta) |
Math.min() | Minimum of arguments | Math.min($values.a, $values.b) |
Math.max() | Maximum of arguments | Math.max($values.a, 0) |
Examples
Common Expressions
| Expression | Result Type | Use Case |
|---|---|---|
$values.quantity * $values.unitPrice | number | Calculate total |
$values.firstName + " " + $values.lastName | string | Full name |
$values.startDate < $values.endDate | boolean | Date comparison |
Math.round($values.total * 100) / 100 | number | Round to 2 decimal places |
$values.subtotal + $values.tax | number | Sum fields |
$values.price * (1 - $values.discountPercent / 100) | number | Apply percentage discount |
$values.quantity > 0 ? $values.quantity * $values.unitPrice : 0 | number | Conditional 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 usesnew Function()with a restricted scope. Only theMathobject 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
nullorundefinedresolve to the literalundefinedin the expression. This may cause arithmetic expressions to returnNaN. - Error suppression: Invalid expressions return
undefinedinstead 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
| Context | Syntax | Description |
|---|---|---|
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 Expressions | Runtime Expressions | |
|---|---|---|
| Syntax | {{params.country}} | $values.fieldName |
| Evaluated | Template expansion time (before render) | Runtime (on every form value change) |
| Context | params, $lookup | $values, $fn, $parent, $root |
| Used in | Template field definitions | computedValue property |
| Result | Static config values baked into IFieldConfig | Dynamic 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.streetinside ashippingfragment resolves to$values.shipping.street- Use
$root.fieldNameto reference root-level form fields:$root.globalDiscount
See the Templates & Composition guide for full documentation.
Limitations
| Limitation | Workaround |
|---|---|
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 statements | Use the ternary operator ? : instead |
| No regex operations | Use 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 + |