A Senior Developer’s Guide to Dynamic Visibility and Mandatory States
Most modern enterprise applications depend heavily on forms. These forms are rarely static. Their behaviour usually depends on user selections, configuration rules, user roles, or backend responses. Simple conditional visibility like showing a GST field only when the user selects a business account is common. But as applications grow, the number of conditions grows even faster. Hardcoded conditions scattered across components become difficult to maintain, test, or audit.
To solve this problem, many teams build a Form Field Behaviour Rules Engine. It is a systematic way to control form visibility, mandatory rules, enable/disable behaviour, and default values through configuration instead of code branching.
In this article, we will build a production-ready rules engine for Angular forms. The focus is on:
Clean and scalable architecture
Real-world patterns used in enterprise systems
JSON-driven rule configuration
Working Angular code
Best practices for maintainability and performance
This article is written for senior developers who want a strong architectural foundation and ready-to-use code instead of theoretical discussions.
1. Why a Rules Engine Instead of Hardcoded Logic
Let us start with the common problem. Many Angular forms begin like this:
if (this.form.get('accountType')?.value === 'Business') {
this.form.get('gstNumber')?.setValidators([Validators.required]);
this.form.get('gstNumber')?.updateValueAndValidity();
this.showGstNumber = true;
}
It is fine for one or two conditions. But eventually, you will find code like:
if (country == 'IN' && accountType == 'Business' && userRole == 'Admin') {
...
} else if (country == 'US' && subscription == 'Enterprise' && age < 18) {
...
}
What begins as small condition blocks becomes unmanageable:
Hard to read
Hard to maintain
Hard to test
Hard to audit
Hard to update when business rules change
A Rules Engine solves these problems by separating:
Rules are defined declaratively. Angular runtime applies rules using a consistent engine. This keeps business logic away from component logic.
2. Defining the Behaviour Model
Before writing any Angular code, define the behaviour model. A good rules engine usually has these concepts:
2.1 Field Behaviour Attributes
Every form field can be controlled by rules:
Visible or hidden
Mandatory or optional
Enabled or disabled
Default values
Validations
Computed values
In this article we focus on the first two:
Visibility
Mandatory state
These alone solve 80 percent of real form behaviour needs.
2.2 Triggers
Rules are triggered when some field value changes.
Examples:
Country changes
Account Type changes
User selects a checkbox
User enters a number
2.3 Conditions
A rule is applied only if its condition evaluates to true.
Example
"condition": {
"field": "accountType",
"operator": "equals",
"value": "Business"
}
More complex rules can combine multiple conditions using AND/OR.
2.4 Actions
Actions define what to do when conditions are true:
A simple action:
"action": {
"target": "gstNumber",
"visibility": "show",
"mandatory": true
}
2.5 Rules List
Rules engine processes a list of rules:
{
"rules": [
{
"condition": { ... },
"action": { ... }
},
{
"conditionGroup": { "and": [ ... ] },
"action": { ... }
}
]
}
This gives us a scalable JSON structure. Now let us move to implementation.
3. Designing the Angular Architecture
A clean architecture may look like this:
app/
rules/
rules-engine.service.ts
rule-parser.ts
models/
behaviour-rule.ts
condition.ts
action.ts
forms/
customer-form/
customer-form.component.ts
customer-form.config.ts
customer-behaviour.rules.json
3.1 Principles to Follow
A senior-team-ready rules engine should follow these:
Rules engine is framework independent
Angular integration happens through a wrapper service
Rules and form controls must not depend on each other directly
No circular dependencies
Avoid tight coupling with reactive forms
3.2 Angular Reactive Forms as Foundation
We use Angular Reactive Forms because they:
allow programmatic control
offer strong validation APIs
are suitable for dynamic forms
integrate naturally with rule evaluation
4. Rule Configuration Example (JSON)
Let us take a common business case.
Scenario
Show GST Number only for Business accounts.
Make GST Number mandatory when visible.
Hide it and clear its value when not Business.
Rules file: customer-behaviour.rules.json
{
"rules": [
{
"id": "business-gst-visibility",
"condition": {
"field": "accountType",
"operator": "equals",
"value": "Business"
},
"action": {
"target": "gstNumber",
"visibility": "show",
"mandatory": true
}
},
{
"id": "non-business-hide-gst",
"condition": {
"field": "accountType",
"operator": "notEquals",
"value": "Business"
},
"action": {
"target": "gstNumber",
"visibility": "hide",
"mandatory": false,
"clearValue": true
}
}
]
}
5. Implementing the Angular Rules Engine
Step 1: Create Models
behaviour-rule.ts
export interface Condition {
field: string;
operator: 'equals' | 'notEquals' | 'in' | 'notIn' | 'greaterThan' | 'lessThan';
value: any;
}
export interface Action {
target: string;
visibility?: 'show' | 'hide';
mandatory?: boolean;
clearValue?: boolean;
}
export interface BehaviourRule {
id: string;
condition: Condition;
action: Action;
}
Step 2: Rule Evaluator
rule-parser.ts
export class RuleEvaluator {
evaluate(condition: Condition, formValue: any): boolean {
const fieldVal = formValue[condition.field];
switch (condition.operator) {
case 'equals':
return fieldVal === condition.value;
case 'notEquals':
return fieldVal !== condition.value;
case 'in':
return Array.isArray(condition.value) && condition.value.includes(fieldVal);
case 'notIn':
return Array.isArray(condition.value) && !condition.value.includes(fieldVal);
case 'greaterThan':
return Number(fieldVal) > Number(condition.value);
case 'lessThan':
return Number(fieldVal) < Number(condition.value);
default:
return false;
}
}
}
This evaluator is pure and testable. It does not depend on Angular. This is important for unit tests.
Step 3: Rules Engine Service
This service integrates the evaluator with Angular Reactive Forms.
rules-engine.service.ts
import { Injectable } from '@angular/core';
import { FormGroup, Validators } from '@angular/forms';
import { BehaviourRule } from './models/behaviour-rule';
import { RuleEvaluator } from './rule-parser';
@Injectable({ providedIn: 'root' })
export class RulesEngineService {
private evaluator = new RuleEvaluator();
applyRules(rules: BehaviourRule[], form: FormGroup): void {
const formValue = form.getRawValue();
rules.forEach(rule => {
const conditionMet = this.evaluator.evaluate(rule.condition, formValue);
if (conditionMet) {
this.applyAction(rule.action, form);
}
});
}
private applyAction(action: any, form: FormGroup): void {
const control = form.get(action.target);
if (!control) return;
if (action.visibility === 'show') {
control.enable({ emitEvent: false });
}
if (action.visibility === 'hide') {
control.disable({ emitEvent: false });
if (action.clearValue) {
control.setValue(null, { emitEvent: false });
}
}
if (action.mandatory === true) {
control.setValidators([Validators.required]);
control.updateValueAndValidity({ emitEvent: false });
}
if (action.mandatory === false) {
control.clearValidators();
control.updateValueAndValidity({ emitEvent: false });
}
}
}
Key Points
Disable a field to treat it as invisible to user.
Use emitEvent: false to avoid recursive rule triggers.
Every rule applies on the latest form state.
The engine can run after each form value change.
6. Wiring the Engine in an Angular Component
Form Initialization
customer-form.component.ts
@Component({
selector: 'app-customer-form',
templateUrl: './customer-form.component.html'
})
export class CustomerFormComponent implements OnInit {
form: FormGroup;
rules: BehaviourRule[] = [];
constructor(
private fb: FormBuilder,
private rulesEngine: RulesEngineService
) {}
ngOnInit() {
this.buildForm();
this.loadRules();
this.listenForChanges();
}
buildForm() {
this.form = this.fb.group({
accountType: [''],
gstNumber: [''],
country: ['']
});
}
loadRules() {
import('./customer-behaviour.rules.json').then(r => {
this.rules = r.rules;
this.rulesEngine.applyRules(this.rules, this.form);
});
}
listenForChanges() {
this.form.valueChanges.subscribe(() => {
this.rulesEngine.applyRules(this.rules, this.form);
});
}
}
Template Example
customer-form.component.html
<div>
<label>Account Type</label>
<select formControlName="accountType">
<option value="Individual">Individual</option>
<option value="Business">Business</option>
</select>
</div>
<div *ngIf="form.get('gstNumber')?.enabled">
<label>GST Number</label>
<input type="text" formControlName="gstNumber">
</div>
7. Adding Support for AND Conditions
Real projects require multiple conditions:
Show field only when:
accountType is Business
country is IN
Extend the rule model:
{
"conditionGroup": {
"and": [
{ "field": "accountType", "operator": "equals", "value": "Business" },
{ "field": "country", "operator": "equals", "value": "IN" }
]
}
}
Extend evaluator:
evaluateGroup(group: any, formValue: any): boolean {
if (group.and) {
return group.and.every((c: Condition) => this.evaluate(c, formValue));
}
if (group.or) {
return group.or.some((c: Condition) => this.evaluate(c, formValue));
}
return false;
}
Rules Engine updates to check condition or conditionGroup.
8. Clearing Hidden Fields: Why It Matters
When a field is hidden, leaving stale data is dangerous:
This is why the rules engine supports "clearValue": true.
It keeps the form clean and predictable.
9. Performance and Stability Considerations
9.1 Avoid Reprocessing All Rules Unnecessarily
If rules are many (50+), re-evaluating all of them on every keystroke becomes expensive.
Better approach:
Track which fields can trigger which rules
Apply only rules linked to that field
For example:
private rulesMap = new Map<string, BehaviourRule[]>();
private buildRuleMap(rules: BehaviourRule[]) {
rules.forEach(rule => {
const triggerField = rule.condition.field;
if (!this.rulesMap.has(triggerField)) {
this.rulesMap.set(triggerField, []);
}
this.rulesMap.get(triggerField)?.push(rule);
});
}
Then in valueChanges:
form.get(changedField)?.valueChanges.subscribe(() => {
const relevantRules = this.rulesMap.get(changedField) || [];
this.rulesEngine.applyRules(relevantRules, this.form);
});
This improves performance significantly.
10. Validation Strategy
A common mistake is applying validators inside components.
Instead:
Keep static validators in Angular form configuration
Keep dynamic mandatory conditions in rules engine
This separation keeps rules easy to reason about.
11. Handling Conflicting Rules
Two rules may conflict:
Rule A: Make field mandatory
Rule B: Make field optional
To avoid conflicts:
Define rule priority
Process rules in sorted order
Example:
rules.sort((a, b) => (a.priority || 0) - (b.priority || 0));
12. Testing the Rules Engine
Unit tests for evaluator:
it('should evaluate equals condition', () => {
const evaluator = new RuleEvaluator();
const result = evaluator.evaluate(
{ field: 'type', operator: 'equals', value: 'A' },
{ type: 'A' }
);
expect(result).toBe(true);
});
Unit tests for actions:
it('should mark field mandatory', () => {
const engine = new RulesEngineService();
const form = new FormGroup({ x: new FormControl('') });
engine.applyRules([
{
id: 'm1',
condition: { field: 'x', operator: 'equals', value: '' },
action: { target: 'x', mandatory: true }
}
], form);
expect(form.get('x')?.hasValidator(Validators.required)).toBe(true);
});
Testing makes the engine safe for long-term enterprise usage.
13. Real World Use Cases
13.1 Tax Forms
Different states require different tax fields.
Rules engine manages state-specific visibility.
13.2 Insurance Applications
Many questions appear only after the user selects a particular coverage type.
13.3 Loan Applications
Mandatory document lists change by borrower category.
13.4 Registration Forms
Rules change based on user roles, age, region, subscription, or product type.
Rules engine makes all of these maintainable.
14. Versioning Rules
Business rules change regularly.
Use versioned JSON files:
rules/
customer/
v1/
v2/
Save version metadata:
{
"version": "2.1.0",
"rules": [ ... ]
}
This helps rollback and audit.
15. Externalising Rules to a Backend
Large systems load rules from backend APIs.
Advantages:
Rules updated without redeploying Angular app
Centralised business logic
Multi-language support (mobile, web)
Versioning and access control
Angular only loads JSON and applies it.
16. Production-Ready Guidelines
Never mutate the original rule set
Always disable hidden fields instead of removing them
Keep evaluator pure and independent
Cache rules for performance
Log rule hits for debugging
Strictly separate view logic from rule logic
Use debounceTime for high-frequency fields
Combine with feature flags when needed
Conclusion
A Form Field Behaviour Rules Engine is one of the most valuable tools for large Angular applications. It reduces complexity, simplifies maintenance, improves scalability, and allows business teams to change rules without touching component code.
In this article, we built a complete and production-ready engine that manages:
Dynamic visibility
Mandatory state
Rule-based field behaviour
JSON-driven configuration
Testability
Performance optimisation
Maintainable architecture