Home/Documentation/Angular Package

RxAngular State with @validation-rules-engine/angular

RxAngular State provides component-scoped reactive state with RxJS-friendly updates and lifecycle cleanup.

Enterprise use cases

  • Performance-sensitive Angular screens with local state.
  • Components that need observable state slices without a global store.
  • Teams optimizing rendering and subscription ownership in complex views.

Why choose it

Choose RxAngular State when form state should be scoped to the component tree. Validation Rules Engine (VRE) evaluates the model, and RxAngular State publishes the validated snapshot as local reactive state.

Integration pattern

The unified Angular showcase provides RxAngular State at the showcase component boundary and stores the same simple, complex, and performance state shape used by the other strategies.

Showcase pages

Using @validation-rules-engine/angular

RxAngular State is ideal for component-scoped form state. The component provides RxState, updates local slices, and VRE validates the current snapshot.

Step 1 — Install Package

Install VRE, Angular Forms, and RxAngular State.

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

RxAngular is an app dependency; @validation-rules-engine/angular continues to validate plain objects.

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

Step 2 — Configure Project

Import forms and VRE. Provide RxState at the component boundary so each route owns an isolated store.

import { Component } from '@angular/core';
import { RxState } from '@rx-angular/state';
import { ValidationModule, provideBootstrapValidationDisplay } from '@validation-rules-engine/angular';

@Component({
  selector: 'app-rx-profile',
  templateUrl: './rx-profile.component.html',
  providers: [RxState, ...provideBootstrapValidationDisplay()]
})
export class RxProfileComponent {}

Step 3 — Create Validation Policy

Policies validate the local state shape published by RxState.

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

export interface RxProfileState extends ValidationModel {
  firstName: string;
  lastName: string;
  email: string;
  phone: string;
  consent: boolean;
}

export class RxProfilePolicy 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('consent').isChecked('Consent is required')
    ];
  }
}

Step 4 — Register Policy

Register policies when the component initializes and unregister them during cleanup.

ngOnInit(): void {
  this.rxState.set(initialRxProfileState);
  this.validation.replacePolicy('RxProfile', new RxProfilePolicy());
  this.validation.registerFormGroupPolicy('rxProfileGroup', 'RxProfile');
  this.validation.formGroup['rxProfileGroup'] = ['firstName', 'lastName', 'email', 'phone', 'consent'];
}

ngOnDestroy(): void {
  this.validation.unregisterFormGroupPolicy('rxProfileGroup');
  this.validation.unregisterPolicy('RxProfile');
}

Step 5 — Connect State Management

Use RxState.select() for the view model and RxState.set() for deterministic updates.

const initialRxProfileState: RxProfileState = {
  firstName: '',
  lastName: '',
  email: '',
  phone: '',
  consent: false
};

readonly draft$ = this.rxState.select();

change(path: keyof RxProfileState, value: unknown): void {
  this.rxState.set((state) => ({ ...state, [path]: value }));
}

commitValidated(draft: RxProfileState): void {
  this.rxState.set(structuredClone(draft));
}

Step 6 — Bind Controls

Bind the selected snapshot to policyValidator and publish changes through RxState.

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

Step 7 — Validate

Validate the current RxState snapshot and write the decorated copy back into the component store.

submit(draft: RxProfileState): void {
  this.validation.validateAll(draft, 'RxProfile', { showAllErrors: true, evaluateGroups: true }).subscribe(() => {
    this.commitValidated(draft);
  });
}

validateGroup(draft: RxProfileState): void {
  this.validation.evaluateFormGroup(draft, 'rxProfileGroup', 'RxProfile');
  this.commitValidated(draft);
}

Step 8 — Reset

Clear metadata and reset the local component state.

reset(draft: RxProfileState): void {
  this.validation.clearValidationState(draft, ['RxProfile']);
  this.rxState.set(initialRxProfileState);
}

Step 9 — Best Practices

Keep RxState scoped to the screen unless multiple routed components need the same draft.

readonly errorCount$ = this.rxState.select().pipe(
  map((state) => state.validationResults?.length ?? 0)
);

Use observable projections for derived view state, keep policy construction stable, and do not expose mutable model references outside the feature component.

Step 10 — Complete Working Example

import { Component, OnDestroy, OnInit } from '@angular/core';
import { RxState } from '@rx-angular/state';
import { Observable } from 'rxjs';
import { ValidationProviderService } from '@validation-rules-engine/angular';
import { RxProfileState, RxProfilePolicy } from './profile.policy';

const initialRxProfileState: RxProfileState = { firstName: '', lastName: '', email: '', phone: '', consent: false };

@Component({
  selector: 'app-rx-profile',
  templateUrl: './rx-profile.component.html',
  providers: [RxState]
})
export class RxProfileComponent implements OnInit, OnDestroy {
  readonly draft$: Observable<RxProfileState> = this.rxState.select();

  constructor(private readonly rxState: RxState<RxProfileState>, private readonly validation: ValidationProviderService) {}

  ngOnInit(): void {
    this.rxState.set(initialRxProfileState);
    this.validation.replacePolicy('RxProfile', new RxProfilePolicy());
    this.validation.registerFormGroupPolicy('rxProfileGroup', 'RxProfile');
    this.validation.formGroup['rxProfileGroup'] = ['firstName', 'lastName', 'email', 'phone', 'consent'];
  }

  change(path: keyof RxProfileState, value: unknown): void {
    this.rxState.set((state) => ({ ...state, [path]: value }));
  }

  submit(draft: RxProfileState): void {
    this.validation.validateAll(draft, 'RxProfile', { showAllErrors: true, evaluateGroups: true }).subscribe(() => {
      this.rxState.set(structuredClone(draft));
    });
  }

  reset(draft: RxProfileState): void {
    this.validation.clearValidationState(draft, ['RxProfile']);
    this.rxState.set(initialRxProfileState);
  }

  ngOnDestroy(): void {
    this.validation.unregisterFormGroupPolicy('rxProfileGroup');
    this.validation.unregisterPolicy('RxProfile');
  }
}
@if (draft$ | async; as draft) {
  <form (ngSubmit)="submit(draft)" novalidate>
    <label>Email <input type="email" name="email" [ngModel]="draft.email" (ngModelChange)="change('email', $event)" policyValidator
      [validateModel]="'email'" [actualModel]="draft" [withPolicy]="'RxProfile'" groupName="rxProfileGroup" /></label>
    <label><input type="checkbox" name="consent" [ngModel]="draft.consent" (ngModelChange)="change('consent', $event)" policyValidator
      [validateModel]="'consent'" [actualModel]="draft" [withPolicy]="'RxProfile'" groupName="rxProfileGroup" /> Consent received</label>
    <policy-validation-group-status [model]="draft" groupName="rxProfileGroup"></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 →