Skip to content

FormulaInput

Paulo Gomes da Cruz Junior edited this page Sep 11, 2026 · 4 revisions

FormulaInput

Overview

FormulaInput is a billing-grade formula editor that combines Monaco Editor with mathjs BigNumber evaluation. It is designed for authoring formulas such as rate * hours + bonus that are stored in a database and re-evaluated by a Java backend.

Key characteristics:

  • Live evaluation — the formula is parsed and evaluated on every keystroke via mathjs in 64-digit BigNumber mode. The result appears inline (as a ghost-text decoration) and, optionally, in a ReadonlyInput below the editor.
  • Variable-aware autocomplete — both fixed (API-sourced) and dynamic (user-defined) parameters are registered as Monaco completions and hover providers.
  • Precision drift detection — if the BigNumber result deviates from a JS number result beyond a configurable threshold, a warning is shown to signal a potential backend disagreement.
  • Carbon Design System styling — the editor is wrapped in a container styled to match Carbon's TextInput, including focus rings, error/warning border colours, and helper-text-01 typography for validation messages.
  • Native form integration — an optional name prop renders a hidden <input type="hidden"> so the formula participates in FormData on form submission.

Architecture

FormulaInput (index.tsx)
  ├── useFormulaEngine    — evaluation, variable resolution, precision check
  │     └── utils.ts      — pure helpers: parseFormula, evaluateFormula,
  │                         extractVariables, checkDoublePrecisionDrift
  ├── useMonacoFormula    — Monaco language registration, completions,
  │                         hover providers, squiggle markers
  ├── DependencyGraph     — visualises which variables the formula uses
  │                         (Carbon Tag graph)
  ├── VariablePanel       — lists all available variables (fixed / dynamic)
  │                         with used/unused opacity differentiation
  └── ReadonlyInput       — displays the evaluated numeric result

Data flow

fixedParams + dynamicParams
        │
        ▼
  useFormulaEngine ──────► mergedScope ──► useMonacoFormula
        │
        ├── formula (string)
        ├── result  { value, error, raw }
        ├── usedVariables (string[])
        └── precisionWarning (string | null)
                │
                ▼
          FormulaInput JSX
          ├── <Editor>  (Monaco, FORMULA_LANGUAGE_ID)
          ├── helper-text div (error / warning)
          ├── <ReadonlyInput>  (evaluated result)
          ├── <DependencyGraph>
          └── <VariablePanel × 2>  (fixed + dynamic)

Props

Name Type Required Default Description
fixedParams Record<string, number> ✅ — API-sourced parameters (e.g. from TanStack Query). Take precedence over dynamicParams on name collision — server values always win.
dynamicParams Record<string, number> ✅ — Runtime-mutable parameters added or removed by the user upstream. Immediately available in autocomplete.
id string ❌ React useId() Stable HTML id prefix. Derived ids ${id}-formula-label and ${id}-evaluated-result are applied to internal elements. Falls back to a React-generated uid so multiple instances on the same page remain accessible.
ariaLabel string ❌ 'Formula editor' Accessible label for the <section> landmark. Pass distinct values when multiple instances appear on the same page.
initialFormula string ❌ '' Starting formula on first mount. Non-reactive — changes after mount have no effect. Remount with a new key to reset.
onChange (formula: string) => void ❌ — Called on every formula change. The raw string is passed through without sanitisation; the consumer is responsible for persisting.
displayResult boolean ❌ true When false, hides the ReadonlyInput below the editor. Errors and precision warnings are always shown regardless of this flag.
displayDependencyGraph boolean ❌ true When false, hides the dependency graph and variable panels. Suppresses the five related useMemo computations.
readOnly boolean ❌ false Renders the editor in read-only mode. All editing interactions are disabled; selection and copy still work.
name string ❌ — HTML name for native form integration. Renders a hidden <input type="hidden"> carrying the current formula so it is included in FormData.

Sub-components and Hooks

useFormulaEngine

Manages the formula lifecycle: state, parsing, evaluation, variable extraction, and precision drift detection. The formula string is parsed once per keystroke into a shared AST; both result and usedVariables derive from that single node.

const { formula, result, usedVariables, precisionWarning, mergedScope, setFormula } =
  useFormulaEngine({ fixedParams, dynamicParams, initialFormula, onChange });

Returns a FormulaEngineState object.

useMonacoFormula

Registers the custom FORMULA_LANGUAGE_ID Monaco language (completions, hover docs, squiggle markers) and returns the onMount handler that activates it. Depends on allVariables (merged scope), the current formula, evaluationResult, evaluationError, and the active Carbon theme.

DependencyGraph

A graphical component (Carbon Tags) that highlights which variables from mergedScope appear in the current formula. Variables not referenced are rendered at reduced opacity.

VariablePanel

Renders an accessible list of parameter name / value pairs as Carbon Tags. Used twice: once for fixed params (blue) and once for dynamic params (teal). Tags in usedSet are rendered at full opacity.


Usage Examples

Basic — billing formula with live evaluation

import { FormulaInput } from '@/components/Form/FormulaInput';

function BillingForm() {
  const [formula, setFormula] = React.useState('');

  return (
    <FormulaInput
      id="billing-formula"
      ariaLabel="Billing rate formula"
      fixedParams={{ tax_rate: 0.15, base_price: 100 }}
      dynamicParams={{ quantity: 5, discount: 0.1 }}
      initialFormula="base_price * quantity * (1 - discount) * (1 + tax_rate)"
      onChange={setFormula}
    />
  );
}

Native form integration

The name prop adds a hidden input so the formula string is submitted with FormData:

<form onSubmit={(e) => {
  const data = new FormData(e.currentTarget);
  console.log(data.get('rate_formula')); // current formula string
}}>
  <FormulaInput
    name="rate_formula"
    fixedParams={{ hourly_rate: 45 }}
    dynamicParams={{ hours: 40 }}
  />
  <button type="submit">Save</button>
</form>

Multiple instances on the same page

Always supply distinct id and ariaLabel props so landmark navigation and derived element ids (${id}-formula-label, ${id}-evaluated-result) remain unique:

<FormulaInput
  id="rate-calc"
  ariaLabel="Rate formula"
  fixedParams={rateParams}
  dynamicParams={dynamicRateParams}
  onChange={setRateFormula}
/>

<FormulaInput
  id="discount-calc"
  ariaLabel="Discount formula"
  fixedParams={discountParams}
  dynamicParams={dynamicDiscountParams}
  onChange={setDiscountFormula}
/>

Edge case — read-only preview

Use readOnly to display a stored formula without allowing editing:

<FormulaInput
  id="saved-formula"
  ariaLabel="Saved billing formula (read-only)"
  fixedParams={savedFixedParams}
  dynamicParams={{}}
  initialFormula={storedFormulaFromDB}
  readOnly
  displayDependencyGraph={false}
/>

Edge case — precision warning

When a formula produces a result that differs between BigNumber and JS number arithmetic (e.g. chained floating-point multiplications), a warning is rendered below the editor:

⚠ Precision loss detected: relative difference 2.3e-12. The backend result may differ
  from what is shown here.

This warning is surfaced automatically — no consumer code is required. It is a hint that the formula is sensitive to floating-point representation on the Java backend.

Failure case — unknown variable

If the formula references a variable not present in either fixedParams or dynamicParams, the engine returns an error and the editor displays a red squiggle on the unknown token:

Formula: rate * hours + bonus
Error:   Undefined symbol: bonus

The helper text below the editor shows the error message, and the editor border turns red (Carbon $support-error).


Known Constraints

  • initialFormula is non-reactive. The prop is consumed once on mount via useState. Changing it after mount has no effect. Remount the component with a different React key to force a reset.

  • The visible "Formula" label does not focus Monaco on click. htmlFor cannot target Monaco's auto-generated internal textarea id, so the <label> element is visually connected via aria-labelledby on the wrapper div but does not receive native focus-forwarding on click.

  • Monaco's internal ariaLabel option is hardcoded to 'Formula expression' and does not derive from the ariaLabel prop. Screen readers will announce 'Formula expression' for the editor widget regardless of what ariaLabel is passed to the component. The ariaLabel prop controls only the outer <section> landmark.

  • Horizontal overflow is not scrollable. wordWrap: 'off' and scrollbar: { horizontal: 'hidden' } are set by default. Long formulas that exceed the editor width are clipped; users must navigate with arrow keys.

  • No maximum editor height. The editor grows to fit multi-line content without an upper bound. Layout shift may occur when long formulas are pasted.

  • BigNumber precision is 64 digits. Results beyond this precision will round. The precision-drift warning compares the BigNumber result against a standard JS number (IEEE 754 double) result to surface the most common disagreement with the Java backend.


Change Impact


See Also

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally