Formula Rule Builder — Comprehensive Implementation Documentation
Scope: Commits
84d5ee9→63e1dcb(16 commits) insrc/ifile-teapot-web-dynaformsBranch: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'ANDweight > 50, apply the heavy-freight formula."
Key Design Decisions
- Backward-compatible dual mode — A
useRulestoggle switches between legacy (single formula) and rule-based mode. Existing forms continue to work unchanged. - Formula Library — Formulas are defined once in a library and referenced by ID from rules. This avoids duplication and makes formulas reusable.
- Recursive condition trees — Rule conditions support arbitrary nesting with AND/OR groups, enabling complex boolean logic.
- Sequential Priority — Rules are evaluated in order; the first match wins. Drag-and-drop reordering controls priority.
- Field-to-field comparison — Conditions can compare a field against a static value or another form field's live value.
2. Architecture
File Map
| File | Role |
|---|---|
| form-element-config.types.ts | Data model — 4 new interfaces |
| formula-rule-evaluator.util.ts | Runtime evaluation engine |
| rule-condition-builder.component.ts | Recursive condition UI component |
| right-panel-static.component.ts | Builder-side formula/rule management |
| formula-field.component.ts | Runtime 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
useRulesisfalse(or absent), the field behaves in legacy mode with a singleformulastring. Whentrue, theformulaproperty is cleared andformulaLibrary+rulesare 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:
- Initializes
formulaLibraryas an empty array (if not present) - Migrates the existing legacy
formulainto the library as "Migrated Formula" - Sets the migrated formula as the
defaultFormulaId - Initializes an empty
rulesarray - Clears the legacy
formulaproperty
When disabled:
- Restores the default formula back into the
formulaproperty - Clears
formulaLibrary,defaultFormulaId, andrules
4.2 Formula Library Management
The right panel provides full CRUD for the formula library:
| Action | Method | Behavior |
|---|---|---|
| Create | createNewFormula | Creates a new formula shell with a UUID, opens the formula editor tab |
| Edit | editFormulaInLibrary | Deep-clones the formula to avoid mutations until save, syncs the label-formula to the input |
| Save | saveFormulaFromEditor | Validates name, updates-or-adds to the library, auto-sets as default if first formula |
| Delete | deleteFormulaFromLibrary | Blocks deletion if formula is in use by a rule or as default; uses PrimeNG p-confirmDialog |
| Display | getFormulaDisplayLabel | Converts 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
| Action | Method | Behavior |
|---|---|---|
| Create | createNewRule | Creates rule shell with root group condition (AND, empty), opens rule configurator tab |
| Edit | editRuleInList | Deep-clones the rule for editing |
| Save | saveRuleFromConfigurator | Triple validation: name required, formula selection required, condition tree completeness |
| Delete | deleteRuleFromList | Uses PrimeNG p-confirmDialog |
| Reorder | onRuleDrop | Native HTML5 drag-and-drop with visual drag-over feedback |
Condition Tree Validation — isConditionTreeInvalid recursively checks:
- Comparisons: both
fieldandvaluemust 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 ID | Purpose |
|---|---|
main | Default view — formula library list, rules list, default formula selector |
formulaEditor | Formula editing view with @-mention suggestions, operators, and validate button |
ruleConfigurator | Rule 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-selectButtonto 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-selectwith search/filter, bound tocondition.field - Comparator selector —
>,<,>=,<=,==,!= - Value input — Either a text input (static) or a field selector (field reference)
- Toggle button —
pi-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:
bg-blue-200bg-purple-200bg-pink-200bg-orange-200bg-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.fieldfromformValuesto get the left-hand value - Resolves the right-hand value from either a static
condition.valueor another field (whenvalueType === 'field') - Performs numeric comparison using the
condition.comparator - Returns
falsefor null/undefined field values (safe default)
6.2 evaluateRuleCondition (Recursive)
evaluateRuleCondition(condition: RuleCondition, formValues: Record<string, any>): boolean
- Base case (
comparison): delegates toevaluateComparison - Recursive case (
group): evaluates all children, then combines withAND(.every()) orOR(.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
formulaIdof the first rule whose condition evaluates totrue - Returns
nullif 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
fieldand right-handvaluewhenvalueType === 'field')
This is achieved by:
- Concatenating all formula strings from the library
- Recursively extracting fields from all rule conditions via extractFieldsFromCondition
- 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:
- Legacy formulas —
el.formulastring (regex word-boundary replace) - Formula library — All
formulastrings inel.formulaLibrary[] - Rule conditions — Delegates to
updateRuleConditionKeys() - Nested elements — Recurses into
childrenandtabs
8.2 updateRuleConditionKeys
Recursively walks the condition tree and updates:
condition.field(left-hand side) if it matchesoldKeycondition.value(right-hand side) ifvalueType === 'field'and it matchesoldKey
9. Rule Drag-and-Drop Reordering
Rules use native HTML5 drag-and-drop (not a library) for priority reordering:
| Event | Handler | Behavior |
|---|---|---|
dragstart | onRuleDragStart(index) | Stores the dragged rule's index |
dragenter | onRuleDragEnter(index) | Tracks the hovered drop target for visual feedback |
dragleave | onRuleDragLeave() | Clears the hover indicator |
drop | onRuleDrop(dropIndex) | Splices the rule out of old position and inserts at new position |
dragend | onRuleDragEnd() | Resets all drag state |
The visual feedback uses draggedOverIndex to apply a CSS highlight on the hovered rule card.
10. Commit-by-Commit Changelog
| # | Hash | Description |
|---|---|---|
| 1 | 84d5ee9 | Foundation — Added useRules, formulaLibrary, rules, defaultFormulaId to FormulaFieldConfig. Created FormulaDefinition, FormulaRule, RuleCondition interfaces. |
| 2 | dac713e | "Use Rules" toggle — Implemented onUseRulesToggle with automatic formula migration. |
| 3 | 8d2a49c | Formula library CRUD — Added create, edit, save, delete, and display-label methods. Integrated the formula editor tab. |
| 4 | 99207fd | Legacy migration fix — Ensured defaultFormula is migrated to formula when switching back from rules to legacy mode. |
| 5 | 99790fe | Rule condition builder — New RuleConditionBuilderComponent with recursive rendering, AND/OR toggle, comparison rows, and nested groups. Rule CRUD in right panel. |
| 6 | 38efc96 | Drag-and-drop reordering — Native HTML5 drag-and-drop for rule priority reordering. |
| 7 | f473745 | Runtime evaluator — Created formula-rule-evaluator.util.ts with evaluateComparison, evaluateRuleCondition, and findMatchingRule. Updated FormulaFieldComponent to use rule matching. |
| 8 | 443cfe5 | Dynamic panel width — Right panel extended width when formula editor or rule configurator tabs are active. |
| 9 | dc56b40 | UI redesign — Visual nesting with depth-colored bars, operator connector pills (&&/` |
| 10 | 80db274 | Field-to-field comparison — Added valueType property to RuleCondition, toggle between static/field values, updated evaluator. |
| 11 | 42288f0 | Field rename propagation — updateAllFormulas now also updates formula library and calls updateRuleConditionKeys for all rule conditions. |
| 12 | 9c7819e | Default number value — Set default number input value to 0 instead of null. |
| 13 | 74c23fc | Refactoring — Stored depth color in a component property, simplified field selection using available fields directly. |
| 14 | fb424ca | UI polish — Updated formula selection icons/colors, renamed "Edit Formula" → "Configure Formula". |
| 15 | c2ed91f | Toast notifications — Replaced native alert() calls with CommonService.showToast(). Added rule condition validation before save. |
| 16 | 749b1fe | Confirm dialogs — Replaced native confirm() with PrimeNG p-confirmDialog for formula and rule deletion. |
| 17 | 63e1dcb | Field 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
| File | Lines Changed | What Changed |
|---|---|---|
form-element-config.types.ts | +47 | 4 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 | +554 | Formula library CRUD, rule CRUD, drag-and-drop, field rename propagation, condition validation |
right-panel-static.component.html | +467 | Formula library list, rule list, formula editor tab, rule configurator tab |
formula-field.component.ts | +174 | Rule-aware dependency detection, findMatchingRule integration, fallback to default formula |
dynaform-builder.component.ts | +224 | Dynamic right panel width based on active tab |
dynaform.config.ts | +5 | Configuration constants |
dynaform.service.ts | +10 | Service updates for formula validation |
left-panel.config.ts | +2 | Minor config update |
| Total | ~1943 lines |