Architecture

June 5, 2026 · View on GitHub

This document describes the internal structure of FormLens for contributors and maintainers.

This document covers internals. If you want to use FormLens, see Quick Start or the API Reference.


Overview

FormLens is a single Angular library (form-lens-angular) organized into six internal areas. Each area has a clear boundary and a focused responsibility.

form-lens-angular/
├── core/          — config, tokens, DI provider, registry
├── registration/  — directive and form lifecycle integration
├── inspector/     — snapshots, selected state, search derivation
├── overlay/       — FAB button and panel open/close
├── highlight/     — DOM outline for invalid controls
└── ui/            — panel, tree, details, and status components

Core

Path: src/lib/core/

Owns the foundational pieces that everything else depends on.

FileResponsibility
formlens.config.tsFormLensConfig interface definition
formlens.tokens.tsAngular injection tokens (FORMLENS_CONFIG)
formlens.provider.tsprovideFormLens() — composes all providers and initializers
formlens.registry.tsFormLensRegistry — stores registered FormGroup references
formlens.types.tsShared internal types

The registry is the central state for which forms are currently active on the page. It is a simple signal-based map of { id, name, formGroup } entries. The inspector reads from the registry; the directive writes to it.


Registration

Path: src/lib/registration/

Bridges Angular's Reactive Forms API with FormLens.

FileResponsibility
form-lens.directive.tsFormLensDirective — attaches to [formGroup] elements
form-lens-registration.utils.tsID generation and name resolution utilities

The directive runs ngOnInit to register the form and ngOnDestroy to clean up the registry entry and highlight state. It does not hold state itself — it delegates everything to the registry and highlight service.


Inspector

Path: src/lib/inspector/

Produces a reactive snapshot of form state for display in the panel.

FileResponsibility
form-snapshot.store.tsReactive store: selected form, active snapshot, search
control-snapshot.factory.tsBuilds ControlSnapshot trees from a AbstractControl
control-snapshot.model.tsControlSnapshot type definition
control-tree.utils.tsTree traversal and path lookup utilities
control-tree-filter.utils.tsSearch filtering logic over snapshot trees

Snapshot model

A ControlSnapshot is a plain serializable object representing the state of a single control at a point in time:

interface ControlSnapshot {
  name: string;
  path: string;
  kind: 'control' | 'group' | 'array';
  value: unknown;
  status: string;
  valid: boolean;
  invalid: boolean;
  dirty: boolean;
  pristine: boolean;
  touched: boolean;
  untouched: boolean;
  pending: boolean;
  disabled: boolean;
  enabled: boolean;
  errors: Record<string, unknown> | null;
  validators: string[];
  children: ControlSnapshot[];
}

Snapshots are rebuilt reactively whenever valueChanges or statusChanges emits on the selected form. The store subscribes to these observables and triggers a full snapshot rebuild.

Store flow

FormLensRegistry (signal)


FormSnapshotStore (effect)
  ├── auto-selects first form when registry changes
  ├── subscribes to valueChanges + statusChanges of selected form
  ├── rebuilds ControlSnapshot tree on each emission
  └── derives filteredSnapshot from search query (computed)

Overlay

Path: src/lib/overlay/

Controls the lifecycle of the inspector panel using Angular CDK.

FileResponsibility
formlens-overlay.service.tsOpens, closes, and toggles the CDK overlay
formlens-fab.component.tsFloating action button — icon synced to isOpen signal
formlens-fab.initializer.tsAPP_INITIALIZER that injects the FAB into the DOM at startup

The overlay service creates a CDK OverlayRef on open() and destroys it on close(). It subscribes to overlayRef.detachments() to handle cases where the CDK disposes the overlay externally (e.g., on route navigation), keeping isOpen in sync.

The FAB is injected once at app startup via APP_INITIALIZER. It is not part of any component tree — it is appended directly to document.body via createComponent.


Highlight

Path: src/lib/highlight/

Maps invalid AbstractControl instances to their DOM elements and applies an outline.

FileResponsibility
invalid-control-highlight.service.tsQueries DOM elements and manages outline styles

The highlight service receives the root nativeElement of the registered form and searches for [formControlName] elements whose corresponding control is INVALID. It applies a CSS class or inline style outline and clears it on demand.

Known limitation: deep FormArray with dynamically-added controls can produce stale highlight state in the current alpha.


UI

Path: src/lib/ui/

Standalone Angular components that render the inspector panel content.

ComponentResponsibility
formlens-panel.component.tsRoot panel layout — form selector, search, tree, details
control-tree.component.tsRecursive tree node — expand/collapse, selection, state classes
control-details.component.tsDetail view for the selected ControlSnapshot
status-badge.component.tsRenders a colored badge for VALID, INVALID, PENDING, DISABLED

All components are standalone and use signal inputs (input()). The panel reads from FormSnapshotStore signals directly — no @Input() binding needed at the panel level.


Data flow

User interacts with form


AbstractControl emits valueChanges / statusChanges


FormSnapshotStore rebuilds ControlSnapshot tree


Panel components read signals (filteredSnapshot, selectedNode)


DOM updates via Angular's signal-based change detection

Key design decisions

Snapshot-based, not live binding. The inspector works with immutable snapshots rebuilt on each change rather than binding directly to the live AbstractControl. This makes the UI predictable and keeps the inspector isolated from accidental mutations.

Signal-first. Internal state (registry, selected form, snapshot, search) uses Angular signals and computed values. Effects are used only for side effects that require subscriptions (RxJS observables from the forms API).

Standalone components only. No NgModules. All components and the directive are standalone. This keeps the import surface clean and compatible with modern Angular library publishing.

Small public API. Only three symbols are exported as the public API: provideFormLens, FormLensDirective, and FormLensOverlayService. Everything else is internal. This keeps the library evolvable without breaking consumers.


Running locally

# Install dependencies
npm install

# Build the library
ng build formlens --configuration production

# Run the demo app
ng serve demo

# Run tests
ng test formlens

To test the built package locally before publishing:

cd dist/formlens
npm pack
# then in another project:
npm install /path/to/form-lens-angular-x.x.x.tgz