Home/Documentation/Angular Package

Angular ngModel Integration

Template-driven forms

The Angular adapter was designed around template-driven forms and works naturally with [(ngModel)].

<form #form="ngForm" (ngSubmit)="submit()">
  <input
    name="firstName"
    [(ngModel)]="model.firstName"
    policyValidator
    [validateModel]="'form.firstName'"
    [actualModel]="model"
    [withPolicy]="'Registration'"
    groupName="registration"
  />
  <button type="submit">Create account</button>
</form>

Submit lifecycle

submit(): void {
  this.validation.validateAll(this.model, 'Registration').subscribe((snapshot) => {
    this.submitted = true;
    if (snapshot.isValid) this.save();
  });
}

Best practices

Keep the model object stable during validation, use explicit name attributes, and align validateModel paths with policy paths.

Using @validation-rules-engine/angular

Template Driven Forms are the lowest-friction way to consume @validation-rules-engine/angular: Angular owns [(ngModel)] binding and Validation Rules Engine owns policy execution, field messages, group status, and submit validation.

Step 1 — Install Package

Install the Angular adapter next to Angular Forms. The adapter exports Core policy types, so application code can import policies from one package.

npm install @validation-rules-engine/angular @validation-rules-engine/core
npm install @angular/forms

Use the adapter version that matches the Validation Rules Engine workspace version used by your application. Angular itself remains a peer dependency of your app.

{
  "dependencies": {
    "@angular/forms": "^20.0.0",
    "@validation-rules-engine/angular": "^1.0.0",
    "@validation-rules-engine/core": "^1.0.0"
  }
}

Step 2 — Configure Project

Import FormsModule for ngModel, ValidationModule for directives/components, and an optional display provider for your CSS framework.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { ValidationModule, provideBootstrapValidationDisplay } from '@validation-rules-engine/angular';
import { ProfileComponent } from './profile.component';

@NgModule({
  declarations: [ProfileComponent],
  imports: [BrowserModule, FormsModule, ValidationModule],
  providers: [...provideBootstrapValidationDisplay()],
  bootstrap: [ProfileComponent]
})
export class AppModule {}

Step 3 — Create Validation Policy

Create a plain model and a Core-compatible policy. The paths used by validateFor() must match the validateModel paths in the template.

import { ValidationModel, ValidationPolicy, Validator, ValidatorHelper } from '@validation-rules-engine/angular';

export interface ProfileModel extends ValidationModel {
  form: {
    firstName: string;
    lastName: string;
    email: string;
    phone: string;
    country: string;
    acceptedTerms: boolean;
  };
}

export class ProfilePolicy implements ValidationPolicy {
  addValidations(v: ValidatorHelper): Validator[] {
    return [
      v.validateFor('form.firstName').isRequired('First name is required'),
      v.validateFor('form.lastName').isRequired('Last name is required'),
      v.validateFor('form.email').isRequired('Email is required').isEmail('Enter a valid email'),
      v.validateFor('form.phone').isPhone('Enter a valid US phone number'),
      v.validateFor('form.country').isRequired('Country is required'),
      v.validateFor('form.acceptedTerms').isChecked('Terms must be accepted')
    ];
  }
}

Step 4 — Register Policy

Register policies when the component is created and unregister them when the route is destroyed. This keeps generated or route-scoped policies from leaking into other screens.

import { OnDestroy, OnInit } from '@angular/core';
import { ValidationProviderService } from '@validation-rules-engine/angular';
import { ProfilePolicy } from './profile.policy';

export class ProfileComponent implements OnInit, OnDestroy {
  private readonly policyName = 'Profile';
  private readonly groupName = 'profileForm';

  constructor(private readonly validation: ValidationProviderService) {}

  ngOnInit(): void {
    this.validation.replacePolicy(this.policyName, new ProfilePolicy());
    this.validation.registerFormGroupPolicy(this.groupName, this.policyName);
    this.validation.formGroup[this.groupName] = [
      'form.firstName',
      'form.lastName',
      'form.email',
      'form.phone',
      'form.country',
      'form.acceptedTerms'
    ];
  }

  ngOnDestroy(): void {
    this.validation.unregisterFormGroupPolicy(this.groupName);
    this.validation.unregisterPolicy(this.policyName);
  }
}

Step 5 — Connect State Management

For Template Driven Forms, the state-management mechanism is the component model itself. Keep the model object as the single source of truth and let [(ngModel)] mutate it.

const emptyProfile = (): ProfileModel => ({
  form: {
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    country: '',
    acceptedTerms: false
  }
});

export class ProfileComponent {
  model = emptyProfile();

  updateCountry(country: string): void {
    this.model.form.country = country;
    this.validation.evaluateFormGroup(this.model, 'profileForm', 'Profile');
    this.validation.notifyValidationRefresh(this.model);
  }
}

Step 6 — Bind Controls

Attach policyValidator to each control and point it at the same model object used by the policy.

<input name="firstName" [(ngModel)]="model.form.firstName" policyValidator
  [validateModel]="'form.firstName'" [actualModel]="model" [withPolicy]="'Profile'" groupName="profileForm" />
<policy-validation-message [model]="model" propertyName="form.firstName"></policy-validation-message>

<select name="country" [(ngModel)]="model.form.country" policyValidator
  [validateModel]="'form.country'" [actualModel]="model" [withPolicy]="'Profile'" groupName="profileForm">
  <option value="">Select country</option>
  <option value="US">United States</option>
</select>
<policy-validation-summary [model]="model"></policy-validation-summary>

Step 7 — Validate

Use field validation through the directive, group validation for section badges, and validateAll() for submit.

submit(): void {
  this.validation.validateAll(this.model, 'Profile', {
    showAllErrors: true,
    evaluateGroups: true,
    markEvaluated: true
  }).subscribe(() => {
    if (!this.model.validationResults?.length) {
      this.saveProfile(this.model);
    }
  });
}

validateProfileGroup(): void {
  this.validation.evaluateFormGroup(this.model, 'profileForm', 'Profile');
}

Step 8 — Reset

Reset both the form model and Validation Rules Engine metadata. If the component is going away, unregister the policy and group.

reset(): void {
  this.model = emptyProfile();
  this.validation.clearValidationState(this.model, ['Profile']);
}

ngOnDestroy(): void {
  this.validation.unregisterFormGroupPolicy('profileForm');
  this.validation.unregisterPolicy('Profile');
}

Step 9 — Best Practices

Centralize paths so policy rules, form groups, and templates do not drift.

export const profilePaths = [
  'form.firstName',
  'form.lastName',
  'form.email',
  'form.phone',
  'form.country',
  'form.acceptedTerms'
] as const;

Prefer one route-scoped policy name per feature form, stable model objects while validation is running, explicit name attributes, and replacePolicy() when a dynamic form can change shape.

Step 10 — Complete Working Example

import { Component, OnDestroy, OnInit } from '@angular/core';
import { ValidationProviderService } from '@validation-rules-engine/angular';
import { ProfileModel, ProfilePolicy, profilePaths } from './profile.policy';

function emptyProfile(): ProfileModel {
  return {
    form: { firstName: '', lastName: '', email: '', phone: '', country: '', acceptedTerms: false }
  };
}

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html'
})
export class ProfileComponent implements OnInit, OnDestroy {
  model = emptyProfile();
  message = '';

  constructor(private readonly validation: ValidationProviderService) {}

  ngOnInit(): void {
    this.validation.replacePolicy('Profile', new ProfilePolicy());
    this.validation.registerFormGroupPolicy('profileForm', 'Profile');
    this.validation.formGroup['profileForm'] = [...profilePaths];
  }

  submit(): void {
    this.validation.validateAll(this.model, 'Profile', {
      showAllErrors: true,
      evaluateGroups: true,
      markEvaluated: true
    }).subscribe(() => {
      const errors = this.model.validationResults?.length ?? 0;
      this.message = errors ? `${errors} profile field(s) need attention.` : 'Profile saved.';
    });
  }

  reset(): void {
    this.model = emptyProfile();
    this.message = '';
    this.validation.clearValidationState(this.model, ['Profile']);
  }

  ngOnDestroy(): void {
    this.validation.unregisterFormGroupPolicy('profileForm');
    this.validation.unregisterPolicy('Profile');
  }
}
<form #profileForm="ngForm" (ngSubmit)="submit()" novalidate>
  <label>
    First name
    <input name="firstName" [(ngModel)]="model.form.firstName" policyValidator
      [validateModel]="'form.firstName'" [actualModel]="model" [withPolicy]="'Profile'" groupName="profileForm" />
  </label>
  <policy-validation-message [model]="model" propertyName="form.firstName"></policy-validation-message>

  <label>
    Email
    <input type="email" name="email" [(ngModel)]="model.form.email" policyValidator
      [validateModel]="'form.email'" [actualModel]="model" [withPolicy]="'Profile'" groupName="profileForm" />
  </label>
  <policy-validation-message [model]="model" propertyName="form.email"></policy-validation-message>

  <label>
    <input type="checkbox" name="acceptedTerms" [(ngModel)]="model.form.acceptedTerms" policyValidator
      [validateModel]="'form.acceptedTerms'" [actualModel]="model" [withPolicy]="'Profile'" groupName="profileForm" />
    I accept the terms
  </label>

  <policy-validation-group-status [model]="model" groupName="profileForm"></policy-validation-group-status>
  <policy-validation-summary [model]="model"></policy-validation-summary>
  <p role="status">{{ message }}</p>
  <button type="submit">Save</button>
  <button type="button" (click)="reset()">Reset</button>
</form>

Continue in the live platform

Open Angular ShowcaseSee the concepts running in a real application →