Home/Documentation/Angular Package

Elf with @validation-rules-engine/angular

Elf is a small reactive store toolkit that uses composable stores and immutable update reducers.

Enterprise use cases

  • Feature-local stores that should stay lightweight.
  • Angular screens that already compose RxJS streams.
  • Teams that want predictable immutable updates without a large framework surface.

Why choose it

Choose Elf when you want store discipline with minimal ceremony. Validation Rules Engine policies remain framework-neutral, while Elf commits the validated snapshot through store reducers.

Integration pattern

The unified Angular showcase uses an Elf store for state snapshots and the same @validation-rules-engine/angular policy registration, validation groups, summaries, and performance generator as every other implementation.

Showcase pages

Using @validation-rules-engine/angular

Elf works best with Validation Rules Engine when the store owns immutable draft snapshots and the Angular adapter decorates those snapshots with validation metadata before they are committed.

Step 1 — Install Package

Install Validation Rules Engine, Angular Forms, and Elf.

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

Elf is an application-level state dependency; the Angular adapter remains state-library agnostic.

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

Step 2 — Configure Project

Import the Angular validation module and provide the feature store service.

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

@NgModule({
  imports: [FormsModule, ValidationModule],
  providers: [ProfileElfStore, ...provideBootstrapValidationDisplay()]
})
export class ProfileFeatureModule {}

Step 3 — Create Validation Policy

Define the form state as a plain object and validate the same paths that the Elf store writes.

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

export interface ProfileElfState extends ValidationModel {
  firstName: string;
  lastName: string;
  email: string;
  country: string;
  notificationFrequency: string;
}

export class ProfileElfPolicy 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('country').isRequired('Country is required'),
      v.validateFor('notificationFrequency').isRequired('Notification frequency is required')
    ];
  }
}

Step 4 — Register Policy

Register policy and group mappings in the feature component.

ngOnInit(): void {
  this.validation.replacePolicy('ProfileElf', new ProfileElfPolicy());
  this.validation.registerFormGroupPolicy('profileElfGroup', 'ProfileElf');
  this.validation.formGroup['profileElfGroup'] = ['firstName', 'lastName', 'email', 'country', 'notificationFrequency'];
}

ngOnDestroy(): void {
  this.validation.unregisterFormGroupPolicy('profileElfGroup');
  this.validation.unregisterPolicy('ProfileElf');
}

Step 5 — Connect State Management

Create an Elf store and expose update methods that preserve immutable state.

import { Injectable } from '@angular/core';
import { createStore, select, setProps, withProps } from '@ngneat/elf';
import { ProfileElfState } from '../profile.policy';

const initialState: ProfileElfState = {
  firstName: '',
  lastName: '',
  email: '',
  country: '',
  notificationFrequency: ''
};

@Injectable()
export class ProfileElfStore {
  private readonly store = createStore({ name: 'profile-elf' }, withProps<ProfileElfState>(initialState));
  readonly draft$ = this.store.pipe(select((state) => state));

  change(path: keyof ProfileElfState, value: unknown): void {
    this.store.update(setProps({ [path]: value } as Partial<ProfileElfState>));
  }

  commitValidated(draft: ProfileElfState): void {
    this.store.update(setProps(structuredClone(draft)));
  }

  reset(): void {
    this.store.update(setProps(initialState));
  }
}

Step 6 — Bind Controls

Use the selected store snapshot as the actualModel for directives and summaries.

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

Step 7 — Validate

Validate the current snapshot, then commit a cloned decorated result back through Elf.

submit(draft: ProfileElfState): void {
  this.validation.validateAll(draft, 'ProfileElf', { showAllErrors: true, evaluateGroups: true }).subscribe(() => {
    this.state.commitValidated(structuredClone(draft));
  });
}

validateGroup(draft: ProfileElfState): void {
  this.validation.evaluateFormGroup(draft, 'profileElfGroup', 'ProfileElf');
  this.state.commitValidated(structuredClone(draft));
}

Step 8 — Reset

Reset the Elf store and clear any validation metadata on the previous snapshot.

reset(draft: ProfileElfState): void {
  this.validation.clearValidationState(draft, ['ProfileElf']);
  this.state.reset();
}

Step 9 — Best Practices

Keep the Elf store thin and make the validation policy the only place where business validation rules live.

export const profileElfGroupFields = ['firstName', 'lastName', 'email', 'country', 'notificationFrequency'] as const;

Avoid mutating the object returned from the store outside Validation Rules Engine execution. Commit cloned validated snapshots so subscribers receive predictable immutable updates.

Step 10 — Complete Working Example

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ValidationProviderService } from '@validation-rules-engine/angular';
import { ProfileElfState, ProfileElfPolicy } from './profile.policy';
import { ProfileElfStore } from './state/profile-elf.store';

@Component({
  selector: 'app-elf-profile',
  templateUrl: './elf-profile.component.html'
})
export class ElfProfileComponent implements OnInit, OnDestroy {
  readonly draft$: Observable<ProfileElfState> = this.state.draft$;

  constructor(public readonly state: ProfileElfStore, private readonly validation: ValidationProviderService) {}

  ngOnInit(): void {
    this.validation.replacePolicy('ProfileElf', new ProfileElfPolicy());
    this.validation.registerFormGroupPolicy('profileElfGroup', 'ProfileElf');
    this.validation.formGroup['profileElfGroup'] = ['firstName', 'lastName', 'email', 'country', 'notificationFrequency'];
  }

  submit(draft: ProfileElfState): void {
    this.validation.validateAll(draft, 'ProfileElf', { showAllErrors: true, evaluateGroups: true }).subscribe(() => {
      this.state.commitValidated(structuredClone(draft));
    });
  }

  reset(draft: ProfileElfState): void {
    this.validation.clearValidationState(draft, ['ProfileElf']);
    this.state.reset();
  }

  ngOnDestroy(): void {
    this.validation.unregisterFormGroupPolicy('profileElfGroup');
    this.validation.unregisterPolicy('ProfileElf');
  }
}
@if (draft$ | async; as draft) {
  <form (ngSubmit)="submit(draft)" novalidate>
    <label>Email <input type="email" name="email" [ngModel]="draft.email" (ngModelChange)="state.change('email', $event)" policyValidator
      [validateModel]="'email'" [actualModel]="draft" [withPolicy]="'ProfileElf'" groupName="profileElfGroup" /></label>
    <label>Frequency <select name="frequency" [ngModel]="draft.notificationFrequency" (ngModelChange)="state.change('notificationFrequency', $event)" policyValidator
      [validateModel]="'notificationFrequency'" [actualModel]="draft" [withPolicy]="'ProfileElf'" groupName="profileElfGroup">
      <option value="">Select</option><option value="weekly">Weekly</option>
    </select></label>
    <policy-validation-group-status [model]="draft" groupName="profileElfGroup"></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 →