Computed Fields: Calculations That Just Work
Your customer fills out a form. They select options, enter quantities, pick add-ons. At the bottom, they see... nothing. Just a "Submit" button and a promise that someone will "get back to them with a quote." They leave. They fill out your competitor's form instead - the one that shows the price updating in real time.
Computed fields solve this. They're form fields that calculate their value from other fields, updating instantly whenever inputs change. No refresh, no "Calculate" button, no waiting. The customer sees what they're getting as they build it.
In most visual form builders, calculations are either impossible or painfully limited. "Field A + Field B" might work. Anything more complex - tiered discounts, conditional pricing, multipliers based on selections - and you're out of luck.
In FormTs, you write the formula. Whatever TypeScript can calculate, your form can display.
The Basics
A computed field is just a field with a computedValue function instead of user input. The function runs whenever any value it references changes.
form.addRow(row => {
row.addInteger('quantity', { label: 'Quantity', defaultValue: 1 });
row.addDecimal('unitPrice', { label: 'Unit Price', defaultValue: 50 });
});
form.addRow(row => {
row.addPriceDisplay('total', {
label: 'Total',
computedValue: () => {
const qty = form.integer('quantity')?.value() || 0;
const price = form.decimal('unitPrice')?.value() || 0;
return qty * price;
}
});
}); That's it. The total field shows quantity × unitPrice, and it updates the moment either input changes.
Pro tip
Computed fields are read-only by default. Users see the result but can't edit it directly. The value comes purely from the calculation.
Reactivity Is Automatic
You don't wire up event handlers. You don't call "recalculate" anywhere. You don't manage state. The form tracks dependencies automatically.
User changes quantity from 1 to 5? Total updates instantly. Changes unit price from 50 to 75? Total updates again. No refresh button, no delays. Change any input that affects the formula and the output updates. This works no matter how complex the dependency chain gets.
Real Pricing Scenarios
Let's look at calculations you'll actually need. These patterns cover most pricing logic for service businesses.
Base Price Plus Add-ons
The most common pattern: a base service with optional extras.
computedValue: () => {
const base = form.decimal('baseService')?.value() || 0;
const addon1 = form.checkbox('addon1')?.value() ? 25 : 0;
const addon2 = form.checkbox('addon2')?.value() ? 40 : 0;
const addon3 = form.checkbox('addon3')?.value() ? 15 : 0;
return base + addon1 + addon2 + addon3;
}Each checkbox adds its price when selected. The total reflects exactly what the customer has chosen.
Percentage Discounts
"10% off for returning customers" - a simple multiplier based on a condition.
computedValue: () => {
const subtotal = calculateSubtotal(); // helper function
const isReturning = form.checkbox('returningCustomer')?.value();
const discount = isReturning ? 0.10 : 0;
return subtotal * (1 - discount);
}The discount applies automatically when the checkbox is selected. Uncheck it, discount disappears, price updates.
Tiered Discounts
Bigger orders get bigger discounts. This is where visual builders usually give up.
computedValue: () => {
const subtotal = calculateSubtotal();
let discountRate = 0;
if (subtotal >= 1000) discountRate = 0.15;
else if (subtotal >= 500) discountRate = 0.10;
else if (subtotal >= 200) discountRate = 0.05;
return subtotal * (1 - discountRate);
}As the subtotal crosses thresholds, the discount rate changes. The customer sees the discount kick in as they add items - a nice incentive to order more.
See tiered pricing in action in our calculator gallery.
Size and Type Multipliers
Different prices for different sizes, vehicle types, property types, or service tiers.
const sizeMultipliers: Record<string, number> = {
'small': 1.0,
'medium': 1.5,
'large': 2.0,
'xl': 2.5
};
computedValue: () => {
const base = form.decimal('basePrice')?.value() || 0;
const size = form.dropdown('vehicleSize')?.value() || 'small';
return base * (sizeMultipliers[size] || 1);
}Select "large" instead of "small" and the price doubles. The multiplier map makes it easy to adjust rates without touching the formula.
Location-Based Pricing
Same service, different prices by area. Downtown costs more than suburbs.
const locationRates: Record<string, number> = {
'downtown': 1.25,
'suburbs': 1.0,
'rural': 0.85
};
computedValue: () => {
const base = calculateServiceTotal(); // helper function
const location = form.dropdown('serviceArea')?.value() || 'suburbs';
return base * (locationRates[location] || 1);
}This pattern works for zip codes, neighborhoods, service regions - anything where location affects price.
Date Calculations
Not all computed fields are prices. Sometimes you need to calculate durations, deadlines, or time-based conditions.
Days Between Dates
Rental duration, project length, event planning windows.
computedValue: () => {
const start = form.datepicker('startDate')?.value();
const end = form.datepicker('endDate')?.value();
if (!start || !end) return 0;
const diffTime = end.getTime() - start.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
}Select a start and end date, see the duration instantly. Useful for rentals, bookings, or any service priced by time.
Days Until Event
Rush fees, early-bird discounts, booking deadlines.
computedValue: () => {
const eventDate = form.datepicker('eventDate')?.value();
if (!eventDate) return null;
const today = new Date();
const diffTime = eventDate.getTime() - today.getTime();
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}Combine this with conditional logic: if days until event is less than 7, show a rush fee field. If it's more than 30, show an early-bird discount.
Advanced Patterns
Chained Calculations
One computed value feeds into another. Subtotal → tax → total.
// Intermediate computed value: subtotal
const subtotal = form.computedValue(() => {
const qty = form.integer('quantity')?.value() || 0;
const price = form.decimal('unitPrice')?.value() || 0;
return qty * price;
});
// Another intermediate: tax (uses subtotal)
const tax = form.computedValue(() => subtotal() * 0.08);
// Final display field uses both
row.addPriceDisplay('total', {
label: 'Total with Tax',
computedValue: () => subtotal() + tax()
});Each calculation is a simple function. Complex totals build from simple pieces. Change any input and the entire chain recalculates.
Formatted Output
Raw numbers work, but "$1,234.56" looks more professional than "1234.56".
computedValue: () => {
const total = calculateTotal();
return total.toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
});
}Format as currency, percentages, or any other display format. The underlying value stays numeric for further calculations.
Conditional Formulas
Different service types need completely different calculations.
computedValue: () => {
const type = form.dropdown('serviceType')?.value();
const sqft = form.integer('squareFeet')?.value() || 0;
switch (type) {
case 'standard':
return sqft * 0.10;
case 'deep-clean':
return sqft * 0.18;
case 'move-out':
return sqft * 0.25 + 50; // flat fee for move-out
default:
return 0;
}
}The formula itself changes based on what the user selects. Standard cleaning uses one rate, deep clean uses another, move-out adds a flat fee on top.
Putting It All Together
Here's a complete example - a catering quote calculator that combines multiple patterns: per-person pricing, add-ons, travel fees, and a weekend surcharge.
// Catering price calculator
form.addRow(row => {
row.addInteger('guests', { label: 'Number of Guests', defaultValue: 50 });
row.addDropdown('menu', {
label: 'Menu Selection',
options: [
{ id: 'basic', name: 'Basic ($25/person)' },
{ id: 'premium', name: 'Premium ($45/person)' },
{ id: 'luxury', name: 'Luxury ($75/person)' }
],
defaultValue: 'basic'
});
});
form.addRow(row => {
row.addCheckbox('dessert', { label: 'Add Dessert Bar (+$8/person)' });
row.addCheckbox('bar', { label: 'Open Bar (+$35/person)' });
});
form.addRow(row => {
row.addInteger('distance', { label: 'Miles from Downtown', defaultValue: 0 });
row.addCheckbox('weekend', { label: 'Weekend Event' });
});
// The magic happens here
form.addRow(row => {
row.addPriceDisplay('estimate', {
label: 'Estimated Total',
computedValue: () => {
const guests = form.integer('guests')?.value() || 0;
const menu = form.dropdown('menu')?.value();
const menuPrices = { basic: 25, premium: 45, luxury: 75 };
let total = guests * (menuPrices[menu] || 25);
if (form.checkbox('dessert')?.value()) total += guests * 8;
if (form.checkbox('bar')?.value()) total += guests * 35;
const miles = form.integer('distance')?.value() || 0;
if (miles > 10) total += (miles - 10) * 2;
if (form.checkbox('weekend')?.value()) total *= 1.15;
return total;
}
});
});The customer adjusts guest count - price updates. Adds open bar - price updates. Changes the date to Saturday - weekend surcharge appears. Every change reflects instantly in the estimate.
Pro tip
Start simple. Get the basic calculation working first, then add complexity. It's easier to debug a formula when you add one thing at a time.
Common Mistakes
Forgetting Null Checks
Fields can be empty. If quantity.value() returns null and you multiply it, you get NaN. Always handle the empty case:
const qty = quantity.value() || 0;Circular Dependencies
Field A depends on Field B, which depends on Field A. Don't do this. The form won't know what to calculate first. Keep your dependency chains one-directional.
Over-Complicated Formulas
If your computedValue function is 50 lines long, break it up. Extract helper functions. Use intermediate computed fields. Future you will be grateful.
When to Use Computed Fields
Computed fields shine when:
- Customers want to see prices before submitting
- The total depends on multiple selections
- You have discounts, surcharges, or multipliers
- You need to show durations, dates, or derived values
- You want to reduce "how much does this cost?" inquiries
They're not needed when:
- Pricing requires human judgment (custom quotes)
- The form just collects information without calculations
- You intentionally want to hide prices until later
Common Questions
Can computed fields be edited by users?
By default, computed fields are read-only - the value comes from the calculation. This prevents users from manually entering a price that doesn't match what they selected. If you need a field that calculates a default but allows override, you can use defaultValue with a function instead of computedValue.
What happens if my calculation has an error?
TypeScript catches most errors before runtime - you'll see red underlines in the editor. Runtime errors (like dividing by zero) will show as NaN or undefined in the field. Add defensive checks for edge cases: null values, empty selections, zero quantities.
Can I use computed values in other computations?
Yes. You can reference one computed field from another, creating calculation chains. Subtotal feeds into tax calculation, tax feeds into total. Just avoid circular dependencies where A depends on B and B depends on A.
How do I format computed values as currency?
Use toLocaleString() with currency options, or any formatting library you prefer. The computedValue function can return a formatted string for display, or you can keep it numeric and apply formatting in the field's display settings.