A Guide for Senior Developers on Mandatory States and Dynamic Visibility
Forms play a major role in the majority of contemporary business applications. Seldom are these shapes static. Typically, user choices, configuration rules, user roles, or backend replies determine how they behave. It is typical to see simple conditional visibility, such as displaying a GST field only when the user chooses a business account. However, the number of criteria increases even more quickly as applications do. It becomes challenging to manage, test, or audit hardcoded conditions that are dispersed among components.

Many teams create a Form Field Behavior Rules Engine to address this issue. Instead of using code branching, it is a methodical technique to regulate form visibility, required rules, enable/disable behavior, and default values using configuration.
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:
- Form structure (Angular code)
- Form behaviour rules (configuration)
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:
- show or hide a field
- set mandatory or optional
- clear value when hidden
- reset validators
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:
- Wrong information submitted
- Backend rejects request
- User gets validation errors at final step
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
For big Angular applications, one of the most useful technologies is a Form Field Behavior Rules Engine. It enhances scalability, lowers complexity, makes maintenance easier, and enables business teams to modify rules without modifying component code.