Skip to content

FormulaEditorPattern

Paulo Gomes da Cruz Junior edited this page Sep 11, 2026 · 1 revision

Pattern: Formula Editor Integration

Type: Pattern Domain: Frontend — Form / Configuration Applies to: React 19 + TypeScript + Carbon Design System + Monaco Editor Enforced by: react-specialist Last updated: 2026-09-10 Status: Current

Overview

The Formula Editor Integration pattern defines how to embed the FormulaInput component (Monaco-powered math formula editor with BigNumber evaluation) into configuration pages for authoring date-effective formula sets. It covers the complete data flow from API DTOs through TanStack Query hooks to the editor component, including carry-forward logic, validation error mapping, and read-only vs editable modes.

Purpose

Without this pattern, developers would:

  • Re-implement formula editor integration inconsistently across pages
  • Miss the carry-forward logic for new formula sets
  • Handle validation errors differently (or not at all)
  • Confuse fixed vs dynamic parameters in FormulaInput
  • Forget to wire the complete-replace PUT semantics

This pattern ensures every formula configuration page follows the same architecture: service → hooks → page → FormulaInput rows → backend validation → UI error display.

Structure

FormulaConfigurationDetailPage
  ├── useFormulaSetList (carry-forward reference data)
  ├── useCreateFormulaSet / useUpdateFormulaSet / useDeleteFormulaSet (mutations)
  ├── State: area, startDate, endDate, formulas[]
  ├── Computed: isEditable (future open-ended only)
  └── FormulaRow[] (one per formula)
        ├── TextInput for formulaKey
        ├── FormulaInput (Monaco + mathjs BigNumber)
        │   ├── fixedParams: {} (server-authoritative, not used here)
        │   ├── dynamicParams: generated from canonical namespaces
        │   ├── displayResult: true (live preview)
        │   ├── displayDependencyGraph: true (variable usage)
        │   └── onChange: updates local formula state
        └── Validation errors from formula.validationErrors[]

Canonical Example

Source: src/pages/FormulaConfigurationDetail/index.tsx — FormulaConfigurationDetailPage + FormulaRow

// Page-level: mutations and state
const updateMutation = useUpdateFormulaSet();
const createMutation = useCreateFormulaSet();
const deleteMutation = useDeleteFormulaSet();

const [area, setArea] = useState<"INTERIOR" | "COASTAL">("INTERIOR");
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [formulas, setFormulas] = useState<FormulaItemDto[]>([]);

// Carry-forward for new sets
useEffect(() => {
  if (isNew && listData?.content?.length) {
    const recentSets = listData.content.filter(
      (s) => s.area === area && !s.deleted,
    );
    if (recentSets.length > 0) {
      const mostRecent = recentSets[0];
      setFormulas(
        mostRecent.formulas.map((f, idx) => ({
          ...f,
          sortOrder: idx + 1,
          validationErrors: [], // Clear errors for carry-forward
        })),
      );
      setStartDate(formatDate(new Date().toISOString()));
    }
  }
}, [area, isNew, listData]);

// Editable determination
const isEditable = useMemo(() => {
  if (isNew) return true;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const start = new Date(startDate);
  return endDate === "" && start > today;
}, [isNew, startDate, endDate]);

// Save: complete-replace PUT
const handleSave = async () => {
  const dto = {
    area,
    startDate,
    endDate: endDate || null,
    formulas: formulas.map((f, idx) => ({ ...f, sortOrder: idx + 1 })),
  };
  if (isNew) await createMutation.mutateAsync(dto);
  else await updateMutation.mutateAsync({ id: Number(formulaSetId), dto });
};

// Per-row FormulaInput integration
<FormulaInput
  initialFormula={expression}
  onChange={handleExpressionChange}
  readOnly={false}
  displayResult={true}
  displayDependencyGraph={true}
  ariaLabel={`Formula ${index + 1}: ${formulaKey || "unnamed"}`}
  fixedParams={}
  dynamicParams={generateDynamicParams(availableNamespaces)}
/>;

// Validation error display
{
  formula.validationErrors.length > 0 && (
    <div className="formula-row__errors" role="alert">
      {formula.validationErrors.map((err, i) => (
        <div key={i} className="formula-row__error">
          <code>{err.code}</code>: {err.message}
        </div>
      ))}
    </div>
  );
}

Anti-Pattern (What to Avoid)

// ❌ WRONG: Partial update via PATCH (API doesn't support it)
await updateFormulaSet(id, { formulas: [newFormula] }); // Missing area, startDate, endDate

// ❌ WRONG: Using fixedParams for user-editable variables
<FormulaInput
  fixedParams={{ "da.mature.volume": 1000 }} // These are server-authoritative!
  dynamicParams={}
/>;

// ❌ WRONG: Skipping carry-forward for new sets
const [formulas, setFormulas] = useState<FormulaItemDto[]>([]); // Empty, no reference data

// ❌ WRONG: Not clearing validationErrors on carry-forward
setFormulas(mostRecent.formulas); // Errors persist, confusing user

// ❌ WRONG: Allowing edit of closed/historical sets
const isEditable = !set.deleted; // Missing date check!

// ❌ WRONG: Not mapping backend validation errors to UI
// Backend returns 422 with validationErrors[] — must populate formula.validationErrors

// ❌ WRONG: Hardcoding namespaces in multiple places
const namespaces = ["da", "sc", "submission", "hbs", "fta"]; // Duplicated in generateDynamicParams

Why this is wrong:

  • Partial updates violate the complete-replace PUT contract
  • fixedParams are for server-provided constants (e.g., from API), not user-editable variables
  • Carry-forward is a core UX requirement — new sets must start from the latest version
  • Validation errors must be cleared on carry-forward or users see stale errors
  • Editing closed sets breaks the date-effective lifecycle
  • Backend validation errors must surface in the UI per formula row
  • Canonical namespaces defined in one place (generateDynamicParams)

When to Apply

  • Creating any page that edits formula sets (or similar date-effective configuration with math expressions)
  • Integrating FormulaInput into a form with multiple formula rows
  • Implementing carry-forward from a previous version
  • Displaying backend validation errors inline with formula editors

When NOT to Apply

  • Simple forms with a single formula (use FormulaInput directly without the row pattern)
  • Read-only display of formulas (use the read-only branch of FormulaRow)
  • Non-formula configuration (different DTO structure, different validation)

Escape Hatch

  • Custom namespaces: Pass different availableNamespaces to FormulaRow if the domain uses different canonical namespaces
  • Additional fixed params: If the backend provides fixed parameters via API, merge them into fixedParams (e.g., from a separate query)
  • Different validation codes: The error display handles any code/message structure from FormulaValidationError
  • Deviations require ADR — this pattern is tied to the backend lifecycle contract

Detection

Check Method
Complete-replace PUT payload Grep for updateFormulaSet.*dto — ensure area, startDate, endDate, formulas all present
Carry-forward logic Grep for useFormulaSetList + isNew + setFormulas with validationErrors: []
Validation error mapping Grep for validationErrors in FormulaRow — must render err.code + err.message
Editable determination Grep for isEditable — must check endDate === '' AND startDate > today
Fixed vs dynamic params Grep for FormulaInput props — fixedParams should be {} or server data, dynamicParams from generateDynamicParams

Related Patterns

See Also

LESSONS_HINT

This pattern page was generated from the Issue #1223 implementation. Consider proposing an agent-update to react-specialist to add this to its ANALYZE checklist for configuration pages with formula editors.

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally