Calculated Fields
Core Forms supports calculated fields that compute values from other form fields in real time. Calculations use a safe expression syntax with arithmetic operators and math functions -- no eval() is ever used.
Expression Syntax
Reference other fields by wrapping their name attribute in curly braces:
{field_name}
Combine field references with arithmetic operators and functions to build expressions:
{qty} * {price}
{subtotal} + {tax}
({width} * {height}) / 144
round({total} * 0.0825, 2)
Operators
| Operator | Description | Example |
|---|---|---|
+ |
Addition | {price} + {shipping} |
- |
Subtraction | {total} - {discount} |
* |
Multiplication | {qty} * {unit_price} |
/ |
Division | {total} / {months} |
Standard math precedence applies: multiplication and division before addition and subtraction. Use parentheses to control evaluation order.
Functions
The following math functions are available in expressions:
| Function | Description | Example |
|---|---|---|
round(value) |
Round to nearest integer | round({total}) |
round(value, decimals) |
Round to N decimal places | round({tax}, 2) |
ceil(value) |
Round up to nearest integer | ceil({qty} / 12) |
floor(value) |
Round down to nearest integer | floor({score} / 10) |
min(a, b) |
Return the smaller value | min({bid}, {max_price}) |
max(a, b) |
Return the larger value | max({subtotal}, 0) |
abs(value) |
Absolute value | abs({balance}) |
Functions can be nested:
round(max({qty} * {price}, 0), 2)
Safe Parser
Core Forms uses a recursive-descent parser to evaluate expressions. This is intentionally restrictive:
- No
eval(),new Function(), or dynamic code execution. - Only arithmetic operators, parentheses, numbers, field references, and whitelisted functions are accepted.
- Invalid tokens cause the parser to return
0orNaNrather than executing arbitrary code. - Division by zero returns
0.
The parser tokenizes the expression, builds an AST, and evaluates it with the current field values substituted in.
Client-Side: data-calculation Attribute
Add the data-calculation attribute to any input to make it a calculated field:
<form class="cf-form">
<label for="qty">Quantity</label>
<input type="number" name="qty" id="qty" value="1" min="1">
<label for="price">Unit Price ($)</label>
<input type="number" name="price" id="price" value="25.00" step="0.01">
<label for="shipping">Shipping ($)</label>
<input type="number" name="shipping" id="shipping" value="5.00" step="0.01">
<label for="total">Order Total ($)</label>
<input type="text" name="total" id="total" readonly
data-calculation="{qty} * {price} + {shipping}">
</form>
Live Updates
Calculated fields update automatically whenever a referenced field changes. Core Forms listens for input and change events on all fields referenced in the expression and recalculates immediately.
// Internally, Core Forms does something like:
referencedFields.forEach(field => {
field.addEventListener('input', () => recalculate(calculatedField));
field.addEventListener('change', () => recalculate(calculatedField));
});
Display Formatting
Calculated fields are read-only by default (use the readonly attribute). To format the displayed value as currency or with specific decimal places, combine with data-calculation-format:
<!-- Display as currency with 2 decimal places -->
<input type="text" name="total" readonly
data-calculation="{qty} * {price}"
data-calculation-format="currency"
data-calculation-decimals="2">
<!-- Display as percentage -->
<input type="text" name="rate" readonly
data-calculation="{correct} / {total_questions} * 100"
data-calculation-format="percent"
data-calculation-decimals="1">
Supported formats:
- number (default) -- plain number
- currency -- prefixed with the currency symbol from form settings
- percent -- suffixed with %
Server-Side: FieldCalculator Class
The FieldCalculator class evaluates calculations on the server during form processing. This ensures calculated values cannot be tampered with by modifying the DOM.
FieldCalculator::evaluate()
Evaluate a single expression with given field values:
use Core_Forms\FieldCalculator;
$calculator = new FieldCalculator();
$result = $calculator->evaluate(
'{qty} * {price} + {shipping}',
[
'qty' => 3,
'price' => 25.00,
'shipping' => 5.00,
]
);
// Result: 80.00
FieldCalculator::process()
Process all calculated fields in a form submission. This method reads the form schema, finds all fields with calculations, evaluates them, and overwrites any client-submitted values:
$calculator = new FieldCalculator();
$processed_data = $calculator->process($form, $submission_data);
// All calculated field values are now server-verified
This is called automatically during Forms::process() before the submission is saved.
Examples
Order Total with Tax
<label for="subtotal">Subtotal</label>
<input type="text" name="subtotal" id="subtotal" readonly
data-calculation="{qty} * {unit_price}">
<label for="tax">Tax (8.25%)</label>
<input type="text" name="tax" id="tax" readonly
data-calculation="round({subtotal} * 0.0825, 2)">
<label for="total">Total</label>
<input type="text" name="total" id="total" readonly
data-calculation="{subtotal} + {tax}"
data-calculation-format="currency"
data-calculation-decimals="2">
BMI Calculator
<label for="weight">Weight (kg)</label>
<input type="number" name="weight" id="weight" step="0.1">
<label for="height">Height (m)</label>
<input type="number" name="height" id="height" step="0.01">
<label for="bmi">Your BMI</label>
<input type="text" name="bmi" id="bmi" readonly
data-calculation="round({weight} / ({height} * {height}), 1)">
Pricing with Quantity Discount
<label for="qty">Quantity</label>
<input type="number" name="qty" id="qty" min="1" value="1">
<label for="unit_price">Unit Price</label>
<input type="number" name="unit_price" id="unit_price" value="10" readonly>
<label for="discount">Bulk Discount</label>
<input type="text" name="discount" id="discount" readonly
data-calculation="floor({qty} / 10) * 5"
data-calculation-format="percent">
<label for="total">Total</label>
<input type="text" name="total" id="total" readonly
data-calculation="round({qty} * {unit_price} * (1 - floor({qty} / 10) * 0.05), 2)"
data-calculation-format="currency"
data-calculation-decimals="2">
Chained Calculations
Calculated fields can reference other calculated fields. Core Forms resolves dependencies in topological order:
<input type="text" name="subtotal" readonly
data-calculation="{qty} * {price}">
<input type="text" name="discount_amount" readonly
data-calculation="{subtotal} * {discount_percent} / 100">
<input type="text" name="after_discount" readonly
data-calculation="{subtotal} - {discount_amount}">
<input type="text" name="tax" readonly
data-calculation="round({after_discount} * 0.08, 2)">
<input type="text" name="grand_total" readonly
data-calculation="{after_discount} + {tax}">
Each field recalculates in order when any upstream value changes.
Error Handling
- If a referenced field is empty or non-numeric, it is treated as
0. - Division by zero returns
0. - Malformed expressions return
NaN, which is displayed as an empty value. - Circular references are detected and prevented (the field shows
0).