MD.OFFICE
FAL

Formula-based Conditional Field Validations

Project: ifile-teapot-web-dynaforms Date: 2026-03-09 Developer: Falguni

Overview

The Formula-based Conditional Field Validations feature allows form builders to define complex, dynamic validation rules for fields based on mathematical or logical formulas involving other fields in the form. If a formula evaluates to false, a custom error message is displayed on the field.

This system bridges the gap between the static form definition (Dynaform Builder) and the dynamic runtime evaluation (Form Renderer Engine).

Implementation Details

Data Model & Builder UI

  1. validationRules in BaseFieldConfig: Fields now accept an array of validation rules in their JSON configuration. Each rule contains:

    • formula: The expression to evaluate (e.g., bonusPercentage < 100).
    • errorKey: A unique identifier for this specific rule's error state (e.g., formulaValidation_xyz123).
    • errorMsgMetadata: Stores the user-defined displayKey (error message text) mapped to the errorKey.
  2. FormulaBuilderComponent Enhancements:

    • Extracted formula building logic into a standalone, reusable FormulaBuilderComponent.
    • Added a mode input ('calculation' | 'validation') to adapt the UI context.
    • For validation mode, the builder strictly enforces the formula output as a boolean expression.
    • Popover Positioning: Replaced standard dropdowns with PrimeNG <p-popover appendTo="body"> to prevent the suggestions list from being cut off by containing accordions.
  3. Error Message Binding (error-i18n-parent.component.ts):

    • The UI correctly displays the associated error messages mapping the errorKey seamlessly without cluttering the component's underlying structures.
    • Refined displayKey generation to use clear dot notation (df.error.[jsonKey].[errorKey]).

Form Renderer Engine (Runtime Validation)

  1. attachFormulaValidators in FormEngineService:

    • Central orchestrator called at the end of buildForm().
    • Takes a flat list of elements with validationRules collected during the single addControlsRecursive() pass (avoids redundant tree traversal).
    • Extracts dependent field keys directly from the formula string using regex matching.
  2. Reactive Subscriptions & Evaluation (attachRuleValidation):

    • Tracks dependencies and triggers the /evaluate API via DynaformService.
    • Error Assignment: Dynamically sets or clears the unique errorKey directly on the Angular FormControl's standard errors object without overwriting existing built-in errors (like required).
    • Cross-Field Reactivity: Subscribes to the valueChanges of every dependent field to re-trigger validation continuously.
  3. Deep Field Resolution (findControl utility):

    • Resolves FormControl instances regardless of how deeply nested they are within FormGroup configurations (such as fields placed inside isParent: true containers like sections or tabs).

Design Decisions

  • Error Label Data Binding (error-i18n-parent): We specifically decided to stick with simple [(ngModel)] mapping for dynamic validation metadata strings rather than attempting to sync them into FormControl models. error-i18n-group explicitly requires predefined static events natively; utilizing ngModel separated dynamic JSON metadata injection safely from formal Angular validation lifecycles.

  • Non-Invasive Rendering: The validation relies completely on the Angular AbstractControl.errors dictionary. Because of this, the existing FieldWrapperComponent was capable of iterating over the errors and fetching the respective displayKey from errorMsgMetadata without requiring any changes to the renderer's display logic.

  • Centralized Engine Utility: Created and housed findControl inside the existing formula-rule-evaluator.util.ts rather than building a fragmented form-control.util.ts. It logically groups all formula evaluation tree-traversal logic together.

Issues Encountered & Solutions

1. Error only showed on submit, not on dirty

Problem: FieldWrapperComponent.showErrors requires a field to be dirty or touched. Validation errors sets on the control naturally when dependency fields changed were not showing until submission because the validated field itself wasn't "dirty." Solution: Added a deliberate self-subscription to the validated field's own valueChanges. This triggers dirtiness when the user types in it, so showErrors passes. Self-referential keys are strictly filtered out of the actual /evaluate payload.

2. Redundant second tree traversal

Problem: After building the form, we needed a second recursive loop over the entire element tree to find validation rules. Solution: Added a formulaValidationElements: FormElementConfig[] field inside FormEngineService. During the initial addControlsRecursive() pass (step 4 for leaves), any element bearing validation rules is pushed into this flat array. attachFormulaValidators() simply iterates this array, yielding O(1) collection.

3. Flat lookups failed for nested isParent: true fields

Problem: Hardcoded form.get(jsonKey) lookups failed when fields were nested inside containers (e.g., form.get('sectionKey.fieldKey')), resulting in undefined variable API errors and broken reactivity. Solution: Created a recursive depth-first findControl(group, jsonKey) search utility that walks all nested child FormGroup structures effectively resolving deeply nested controls seamlessly.

4. Subscription Memory Leaks

Problem: On programmatic form rebuilds (e.g. dynamic state changes, view destructions), subscriptions continuously accumulated, causing heavy memory leaks and bizarre multi-trigger behaviors. Solution: Consolidated all listeners into a single formulaValidationSub = new Subscription() aggregate within FormEngineService. On each buildForm() initiation, the old subscription is thoroughly unsubscribed from and reset.

5. Formula Builder Dropdown Cut-off in Accordions

Problem: The suggestion dropdown in the FormulaBuilderComponent was a standard custom <div>. Because it sat inside a PrimeNG accordion (which uses overflow: hidden for animations), the dropdown was being clipped. Solution: Wrapped the suggestion list in a PrimeNG <p-popover> and used appendTo="body" to move it out of the accordion's stacking context.

6. Popover Lost Width Context After appendTo="body"

Problem: After detaching the popover and appending it to <body>, the standard Tailwind w-full class stopped working because it no longer knew it was supposed to match the width of the formula input box. Solution: Hooked into the popover's (onShow) event, obtained the actual offsetWidth of the input field, and manually mutated popover.container.style.width to match it precisely.

Verification

  • Dependency Tracking: Verified changes to fields like yearsOfExperience correctly re-evaluated the validation of bonusPercentage.
  • Nested Testing: Proven to properly traverse, extract values, and set validation statuses on controls placed deeply inside section (isParent: true) wrappers.
  • Error Lifecycle Checks: Standard built-in errors (like required) were not erased when custom formula validation dynamically passed or failed.
  • UI Feedback: Confirmed error message nodes correctly appear dynamically inside the UI wrapper components when invalidating paths.