Skip to main content

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 0 or NaN rather 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).