Calculations and scoring
Derived numeric fields from a safe expression engine for totals, quotes, and quiz scores.
A calculation field derives its value from a serializable expression over other answers. The client computes it live for display; the server recomputes it from scratch at submit and stores only its own result. The client's number is discarded.
Set calcDisplay: false on the field to compute without rendering (a hidden total or quiz score that conditions and validation can still reference).
The expression AST
Expressions are plain JSON with a type discriminant. No eval, no Function, no untrusted code:
| Node | Shape | Notes |
|---|---|---|
lit | { type: 'lit', value: number } | Literal |
ref | { type: 'ref', field: string } | Another field's answer |
op | { type: 'op', op: '+' | '-' | '*' | '/' | '%', left, right } | Arithmetic |
neg | { type: 'neg', operand } | Negation |
fn | { type: 'fn', fn: 'min' | 'max' | 'round' | 'abs' | 'ceil' | 'floor', args } | Math functions |
weight | { type: 'weight', field: string, weights: Record<string, number> } | Maps an option answer to a score |
A price total:
import type { CalcExpression } from '@10x-media/form-builder'
const total: CalcExpression = {
type: 'op',
op: '*',
left: { type: 'ref', field: 'qty' },
right: { type: 'ref', field: 'unitPrice' },
}A quiz score:
const scoreQ1: CalcExpression = {
type: 'weight',
field: 'q1',
weights: { a: 10, b: 0, c: 5 },
}Authoring in the admin
Expressions are authored in the field's JSON config field. The field describes the node shapes inline and ships a JSON schema for the admin's Monaco editor, so authors get autocomplete and inline diagnostics while typing. Validity is enforced on save: an expression that does not normalize into a well-formed tree is rejected with a field error, so a broken calculation cannot reach the public form.
A visual expression builder is planned; expressions are authored as data today.
Totality and safety
The evaluator always produces a finite number: division or modulo by zero yields 0, a missing ref yields 0, an unknown weight key yields 0, and recursion is depth-guarded (max 64). It runs on the public submit path for every submission, so these properties are load-bearing, not niceties.
Using the engine directly
evaluateCalc, computeCalcFields, calcExpressionOf, and normalizeCalc are exported from the root and from ./react, so custom layouts compute with the identical engine:
import { evaluateCalc, computeCalcFields } from '@10x-media/form-builder/react'
evaluateCalc({ type: 'ref', field: 'qty' }, { qty: 3 }) // 3
const updatedValues = computeCalcFields(fields, currentValues)Downstream
Because the stored value is server-authoritative, calculation fields participate everywhere ordinary fields do: visibleWhen/validateWhen conditions, validation rules, and recall (You scored {{score}} points!).