@n0n3br/ngx-form-dependency-engine
v0.0.5
Published
Reactive dependency engine for Angular FormGroup: declare when/then rules that show, hide, enable, disable, validate and populate controls.
Maintainers
Readme
@n0n3br/ngx-form-dependency-engine
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/ORgroups 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/fieldOptionsexposed as real signals; zoneless-ready
Requires Angular ≥ 22 with Reactive Forms.
Install
npm install @n0n3br/ngx-form-dependency-engineQuick 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 withoutwhen, 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:4200Sections: 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
