Angular  

Building a Form Field Behaviour Rules Engine in Angular

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:

  1. Clean and scalable architecture

  2. Real-world patterns used in enterprise systems

  3. JSON-driven rule configuration

  4. Working Angular code

  5. 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:

  1. Hard to read

  2. Hard to maintain

  3. Hard to test

  4. Hard to audit

  5. 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:

  1. Visibility

  2. 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:

  1. Rules engine is framework independent

  2. Angular integration happens through a wrapper service

  3. Rules and form controls must not depend on each other directly

  4. No circular dependencies

  5. 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

  1. Disable a field to treat it as invisible to user.

  2. Use emitEvent: false to avoid recursive rule triggers.

  3. Every rule applies on the latest form state.

  4. 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:

  1. accountType is Business

  2. 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:

  1. Track which fields can trigger which rules

  2. 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:

  1. Keep static validators in Angular form configuration

  2. 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:

  1. Define rule priority

  2. 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:

  1. Rules updated without redeploying Angular app

  2. Centralised business logic

  3. Multi-language support (mobile, web)

  4. Versioning and access control

Angular only loads JSON and applies it.

16. Production-Ready Guidelines

  1. Never mutate the original rule set

  2. Always disable hidden fields instead of removing them

  3. Keep evaluator pure and independent

  4. Cache rules for performance

  5. Log rule hits for debugging

  6. Strictly separate view logic from rule logic

  7. Use debounceTime for high-frequency fields

  8. 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