MD.OFFICE
FAL

Formula Rule Builder — Comprehensive Implementation Documentation

Scope: Commits 84d5ee963e1dcb (16 commits) in src/ifile-teapot-web-dynaforms Branch: feature/df-rule-builder-in-formula-field


1. Overview & Motivation

Previously, a formula field in a dynaform had a single, static formula expression (e.g. fieldA + fieldB * 2). Every form submission always evaluated the same expression.

The Formula Rule Builder extends formula fields with conditional, rule-based logic — allowing form designers to define multiple formulas and choose which one to evaluate at runtime based on the current values of other form fields. This enables scenarios like:

  • "If quantity > 100, use a bulk-discount formula; otherwise use the standard formula."
  • "If region == 'EU' AND weight > 50, apply the heavy-freight formula."

Key Design Decisions

  1. Backward-compatible dual mode — A useRules toggle switches between legacy (single formula) and rule-based mode. Existing forms continue to work unchanged.
  2. Formula Library — Formulas are defined once in a library and referenced by ID from rules. This avoids duplication and makes formulas reusable.
  3. Recursive condition trees — Rule conditions support arbitrary nesting with AND/OR groups, enabling complex boolean logic.
  4. Sequential Priority — Rules are evaluated in order; the first match wins. Drag-and-drop reordering controls priority.
  5. Field-to-field comparison — Conditions can compare a field against a static value or another form field's live value.

2. Architecture

File Map

FileRole
form-element-config.types.tsData model — 4 new interfaces
formula-rule-evaluator.util.tsRuntime evaluation engine
rule-condition-builder.component.tsRecursive condition UI component
right-panel-static.component.tsBuilder-side formula/rule management
formula-field.component.tsRuntime formula field with rule matching

3. Data Model

Four new TypeScript interfaces were added to form-element-config.types.ts:

3.1 FormulaFieldConfig

interface FormulaFieldConfig extends BaseFieldConfig {
  type: 'formula';
  formula?: string;                    // LEGACY: Single formula expression
  useRules?: boolean;                  // Toggle between legacy and rule-based mode
  defaultFormulaId?: string;           // Fallback formula when no rules match
  formulaLibrary?: FormulaDefinition[];// Library of named formulas
  rules?: FormulaRule[];               // Ordered list of conditional rules
}

[!IMPORTANT] When useRules is false (or absent), the field behaves in legacy mode with a single formula string. When true, the formula property is cleared and formulaLibrary + rules are used instead.

3.2 FormulaDefinition

interface FormulaDefinition {
  id: string;      // Unique identifier (e.g. "formula-<uuid>")
  name?: string;   // Human-readable name
  formula: string; // The expression (e.g. "fieldA + fieldB * 0.18")
}

3.3 FormulaRule

interface FormulaRule {
  uuid: string;              // Unique identifier
  name?: string;             // Human-readable name
  condition: RuleCondition;  // Condition tree that must evaluate to true
  formulaId: string;         // ID of the formula to use when matched
}

3.4 RuleCondition (Recursive)

interface RuleCondition {
  type: 'group' | 'comparison';

  // For type === 'group'
  operator?: 'AND' | 'OR';
  conditions?: RuleCondition[];  // Nested children (recursive)

  // For type === 'comparison'
  field?: string;                // jsonKey of the left-hand field
  comparator?: '>' | '<' | '>=' | '<=' | '==' | '!=';
  valueType?: 'static' | 'field'; // How to interpret 'value'
  value?: string | number;        // Static value OR another field's jsonKey
}

Data Flow Diagram


4. Builder-Side Implementation (Design Time)

4.1 "Use Rules" Toggle — onUseRulesToggle

When enabled:

  1. Initializes formulaLibrary as an empty array (if not present)
  2. Migrates the existing legacy formula into the library as "Migrated Formula"
  3. Sets the migrated formula as the defaultFormulaId
  4. Initializes an empty rules array
  5. Clears the legacy formula property

When disabled:

  1. Restores the default formula back into the formula property
  2. Clears formulaLibrary, defaultFormulaId, and rules

4.2 Formula Library Management

The right panel provides full CRUD for the formula library:

ActionMethodBehavior
CreatecreateNewFormulaCreates a new formula shell with a UUID, opens the formula editor tab
EditeditFormulaInLibraryDeep-clones the formula to avoid mutations until save, syncs the label-formula to the input
SavesaveFormulaFromEditorValidates name, updates-or-adds to the library, auto-sets as default if first formula
DeletedeleteFormulaFromLibraryBlocks deletion if formula is in use by a rule or as default; uses PrimeNG p-confirmDialog
DisplaygetFormulaDisplayLabelConverts jsonKey-based formula to human-readable label formula for display

The formula editor reuses the existing formulaInput infrastructure (suggestions, @-mention field picker, validation via API) — it simply routes saves to the library instead of directly to item.formula.

4.3 Rule Management

ActionMethodBehavior
CreatecreateNewRuleCreates rule shell with root group condition (AND, empty), opens rule configurator tab
EditeditRuleInListDeep-clones the rule for editing
SavesaveRuleFromConfiguratorTriple validation: name required, formula selection required, condition tree completeness
DeletedeleteRuleFromListUses PrimeNG p-confirmDialog
ReorderonRuleDropNative HTML5 drag-and-drop with visual drag-over feedback

Condition Tree ValidationisConditionTreeInvalid recursively checks:

  • Comparisons: both field and value must be non-empty
  • Groups: must have at least one child, and all children must be valid

4.4 Right Panel Tab Navigation

The right panel uses a tab system controlled by activeTabForRightPanel:

Tab IDPurpose
mainDefault view — formula library list, rules list, default formula selector
formulaEditorFormula editing view with @-mention suggestions, operators, and validate button
ruleConfiguratorRule editing view with name input, formula selector, and the condition builder

5. Rule Condition Builder Component

rule-condition-builder.component.ts is a standalone, recursive Angular component that renders a nested condition tree.

5.1 Component Architecture

@Input()  condition: RuleCondition   — The current node (group or comparison)
@Input()  depth: number              — Nesting level for visual styling
@Input()  availableFields            — List of {label, value} pairs for field selectors
@Output() conditionChange            — Emits the updated condition tree upward

The component renders itself recursively for nested groups:

<app-rule-condition-builder
  [condition]="nestedCondition"
  [availableFields]="availableFields"
  [depth]="depth + 1"
  (conditionChange)="onNestedConditionChange(i, $event)"
/>

5.2 Visual Components

Group Node renders:

  • AND/OR Toggle — PrimeNG p-selectButton to switch the logical operator
  • + Condition button — Adds a new comparison child
  • + Group button — Adds a nested group child
  • Nested children — Each with a remove button (visible on hover)
  • Operator connectors&& or || pills between sibling conditions
  • Empty state — Dashed border with "Define logic for this group" prompt

Comparison Node renders a single row:

  • Field selector — PrimeNG p-select with search/filter, bound to condition.field
  • Comparator selector>, <, >=, <=, ==, !=
  • Value input — Either a text input (static) or a field selector (field reference)
  • Toggle buttonpi-hashtag (static) / pi-at (field) icon to switch between value types

5.3 Visual Nesting

Nesting depth is indicated by a colored vertical bar on the left side, cycling through:

  1. bg-blue-200
  2. bg-purple-200
  3. bg-pink-200
  4. bg-orange-200
  5. bg-green-200

This creates a "git-graph-like" visual effect that makes nesting immediately visible.

5.4 Self-Comparison Prevention

When valueType === 'field', the value selector is filtered via updateFilteredFields to exclude the currently selected left-hand field, preventing fieldA > fieldA nonsense.


6. Runtime Evaluation Engine

formula-rule-evaluator.util.ts contains three pure utility functions:

6.1 evaluateComparison

evaluateComparison(condition: RuleCondition, formValues: Record<string, any>): boolean
  • Resolves condition.field from formValues to get the left-hand value
  • Resolves the right-hand value from either a static condition.value or another field (when valueType === 'field')
  • Performs numeric comparison using the condition.comparator
  • Returns false for null/undefined field values (safe default)

6.2 evaluateRuleCondition (Recursive)

evaluateRuleCondition(condition: RuleCondition, formValues: Record<string, any>): boolean
  • Base case (comparison): delegates to evaluateComparison
  • Recursive case (group): evaluates all children, then combines with AND (.every()) or OR (.some())
  • Empty groups default to false

6.3 findMatchingRule

findMatchingRule(rules: FormulaRule[], formValues: Record<string, any>): string | null
  • Iterates rules in order (sequential priority)
  • Returns the formulaId of the first rule whose condition evaluates to true
  • Returns null if no rules match

7. Runtime Formula Field Component

formula-field.component.ts is the runtime renderer that evaluates formulas during form filling.

7.1 Initialization Flow

7.2 Dependency Detection — getFormulaFieldNames

When useRules is enabled, the component must watch all fields that could influence the result:

  • Fields in every formula in the library (not just the currently active one)
  • Fields in every rule condition (both left-hand field and right-hand value when valueType === 'field')

This is achieved by:

  1. Concatenating all formula strings from the library
  2. Recursively extracting fields from all rule conditions via extractFieldsFromCondition
  3. Merging both sets and subscribing to their valueChanges

7.3 Evaluation Flow — evaluateFormula


8. Field Rename Propagation

When a form designer renames a field label (which changes its jsonKey), all formula references must be updated. This is handled by two methods in right-panel-static.component.ts:

8.1 updateAllFormulas

Recursively traverses all canvas elements and updates:

  1. Legacy formulasel.formula string (regex word-boundary replace)
  2. Formula library — All formula strings in el.formulaLibrary[]
  3. Rule conditions — Delegates to updateRuleConditionKeys()
  4. Nested elements — Recurses into children and tabs

8.2 updateRuleConditionKeys

Recursively walks the condition tree and updates:

  • condition.field (left-hand side) if it matches oldKey
  • condition.value (right-hand side) if valueType === 'field' and it matches oldKey

9. Rule Drag-and-Drop Reordering

Rules use native HTML5 drag-and-drop (not a library) for priority reordering:

EventHandlerBehavior
dragstartonRuleDragStart(index)Stores the dragged rule's index
dragenteronRuleDragEnter(index)Tracks the hovered drop target for visual feedback
dragleaveonRuleDragLeave()Clears the hover indicator
droponRuleDrop(dropIndex)Splices the rule out of old position and inserts at new position
dragendonRuleDragEnd()Resets all drag state

The visual feedback uses draggedOverIndex to apply a CSS highlight on the hovered rule card.


10. Commit-by-Commit Changelog

#HashDescription
184d5ee9Foundation — Added useRules, formulaLibrary, rules, defaultFormulaId to FormulaFieldConfig. Created FormulaDefinition, FormulaRule, RuleCondition interfaces.
2dac713e"Use Rules" toggle — Implemented onUseRulesToggle with automatic formula migration.
38d2a49cFormula library CRUD — Added create, edit, save, delete, and display-label methods. Integrated the formula editor tab.
499207fdLegacy migration fix — Ensured defaultFormula is migrated to formula when switching back from rules to legacy mode.
599790feRule condition builder — New RuleConditionBuilderComponent with recursive rendering, AND/OR toggle, comparison rows, and nested groups. Rule CRUD in right panel.
638efc96Drag-and-drop reordering — Native HTML5 drag-and-drop for rule priority reordering.
7f473745Runtime evaluator — Created formula-rule-evaluator.util.ts with evaluateComparison, evaluateRuleCondition, and findMatchingRule. Updated FormulaFieldComponent to use rule matching.
8443cfe5Dynamic panel width — Right panel extended width when formula editor or rule configurator tabs are active.
9dc56b40UI redesign — Visual nesting with depth-colored bars, operator connector pills (&&/`
1080db274Field-to-field comparison — Added valueType property to RuleCondition, toggle between static/field values, updated evaluator.
1142288f0Field rename propagationupdateAllFormulas now also updates formula library and calls updateRuleConditionKeys for all rule conditions.
129c7819eDefault number value — Set default number input value to 0 instead of null.
1374c23fcRefactoring — Stored depth color in a component property, simplified field selection using available fields directly.
14fb424caUI polish — Updated formula selection icons/colors, renamed "Edit Formula" → "Configure Formula".
15c2ed91fToast notifications — Replaced native alert() calls with CommonService.showToast(). Added rule condition validation before save.
16749b1feConfirm dialogs — Replaced native confirm() with PrimeNG p-confirmDialog for formula and rule deletion.
1763e1dcbField filtering & validation — Prevent self-comparison in rule conditions, formula name validation, and editor state protection on item selection change.

11. Summary of Changes by File

FileLines ChangedWhat Changed
form-element-config.types.ts+474 new interfaces (FormulaFieldConfig extended, FormulaDefinition, FormulaRule, RuleCondition)
formula-rule-evaluator.util.ts+132 (NEW)Recursive condition evaluator with 3 exported functions
rule-condition-builder.component.ts+205 (NEW)Recursive Angular component for building condition trees
rule-condition-builder.component.html+202 (NEW)Template with group/comparison rendering, nesting indicators
rule-condition-builder.component.scss+129 (NEW)Custom PrimeNG overrides for compact selects, operator toggles
right-panel-static.component.ts+554Formula library CRUD, rule CRUD, drag-and-drop, field rename propagation, condition validation
right-panel-static.component.html+467Formula library list, rule list, formula editor tab, rule configurator tab
formula-field.component.ts+174Rule-aware dependency detection, findMatchingRule integration, fallback to default formula
dynaform-builder.component.ts+224Dynamic right panel width based on active tab
dynaform.config.ts+5Configuration constants
dynaform.service.ts+10Service updates for formula validation
left-panel.config.ts+2Minor config update
Total~1943 lines