What the calculated field does
A calculated field is a readonly Form Builder field whose value is derived from an expression instead of direct user input. A form author can drag the field into a schema, open its settings, and enter an Excel-like expression such as ROUND(quantity * unit_price, 2). At runtime, ngs-form-renderer evaluates the expression whenever the form value changes and writes the result into the calculated field's Angular form control.
The calculated value is included in form.getRawValue() and in the value emitted by the renderer, even though the control is rendered as readonly. That makes it useful for invoice totals, quote totals, scores, eligibility flags, generated display names, workflow estimates, and admin review fields that should be visible but not manually edited.
Minimal field schema
The field uses the normal FormBuilderField shape. The only special part is type: 'calculated' and the calculation configuration under settings. The name remains the submitted value key, just like every other Form Builder control.
{
id: 'line_total',
name: 'line_total',
type: 'calculated',
label: 'Line total',
width: 4,
settings: {
expression: 'ROUND(quantity * unit_price, 2)',
valueType: 'number',
precision: 2,
emptyValue: '',
},
}Calculation settings
The builder inspector exposes four settings. settings.expression stores the formula. settings.valueType controls result coercion and can be auto, number, text, or boolean. settings.precision optionally rounds numeric results to a fixed number of decimal places. settings.emptyValue is used when the expression is empty or the result cannot be converted to the requested value type.
settings: {
expression: 'IF(discount > 0, subtotal - discount, subtotal)',
valueType: 'number',
precision: 2,
emptyValue: '',
}Invoice line example
This schema renders quantity, unit price, and a readonly line total. If the user changes quantity or unit_price, the renderer recomputes line_total immediately.
const schema: FormBuilderSchema = {
title: 'Invoice',
sections: [
{
id: 'line-item',
title: 'Line item',
fields: [
{
id: 'quantity',
name: 'quantity',
type: 'number',
label: 'Quantity',
defaultValue: 1,
width: 4,
},
{
id: 'unit_price',
name: 'unit_price',
type: 'currency',
label: 'Unit price',
defaultValue: 49,
width: 4,
},
{
id: 'line_total',
name: 'line_total',
type: 'calculated',
label: 'Line total',
width: 4,
settings: {
expression: 'ROUND(quantity * unit_price, 2)',
valueType: 'number',
precision: 2,
emptyValue: '',
},
},
],
},
],
}; The renderer does not need special template syntax for calculated fields. It receives the schema, creates a control for line_total, subscribes to form changes, and keeps that control synchronized with the expression result.
<ngs-form-renderer
[schema]="schema"
[(value)]="value"
(formSubmit)="saveInvoice($event)" />Live renderer with chained calculations
This live ngs-form-renderer calculates several fields from the same editable inputs. subtotal depends on quantity and unit price, discount_amount depends on subtotal, taxable_total depends on both, and grand_total depends on the final tax calculation.
Expression basics
Expressions are written directly and must not start with a spreadsheet-style = prefix. You can write arithmetic, grouped subexpressions, comparisons, string literals, field references, and a small set of built-in functions.
price * quantity
ROUND(price * quantity, 2)
IF(quantity > 10, price * quantity * 0.9, price * quantity)
SUM(subtotal, tax, shipping)
SUM(items.line_total)
AVG(items.score)
COUNT(items.line_total)
CONCAT(first_name, ' ', last_name)+,-,*,/, and^handle numeric arithmetic.&concatenates text values.>,<,>=,<=,=, and<>return boolean results.- Parentheses control grouping, for example
(subtotal + shipping) * tax_rate. - String literals can use single or double quotes.
Referencing fields
A formula references other fields by their name, not by their id or label. In ROUND(quantity * unit_price, 2), the names quantity and unit_price are looked up in the renderer's raw form value. This is why field names should be stable, machine-friendly identifiers.
If a referenced field is missing or currently empty, the default engine reads it as null. Numeric operations convert null, undefined, and empty strings to 0. This keeps simple totals from failing while users are still filling out the form.
value = {
quantity: 3,
unit_price: 49,
line_total: 147,
};Supported functions
The default engine is intentionally small and safe. It is not a full Excel runtime, but it covers common admin form calculations. Supported functions include SUM, ROUND, IF, MIN, MAX, AVG, AVERAGE, ABS, COUNT, COUNTA, POWER, SQRT, MOD, FLOOR, CEIL, CEILING, CONCAT, and CONCATENATE.
SUM(subtotal, tax, shipping)adds multiple values.SUM(items.line_total)adds values from every row in a repeater.AVG(items.score)averages a numeric collection.COUNT(items.line_total)counts numeric values in a collection.ROUND(total, 2)rounds to two decimal places.IF(score >= 80, 'Approved', 'Review')returns one of two values.CONCAT(first_name, ' ', last_name)builds display text.
Result types and coercion
Use settings.valueType when the saved value should have a predictable type. number converts the result with Number(...) and can apply settings.precision. text converts non-empty results with String(...). boolean stores truthy or falsy output. auto stores the raw result returned by the engine.
For financial UI, prefer valueType: 'number' and an explicit precision. For display-only helper values such as a generated customer name, use valueType: 'text'. For eligibility or workflow checks, use valueType: 'boolean'.
Dependency order
Calculated fields can depend on other calculated fields. The renderer asks the engine for dependencies, builds a dependency order, and evaluates upstream calculations before downstream calculations. This makes chained formulas work without manual subscriptions.
[
{
name: 'subtotal',
type: 'calculated',
settings: {
expression: 'quantity * unit_price',
valueType: 'number',
},
},
{
name: 'tax',
type: 'calculated',
settings: {
expression: 'ROUND(subtotal * tax_rate, 2)',
valueType: 'number',
precision: 2,
},
},
{
name: 'total',
type: 'calculated',
settings: {
expression: 'subtotal + tax',
valueType: 'number',
precision: 2,
},
},
] Self-references and circular chains are treated as errors. For example, total = total + 1 or a = b + 1 with b = a + 1 cannot produce a stable value, so the calculated field gets a calculation error instead of silently using a stale result.
Groups and repeaters
Layout groups flatten their child fields into the current form group, so a calculated field inside a group can reference sibling group fields by name. Repeaters are evaluated per row: a calculated field inside a repeater row references fields from the same row. This is the right model for line item totals where each row has its own quantity, unit_price, and line_total.
{
id: 'items',
name: 'items',
type: 'repeater',
kind: 'layout',
label: 'Items',
children: [
{ id: 'quantity', name: 'quantity', type: 'number', label: 'Quantity' },
{ id: 'unit_price', name: 'unit_price', type: 'currency', label: 'Unit price' },
{
id: 'line_total',
name: 'line_total',
type: 'calculated',
label: 'Line total',
settings: {
expression: 'ROUND(quantity * unit_price, 2)',
valueType: 'number',
precision: 2,
},
},
],
} A calculated field outside the repeater can aggregate values from every row with a path expression. items.line_total resolves to an array of all line_total values in the repeater, so collection-aware functions such as SUM, AVG, MIN, MAX, COUNT, and COUNTA can consume it directly.
{
id: 'invoice_total',
name: 'invoice_total',
type: 'calculated',
label: 'Invoice total',
settings: {
expression: 'ROUND(SUM(items.line_total), 2)',
valueType: 'number',
precision: 2,
emptyValue: '',
},
} The renderer calculates row-level expressions first, then recalculates outer fields. That means SUM(items.line_total) sees fresh row totals even when line_total itself is a calculated field.
Errors and readonly rendering
A calculated field renders as a readonly NgStarter input. If parsing or evaluation fails, the renderer stores a formBuilderCalculation error on the control and the field host shows the error message below the input. The form can then surface invalid formulas during preview, QA, or production rendering.
Common errors include unknown functions, unclosed string literals, invalid operators, and circular dependencies. Formula errors are intentionally visible because a saved schema is production configuration and should fail loudly when it cannot be evaluated.
Security model
The default engine does not use eval or new Function. It tokenizes the expression, parses it with a small recursive descent parser, resolves field names against form values, and calls an allowlisted set of functions. This keeps formulas suitable for user-authored admin schemas without giving those users arbitrary JavaScript execution.
Replacing the calculation engine
If you need full spreadsheet behavior, project-specific functions, server-backed values, currency conversion, or a different parser, replace the engine through provideFormBuilder. A custom engine implements FormBuilderCalculationEngine with evaluate and optionally dependencies. The renderer will use it for every calculated field.
import {
FormBuilderCalculationEngine,
provideFormBuilder,
} from '@ngstarter-ui/components/form-builder';
export const calculationEngine: FormBuilderCalculationEngine = {
evaluate(expression, context) {
if (expression === 'CURRENT_USER_DISCOUNT()') {
return { value: context.values['customer_tier'] === 'enterprise' ? 0.15 : 0 };
}
return { value: null, error: 'Unsupported expression.' };
},
dependencies(expression) {
return expression.includes('CURRENT_USER_DISCOUNT')
? ['customer_tier']
: [];
},
};
export const appConfig = {
providers: [
provideFormBuilder({
calculationEngine,
}),
],
};Production checklist
- Use stable field
namevalues because formulas reference names. - Use
valueType: 'number'andprecisionfor money-like totals. - Keep formula fields readonly and do not ask users to edit calculated output manually.
- Prefer small, explicit expressions over deeply nested business logic.
- Validate saved schemas in preview before publishing them to production users.
- Use
SUM(items.line_total),AVG(items.score), or similar paths for repeater aggregates. - Replace the default engine when you need full Excel compatibility, spreadsheet ranges, or custom functions.