npm.io
0.0.3 • Published yesterday

@n0n3br/ngx-form-dependency-engine

Licence
MIT
Version
0.0.3
Deps
1
Size
92 kB
Vulns
0
Weekly
0

@n0n3br/ngx-form-dependency-engine

npm version License: MIT

Reactive dependency engine for Angular FormGroup: declare when → effect rules between controls — "when these conditions hold against the form's value, apply these effects to a target control."

No visual builder, no schema framework. One engine class, pure functions, real Angular signals.

Features

  • 15 condition operators (equals, in, contains, matches, greaterThan, isEmpty, custom predicates…)
  • AND / OR groups with arbitrary nesting
  • Effects: show · hide · enable · disable · setRequired · unsetRequired · addValidators / removeValidators (namespaced) · setValue · setOptions
  • Per-dependency and per-key validator namespacing — strictly additive over baseline validators
  • Chained rules propagate inside one pass; genuine cycles are capped (maxIterations) and warned, never hang
  • Emit-free internal writes (emitEvent: false) — no re-entrancy surprises
  • hiddenFields / fieldOptions exposed as real signals; zoneless-ready

Requires Angular ≥ 22 with Reactive Forms.

Install

npm install @n0n3br/ngx-form-dependency-engine

Quick start

import { Component, DestroyRef, inject } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import {
  FormDependencyEngine,
  dep,
  f,
  allOf,
  show,
  hide,
  setRequired,
} from '@n0n3br/ngx-form-dependency-engine';

@Component({/* ... */})
export class QuoteComponent {
  readonly form = new FormGroup({
    kind: new FormControl<'PF' | 'PJ'>('PF'),
    companyDoc: new FormControl(''),
    personDoc: new FormControl(''),
    income: new FormControl(0),
  });

  readonly engine = new FormDependencyEngine(this.form, [
    dep('doc-type', 'companyDoc')
      .when(f('kind').equals('PJ'))
      .then(show(), setRequired())
      .otherwise(hide())
      .build(),
    dep('premium', 'bonus')
      .when(allOf(f('kind').equals('PF'), f('income').greaterThan(5000)))
      .then(show())
      .build(),
  ]);

  constructor() {
    inject(DestroyRef).onDestroy(() => this.engine.destroy());
    this.engine.activate(); // runs an initial pass, then follows form.valueChanges
  }
}
@if (!engine.isHidden('companyDoc')) {
<input [formControl]="form.controls.companyDoc" />
}

Directive usage

Prefer configuring rules straight from the template? Attach fdeDependencies to the <form> element. The directive owns the engine lifecycle (build, activate, rebuild on input change, destroy) and is exported as fde so templates read state directly.

import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import {
  Dependency,
  dep,
  f,
  hide,
  setOptions,
  setRequired,
  show,
  unsetRequired,
} from '@n0n3br/ngx-form-dependency-engine';

@Component({/* ... */})
export class SignupComponent {
  readonly form = new FormGroup({
    accountType: new FormControl('personal'),
    companyName: new FormControl(''),
    plan: new FormControl('Starter'),
  });

  readonly deps: Dependency[] = [
    dep('business-company', 'companyName')
      .when(f('accountType').equals('business'))
      .then(show(), setRequired())
      .otherwise(hide(), unsetRequired())
      .build(),
    dep('plan-options', 'plan')
      .when(f('accountType').equals('business'))
      .then(setOptions(['Team', 'Enterprise']))
      .build(),
  ];
}
<form [formGroup]="form" [fdeDependencies]="deps" #fde="fde">
  @if (!fde.isHidden('companyName')) {
  <input formControlName="companyName" />
  }

  <select formControlName="plan">
    @for (plan of fde.optionsFor('plan') ?? []; track plan) {
    <option [value]="plan">{{ plan }}</option>
    }
  </select>
</form>

Directive API:

Member Description
fdeDependencies Dependency[] input; pass null to run with no rules. Swapping the array at runtime rebuilds the engine.
fdeConfig Optional FormDependencyEngineConfig (e.g. maxIterations).
engine Readonly signal with the live FormDependencyEngine instance (or null before init).
isHidden(path) Null-safe visibility read for templates.
optionsFor(path) Null-safe option list read.
reevaluate() Force an evaluation pass.

Works with [formGroup] (reactive) and template-driven forms — it resolves whatever ControlContainer hosts the element. Reactive forms are recommended since the engine maps controls at construction time.

Core API

new FormDependencyEngine(form, dependencies, config?)
Member Description
activate(): Subscription Runs the first evaluation pass, then re-evaluates on every form.valueChanges.
destroy(): void Unsubscribes.
reevaluate(): void Forces a manual evaluation pass.
isHidden(field): boolean Reads the hiddenFields signal set.
optionsFor(field): unknown[] | undefined Reads the fieldOptions signal map.
hiddenFields: Signal<ReadonlySet<string>> Fields currently hidden by hide effects.
fieldOptions: Signal<ReadonlyMap<string, unknown[]>> Option lists written by setOptions.
settled: Signal<number> Bumps after every evaluation pass — use it in computed()s to track emit-free writes.

config: { maxIterations?: number } — how many internal passes a single evaluation may take while still changing state (default 5). Hitting the cap logs a circular-dependency warning.

Conditions
interface Condition {
  field: string; // dot-path into the form value, e.g. "endereco.cidade"
  operator: ConditionOperator;
  value?: unknown;
  custom?: (formValue: any) => boolean; // wins over operator entirely
}

Operators: equals · notEquals · in · notIn · contains · notContains · greaterThan · greaterOrEqual · lessThan · lessOrEqual · isEmpty · isNotEmpty · truthy · falsy · matches.

Groups combine with { logic: 'AND' | 'OR', conditions: [...] }; empty AND is vacuously true.

Effects

Every effect accepts an optional target override (defaults to the owning dependency's target).

Effect Extra payload Behavior
show / hide Toggles membership in hiddenFields. Never clears values.
enable / disable Native control enable/disable; guarded no-op if already in that state.
setRequired / unsetRequired Namespaced by the dependency id — two dependencies can require the same field independently.
addValidators / removeValidators key, validators? Arbitrary validator sets under your own stable key.
setValue value literal or (formValue) => value Writes with emitEvent: false.
setOptions options literal or (formValue) => options Writes into the fieldOptions signal only.

Validator management is strictly additive: validators present on a control before the engine ever touched it are preserved through every attach/detach cycle.

Evaluation loop

Because internal writes do not emit events, the engine loops internally: evaluate all dependencies → apply effects → compare state by content (never by reference) → repeat until nothing changes or maxIterations is reached. A chain A→B→C fully propagates within a single pass series; a true cycle caps out and warns.

Fluent rule builder

The builder layer produces plain, frozen contract objects — identical to hand-written literals.

import { allOf, anyOf, dep, f, not } from '@n0n3br/ngx-form-dependency-engine';
import { Validators } from '@angular/forms';

const dependency = dep('show-guarantee', 'guarantee')
  .when(
    allOf(f('amount').greaterThan(50000), anyOf(f('profile').equals('gold'), f('vip').truthy())),
  )
  .then(show(), setRequired(), addValidators('min-len', [Validators.minLength(3)]))
  .otherwise(hide(), unsetRequired())
  .build();
  • f(path) — every operator is a method: .equals(), .in([...]), .isEmpty(), .matches(/re/)
  • .and() / .or() — chain groups: f('a').equals(1).and(f('b').equals(2)).or(f('c').equals(3))
  • allOf(...) / anyOf(...) / not(condition) — combinators
  • Effect factories: show(target?), hide(), enable(), disable(), setRequired(), unsetRequired(), addValidators(key, validators, target?), removeValidators(key, target?), setValue(valueOrFn, target?), setOptions(optionsOrFn, target?)
  • dep(id, target).when(...).then(...).otherwise(...).build() — throws without when, freezes its output

Demo application

This repository ships an interactive showcase covering every feature:

git clone https://github.com/rogeriolaa/ngx-form-dependency-engine
cd ngx-form-dependency-engine
npm install
npm start          # serves the demo at http://localhost:4200

Sections: Playground · Cascade · Validators Lab · Chains & Cycles · State Inspector · Builder Playground.

Development

Command Description
npm run build:lib Build the publishable library to dist/.
npm test Run the library unit tests (Vitest).
npm run test:coverage Unit tests with a V8 coverage report (kept at ~99% statements).
ng test demo Run demo tests.
npm run lint ESLint across workspace.
npm run build:demo Production build of the demo (Pages base href).

License

MIT rogeriolaa

Keywords