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
-
validationRulesinBaseFieldConfig: 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-defineddisplayKey(error message text) mapped to theerrorKey.
-
FormulaBuilderComponentEnhancements:- Extracted formula building logic into a standalone, reusable
FormulaBuilderComponent. - Added a
modeinput ('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.
- Extracted formula building logic into a standalone, reusable
-
Error Message Binding (
error-i18n-parent.component.ts):- The UI correctly displays the associated error messages mapping the
errorKeyseamlessly without cluttering the component's underlying structures. - Refined
displayKeygeneration to use clear dot notation (df.error.[jsonKey].[errorKey]).
- The UI correctly displays the associated error messages mapping the
Form Renderer Engine (Runtime Validation)
-
attachFormulaValidatorsinFormEngineService:- Central orchestrator called at the end of
buildForm(). - Takes a flat list of elements with
validationRulescollected during the singleaddControlsRecursive()pass (avoids redundant tree traversal). - Extracts dependent field keys directly from the formula string using regex matching.
- Central orchestrator called at the end of
-
Reactive Subscriptions & Evaluation (
attachRuleValidation):- Tracks dependencies and triggers the
/evaluateAPI viaDynaformService. - Error Assignment: Dynamically sets or clears the unique
errorKeydirectly on the AngularFormControl's standarderrorsobject without overwriting existing built-in errors (likerequired). - Cross-Field Reactivity: Subscribes to the
valueChangesof every dependent field to re-trigger validation continuously.
- Tracks dependencies and triggers the
-
Deep Field Resolution (
findControlutility):- Resolves
FormControlinstances regardless of how deeply nested they are withinFormGroupconfigurations (such as fields placed insideisParent: truecontainers like sections or tabs).
- Resolves
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 intoFormControlmodels.error-i18n-groupexplicitly requires predefined static events natively; utilizingngModelseparated dynamic JSON metadata injection safely from formal Angular validation lifecycles. -
Non-Invasive Rendering: The validation relies completely on the Angular
AbstractControl.errorsdictionary. Because of this, the existingFieldWrapperComponentwas capable of iterating over the errors and fetching the respectivedisplayKeyfromerrorMsgMetadatawithout requiring any changes to the renderer's display logic. -
Centralized Engine Utility: Created and housed
findControlinside the existingformula-rule-evaluator.util.tsrather than building a fragmentedform-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
yearsOfExperiencecorrectly re-evaluated the validation ofbonusPercentage. - 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.