Home/Documentation/Angular Package

Akita with @validation-rules-engine/angular

Akita models state as stores and queries. Stores own updates; queries expose read models for components and services.

Enterprise use cases

  • CRUD-heavy Angular domains with clear read/write boundaries.
  • Feature stores where query selectors are useful but full Redux ceremony is not needed.
  • Incremental migrations from service state into structured stores.

Why choose it

Choose Akita when Store + Query separation keeps feature state readable. Validation Rules Engine integrates by validating the plain model and writing validation results back to the store snapshot.

Integration pattern

The unified Angular showcase updates an Akita store for the selected strategy and keeps @validation-rules-engine/angular policies identical to every other state implementation.

Showcase pages

Using @validation-rules-engine/angular

In Akita applications, use stores for updates, queries for reads, and @validation-rules-engine/angular for model validation and UI-facing validation metadata.

Step 1 — Install Package

Install Validation Rules Engine, Angular Forms, and Akita.

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

Keep Akita as an application dependency; the Validation Rules Engine adapter does not require Akita unless your feature uses it.

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

Step 2 — Configure Project

Import forms and Validation Rules Engine normally. Akita feature stores are services, so no feature module registration is required.

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

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

Step 3 — Create Validation Policy

The Akita state is a plain object, so policies look the same as they do for local state.

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

export interface ProfileStoreState extends ValidationModel {
  firstName: string;
  lastName: string;
  email: string;
  department: string;
  active: boolean;
}

export class ProfileStorePolicy 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('department').isRequired('Department is required'),
      v.validateFor('active').isChecked('Active employee confirmation is required')
    ];
  }
}

Step 4 — Register Policy

Register the policy where the feature form mounts.

ngOnInit(): void {
  this.validation.replacePolicy('ProfileStore', new ProfileStorePolicy());
  this.validation.registerFormGroupPolicy('profileStoreGroup', 'ProfileStore');
  this.validation.formGroup['profileStoreGroup'] = ['firstName', 'lastName', 'email', 'department', 'active'];
}

ngOnDestroy(): void {
  this.validation.unregisterFormGroupPolicy('profileStoreGroup');
  this.validation.unregisterPolicy('ProfileStore');
}

Step 5 — Connect State Management

Create an Akita store for writes and a query for reads.

import { Injectable } from '@angular/core';
import { Query, Store, StoreConfig } from '@datorama/akita';
import { ProfileStoreState } from '../profile.policy';

const initialState: ProfileStoreState = { firstName: '', lastName: '', email: '', department: '', active: false };

@Injectable()
@StoreConfig({ name: 'profile' })
export class ProfileStore extends Store<ProfileStoreState> {
  constructor() { super(initialState); }

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

  commitValidated(draft: ProfileStoreState): void {
    this.update(() => draft);
  }

  reset(): void {
    this.update(() => initialState);
  }
}

@Injectable()
export class ProfileQuery extends Query<ProfileStoreState> {
  readonly draft$ = this.select();
  constructor(store: ProfileStore) { super(store); }
}

Step 6 — Bind Controls

Bind the queried model to the template and call store update methods from control changes.

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

Step 7 — Validate

Validate the queried draft, then commit the decorated copy back into the store.

submit(draft: ProfileStoreState): void {
  this.validation.validateAll(draft, 'ProfileStore', { showAllErrors: true, evaluateGroups: true }).subscribe(() => {
    this.store.commitValidated(structuredClone(draft));
  });
}

validateGroup(draft: ProfileStoreState): void {
  this.validation.evaluateFormGroup(draft, 'profileStoreGroup', 'ProfileStore');
  this.store.commitValidated(structuredClone(draft));
}

Step 8 — Reset

Clear the selected draft and reset the Akita store.

reset(draft: ProfileStoreState): void {
  this.validation.clearValidationState(draft, ['ProfileStore']);
  this.store.reset();
}

Step 9 — Best Practices

Keep Akita stores focused on state transitions and keep policies in separate files.

export const profileStoreFields = ['firstName', 'lastName', 'email', 'department', 'active'] as const;

Use queries for read-only projections, store methods for writes, and component lifecycle for policy cleanup.

Step 10 — Complete Working Example

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ValidationProviderService } from '@validation-rules-engine/angular';
import { ProfileStoreState, ProfileStorePolicy } from './profile.policy';
import { ProfileQuery, ProfileStore } from './state/profile.store';

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

  constructor(
    public readonly store: ProfileStore,
    private readonly query: ProfileQuery,
    private readonly validation: ValidationProviderService
  ) {}

  ngOnInit(): void {
    this.validation.replacePolicy('ProfileStore', new ProfileStorePolicy());
    this.validation.registerFormGroupPolicy('profileStoreGroup', 'ProfileStore');
    this.validation.formGroup['profileStoreGroup'] = ['firstName', 'lastName', 'email', 'department', 'active'];
  }

  submit(draft: ProfileStoreState): void {
    this.validation.validateAll(draft, 'ProfileStore', { showAllErrors: true, evaluateGroups: true }).subscribe(() => {
      this.store.commitValidated(structuredClone(draft));
    });
  }

  reset(draft: ProfileStoreState): void {
    this.validation.clearValidationState(draft, ['ProfileStore']);
    this.store.reset();
  }

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