Home/Documentation/Angular Package

NgRx with @validation-rules-engine/angular

NgRx is a Redux-inspired Angular state library built around actions, reducers, selectors, and immutable snapshots.

Enterprise use cases

  • Multi-step workflows shared across routes or feature modules.
  • Auditable state transitions where every form change and validation result should be traceable.
  • Large Angular applications that already use selectors, reducers, and effects.

Why choose it

Choose NgRx when explicit events and reducer tests are more important than minimizing boilerplate. Validation Rules Engine fits this model because policies validate a plain draft model, then the validated model and result metadata can be committed through an action.

Integration pattern

The unified Angular showcase stores the form snapshot through NgRx actions and reducers while @validation-rules-engine/angular evaluates the same model paths used by every other Angular state showcase.

Showcase pages

Using @validation-rules-engine/angular

In NgRx applications, keep reducers responsible for immutable draft state and let @validation-rules-engine/angular validate the selected draft before dispatching save or validation-result actions.

Step 1 — Install Package

Install the Angular adapter, Core package, Angular Forms, and NgRx Store.

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

Keep NgRx aligned to the Angular major version used by the app.

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

Step 2 — Configure Project

Register Angular Forms, Validation Rules Engine, and the feature reducer.

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { StoreModule } from '@ngrx/store';
import { ValidationModule, provideBootstrapValidationDisplay } from '@validation-rules-engine/angular';
import { profileReducer } from './state/profile.reducer';

@NgModule({
  imports: [
    FormsModule,
    ValidationModule,
    StoreModule.forFeature('profile', profileReducer)
  ],
  providers: [...provideBootstrapValidationDisplay()]
})
export class ProfileFeatureModule {}

Step 3 — Create Validation Policy

The policy validates the serializable draft model stored by NgRx.

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

export interface ProfileDraft extends ValidationModel {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  country: string;
  accepted: boolean;
}

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

Step 4 — Register Policy

Use component lifecycle for route-scoped policy registration and cleanup.

ngOnInit(): void {
  this.validation.replacePolicy('ProfileDraft', new ProfileDraftPolicy());
  this.validation.registerFormGroupPolicy('profileDraftGroup', 'ProfileDraft');
  this.validation.formGroup['profileDraftGroup'] = ['firstName', 'lastName', 'email', 'phone', 'country', 'accepted'];
}

ngOnDestroy(): void {
  this.validation.unregisterFormGroupPolicy('profileDraftGroup');
  this.validation.unregisterPolicy('ProfileDraft');
}

Step 5 — Connect State Management

Model changes become NgRx actions. Validation runs against the selected draft and then dispatches a validated snapshot.

import { createAction, createReducer, createSelector, on, props } from '@ngrx/store';
import { ProfileDraft } from './profile.policy';

export const profileFieldChanged = createAction('[Profile] Field Changed', props<{ path: keyof ProfileDraft; value: unknown }>());
export const profileValidated = createAction('[Profile] Validated', props<{ draft: ProfileDraft; isValid: boolean }>());
export const profileReset = createAction('[Profile] Reset');

export const initialProfileDraft: ProfileDraft = {
  firstName: '',
  lastName: '',
  email: '',
  phone: '',
  country: '',
  accepted: false
};

export const profileReducer = createReducer(
  initialProfileDraft,
  on(profileFieldChanged, (state, { path, value }) => ({ ...state, [path]: value })),
  on(profileValidated, (_state, { draft }) => ({ ...draft })),
  on(profileReset, () => initialProfileDraft)
);

export const selectProfileDraft = (state: { profile: ProfileDraft }) => state.profile;
export const selectProfileErrors = createSelector(selectProfileDraft, (draft) => draft.validationResults ?? []);

Step 6 — Bind Controls

Read the draft with async, dispatch changes, and bind policyValidator to the selected model.

@if (draft$ | async; as draft) {
  <input [ngModel]="draft.email" (ngModelChange)="change('email', $event)" policyValidator
    [validateModel]="'email'" [actualModel]="draft" [withPolicy]="'ProfileDraft'" groupName="profileDraftGroup" />
  <policy-validation-message [model]="draft" propertyName="email"></policy-validation-message>
  <policy-validation-summary [model]="draft"></policy-validation-summary>
}

Step 7 — Validate

Validate the current draft and commit the decorated snapshot back through NgRx.

submit(draft: ProfileDraft): void {
  this.validation.validateAll(draft, 'ProfileDraft', {
    showAllErrors: true,
    evaluateGroups: true,
    markEvaluated: true
  }).subscribe(() => {
    this.store.dispatch(profileValidated({ draft: structuredClone(draft), isValid: !draft.validationResults?.length }));
  });
}

validateGroup(draft: ProfileDraft): void {
  this.validation.evaluateFormGroup(draft, 'profileDraftGroup', 'ProfileDraft');
  this.store.dispatch(profileValidated({ draft: structuredClone(draft), isValid: !draft.validationResults?.length }));
}

Step 8 — Reset

Reset NgRx state and clear metadata on the current draft before leaving the route.

reset(draft: ProfileDraft): void {
  this.validation.clearValidationState(draft, ['ProfileDraft']);
  this.store.dispatch(profileReset());
}

Step 9 — Best Practices

Keep reducer state serializable and clone validated drafts before dispatching them.

const validatedDraft = structuredClone(draft);
this.store.dispatch(profileValidated({ draft: validatedDraft, isValid: !(validatedDraft.validationResults?.length ?? 0) }));

Prefer feature-scoped reducers, stable policy names, selectors for derived validation summaries, and effects only for persistence or server-side validation.

Step 10 — Complete Working Example

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { ValidationProviderService } from '@validation-rules-engine/angular';
import { ProfileDraft, ProfileDraftPolicy } from './profile.policy';
import { profileFieldChanged, profileReset, profileValidated, selectProfileDraft } from './state/profile.reducer';

@Component({
  selector: 'app-ngrx-profile',
  templateUrl: './ngrx-profile.component.html'
})
export class NgrxProfileComponent implements OnInit, OnDestroy {
  readonly draft$: Observable<ProfileDraft> = this.store.select(selectProfileDraft);

  constructor(private readonly store: Store, private readonly validation: ValidationProviderService) {}

  ngOnInit(): void {
    this.validation.replacePolicy('ProfileDraft', new ProfileDraftPolicy());
    this.validation.registerFormGroupPolicy('profileDraftGroup', 'ProfileDraft');
    this.validation.formGroup['profileDraftGroup'] = ['firstName', 'lastName', 'email', 'phone', 'country', 'accepted'];
  }

  change(path: keyof ProfileDraft, value: unknown): void {
    this.store.dispatch(profileFieldChanged({ path, value }));
  }

  submit(draft: ProfileDraft): void {
    this.validation.validateAll(draft, 'ProfileDraft', { showAllErrors: true, evaluateGroups: true }).subscribe(() => {
      this.store.dispatch(profileValidated({ draft: structuredClone(draft), isValid: !draft.validationResults?.length }));
    });
  }

  reset(draft: ProfileDraft): void {
    this.validation.clearValidationState(draft, ['ProfileDraft']);
    this.store.dispatch(profileReset());
  }

  ngOnDestroy(): void {
    this.validation.unregisterFormGroupPolicy('profileDraftGroup');
    this.validation.unregisterPolicy('ProfileDraft');
  }
}
@if (draft$ | async; as draft) {
  <form (ngSubmit)="submit(draft)" novalidate>
    <label>Email <input type="email" [ngModel]="draft.email" name="email" (ngModelChange)="change('email', $event)" policyValidator
      [validateModel]="'email'" [actualModel]="draft" [withPolicy]="'ProfileDraft'" groupName="profileDraftGroup" /></label>
    <label><input type="checkbox" [ngModel]="draft.accepted" name="accepted" (ngModelChange)="change('accepted', $event)" policyValidator
      [validateModel]="'accepted'" [actualModel]="draft" [withPolicy]="'ProfileDraft'" groupName="profileDraftGroup" /> Accept terms</label>
    <policy-validation-group-status [model]="draft" groupName="profileDraftGroup"></policy-validation-group-status>
    <policy-validation-summary [model]="draft"></policy-validation-summary>
    <button type="submit">Save</button>
    <button type="button" (click)="reset(draft)">Reset</button>
  </form>
}

Continue in the live platform

Open Angular ShowcaseSee the concepts running in a real application →