npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reforgium/regula

v0.0.3

Published

Headless behavior and policy layer for Angular Signal Forms

Readme

@reforgium/regula

regula is a headless behavior layer above Angular Signal Forms.

It is not a new form engine, not a schema-driven renderer, and not a replacement for Angular forms APIs. The package should exist only if it helps move repeated form behavior out of app-level glue code into a small, composable, explainable set of policies.

At this stage that boundary matters even more because Signal Forms is still experimental. regula should treat Signal Forms as the underlying host, not as an implementation detail it can freely absorb.

Problem

Angular Signal Forms can model field state well, but application teams still repeatedly solve the same behavior problems in ad hoc ways:

  • value normalization while typing, on commit, and on submit
  • error visibility rules
  • disabled, readonly, hidden, and excluded semantics
  • cross-field reactions and resets
  • payload shaping for APIs
  • server error mapping
  • diagnostics for "why did this happen?"

Without a dedicated layer, this logic usually ends up split across effects, submit handlers, mappers, directives, and component-local utilities.

Intended Position

Angular should own:

  • form structure
  • field lifecycle
  • validation execution
  • public form primitives
  • the experimental Signal Forms API surface

regula should own:

  • behavior policies
  • relational field logic
  • model-to-payload transitions
  • visibility and interactivity rules
  • explainable derived form state

Non-Goals

regula should not become:

  • a replacement form engine
  • a UI schema renderer
  • a layout builder
  • a giant config DSL
  • a private Angular API wrapper
  • a CVA-centric abstraction layer

If the library starts describing the entire form instead of describing how an existing form behaves, scope has drifted.

Host Boundary

regula should treat Signal Forms as the default host, with the binding hidden behind the main Regula API.

That boundary still exists for two reasons:

  1. Signal Forms is still experimental and may change.
  2. regula must remain a layer above the host form system, not a replacement for it.

The default scenario should be:

  • new Regula(form, { rules })
  • host-form access hidden behind the main Regula instance
  • built-in payload and state reads over that host

One Regula instance should own behavior for one host form. If multiple forms need to collaborate, coordination belongs outside regula.

The lower-level adapter contract should remain available only as an advanced option for non-Signal-Forms hosts.

Value Test

The library is worth building only if v0 can prove all of the following:

  1. A real form can adopt it without rewriting form structure.
  2. The public API stays smaller than the app code it replaces.
  3. Cross-field behavior becomes more explicit, not more magical.
  4. Diagnostics can answer at least basic "why" questions.
  5. The implementation does not depend on unstable Angular internals.

If those conditions are not met, the behavior should stay in app-level code instead of becoming a shared library.

Design Constraints

  • Signal-first where possible.
  • Deterministic execution and override order.
  • Small composable policies over one orchestration object.
  • Field behavior separate from rendering.
  • Explicit separation between:
    • input value, model value, payload value
    • invalid state, visible error state
    • hidden, disabled, readonly, excluded
    • reset, preserve, derive

Proposed Public Surface for v0

v0 should stay narrow and headless.

  • normalization pipeline
  • validation visibility strategy
  • disabled, readonly, hidden, excluded rules
  • payload serialization
  • minimal diagnostics reasons
  • simple cross-field reactions for a few common cases

Everything else should justify its place later.

Implemented Surface

Current v0 already includes:

  • Regula main class with new Regula(form, { rules })
  • form-wide defaults for baseline field behavior
  • field-centric rules
  • built-in Signal Forms host integration
  • auto-connected resetOn relationships for the default Signal Forms host
  • resetOn: 'change' | 'empty' | 'initial'
  • field-level derive for pure derived-value updates
  • field-level submit.canSubmit(...) payload gating
  • typed top-level fields.* access from the host form shape
  • diagnostics summaries on form.normalized(), form.payload(), and form.patch()
  • payload serialization and inbound patching
  • regula-oriented field codecs/presets
  • re-form-field standalone wrapper component

Example

Current usage centers on a main Regula class created through its constructor, with Signal Forms wired in directly:

import { Regula } from '@reforgium/regula';

const regula = new Regula(checkoutForm, {
  defaults: {
    errorVisibility: 'dirty-or-submit',
  },
  rules: {
    email: {
      normalize: {
        commit: ['trim', 'lowercase'],
      },
      codec: 'trimmed-string',
      patch: {
        from: 'profile.email',
      },
      submit: {
        include: true,
        codec: 'trimmed-string',
      },
    },
    country: {
      derive: ({ field }) => String(field.value ?? '').toUpperCase() || undefined,
      submit: {
        include: ({ field }) => !!field.value,
      },
    },
    city: {
      disabled: ({ form }) => !form.fields['country'].value,
      resetOn: {
        country: 'change',
      },
      submit: {
        canSubmit: ({ field }) => !field.disabled && !!field.value,
      },
    },
    hasMiddleName: {
      submit: {
        include: true,
      },
    },
    middleName: {
      hidden: ({ form }) => !form.fields['hasMiddleName'].value,
      resetOn: {
        hasMiddleName: 'change',
      },
      submit: {
        include: ({ field }) => !field.hidden,
      },
    },
  },
});

const emailCommit = regula.fields.email.normalized();
const cityState = regula.fields.city.state();
const emailErrors = regula.fields.email.errorVisibility();
const emailRef = regula.fields.email;

regula.form.patch({
  profile: {
    email: ' [email protected] ',
  },
});

regula.form.submitAttempt();
const payload = regula.form.payload();

For the default Signal Forms host, resetOn relationships are wired automatically. If that auto-connect step cannot be created, Regula now reports a diagnostic and throws instead of silently degrading. Manual react(...) is still available as a low-level escape hatch, but normal host usage should not require template event wiring.

Available main APIs:

  • field(key) / fields.key
  • field(key).snapshot()
  • field(key).normalized()
  • field(key).errorVisibility()
  • field(key).state()
  • field(key).react() for low-level/manual relationship triggering
  • form.snapshot()
  • form.normalized()
  • form.payload()
  • form.patch(source)
  • form.connect() / form.disconnect()
  • form.submitAttempt()

Auto-connect notes:

  • autoConnect defaults to true
  • if relationship effects cannot be wired, Regula reports auto-connect:failed and throws
  • autoConnect: false is the intended path for manual/runtime-only scenarios outside Angular injection context
  • reportDiagnostics(entries) can be passed in the constructor options when using the built-in Signal Forms host path

Diagnostics:

  • form.normalized().diagnostics
  • form.normalized().diagnosticsSummary
  • form.payload().diagnostics
  • form.payload().diagnosticsSummary
  • form.patch(source).diagnostics
  • form.patch(source).diagnosticsSummary

Useful field-rule additions:

  • defaults.errorVisibility for form-wide baseline error visibility
  • resetOn: { source: 'change' | 'empty' | 'initial' }
  • resetOn: { source: (ctx) => undefined } to reset the target to empty
  • resetOn: { source: (ctx) => nextValue } to reset the target to a derived value
  • derive: (ctx) => nextValue | undefined for post-cascade pure derived updates
  • submit.canSubmit(ctx) for payload-level submit gating

Derive Boundary

derive is for pure derived values only.

Good fits:

  • derive unit from current product
  • canonicalize or align a dependent field after a cascade

Bad fits:

  • HTTP calls
  • async work
  • service writes
  • store writes
  • command-style side effects such as setProduct(...)

If the need is "react to a field change and start async work", that belongs in app integration code, not in RegulaFieldRules.

There is also a live sandbox example at test-routing/regula, showing:

  • new Regula(...) as the central integration point
  • new Regula(form, { rules }) as the default host scenario
  • re-form-field as a thin field wrapper centered on [regulaField]
  • email commit normalization
  • form-wide default error visibility
  • city disabled state derived from country and auto-reset on country change
  • country value derivation after cascades
  • middle name branch excluded from payload and auto-cleared when the toggle is turned off
  • error visibility after submit attempt
  • final serialized payload and omission reasons, including payload-level submit blocking

Form Field

re-form-field is intentionally narrow. It is a field wrapper, not a UI kit or renderer.

Primary usage:

<re-form-field
  label="Email"
  description="Commit uses trim + lowercase."
  hint="Shown until validation becomes visible."
  required
  [regulaField]="regula.fields.email"
>
  <input type="email" [formField]="checkoutForm.email" />
</re-form-field>

Supported ergonomics:

  • label
  • description
  • hint
  • error
  • required
  • [regulaField] as the main Regula-driven input
  • optional [field] fallback for non-Regula or bridge scenarios
  • reFormFieldControl for explicit control-level accessibility wiring
  • reFormFieldPrefix
  • reFormFieldSuffix
  • reFormFieldFooter

Form Field Public Contract

re-form-field should be treated as a narrow field shell with a small public contract.

Public slots:

  • [reFormFieldControl] on the projected input/select/textarea/custom control that owns focus and validation semantics
  • [reFormFieldPrefix] for leading affordances such as country code, icon, or currency sign
  • [reFormFieldSuffix] for trailing affordances such as actions, counters, or status badges
  • [reFormFieldFooter] for additional helper content below the main hint/error line

Public states:

  • default: no prefix, no suffix, no error
  • prefix only
  • suffix only
  • prefix + suffix
  • disabled via [field] or [regulaField]
  • hidden via [field] or [regulaField]
  • hint visible
  • error visible

Public accessibility behavior:

  • the projected [reFormFieldControl] receives a stable id
  • the label uses for=<control id> and the control receives aria-labelledby=<label id>
  • description and meta text are merged into aria-describedby
  • visible error state sets aria-invalid="true" on the projected control
  • disabled state mirrors to aria-disabled
  • visible error text uses role="alert" and aria-live="polite"

Anything deeper than these slots, states, and CSS variables should be treated as internal DOM structure rather than stable API.

Form Field CSS Variables

re-form-field keeps styling intentionally small, but the main layout and text tokens can be overridden via CSS variables.

  • --re-form-field-gap - outer vertical gap between field sections (0.375rem)
  • --re-form-field-header-gap - gap between label and description (0.125rem)
  • --re-form-field-disabled-opacity - wrapper opacity in disabled state (0.72)
  • --re-form-field-label-gap - gap between label text and required marker (0.25rem)
  • --re-form-field-label-font-weight - label weight (700)
  • --re-form-field-label-color - label color (inherit)
  • --re-form-field-description-font-size - description font size (0.75rem)
  • --re-form-field-description-color - description color (inherit)
  • --re-form-field-description-opacity - description opacity (0.72)
  • --re-form-field-required-color - required marker color (#b42318)
  • --re-form-field-control-gap - gap between prefix / control / suffix (0.625rem)
  • --re-form-field-control-rounded - control row border radius (0.75rem)
  • --re-form-field-slot-color - prefix/suffix tint (color-mix(in srgb, currentColor 72%, transparent))
  • --re-form-field-prefix-padding-start - prefix inset (0.125rem)
  • --re-form-field-suffix-padding-end - suffix inset (0.125rem)
  • --re-form-field-meta-font-size - hint/error font size (0.75rem)
  • --re-form-field-meta-color - hint color (inherit)
  • --re-form-field-meta-opacity - hint opacity (0.78)
  • --re-form-field-error-color - error text color (#b42318)
  • --re-form-field-error-opacity - error text opacity (1)
  • --re-form-field-error-font-weight - error font weight (600)

The older regula + path binding style is intentionally not part of the current surface.

Codecs

regula supports narrow bidirectional field codecs for the common case where:

  • server or storage payload comes in one shape
  • component or form logic works with another shape
  • submit payload needs to be shaped again on the way out

Field rules can use:

  • top-level codec
  • patch.codec
  • submit.codec

Built-in presets are exported as REGULA_CODEC_PRESETS, and the resolver also supports preset names directly.

Typical uses:

  • string trimming
  • lowercased transport values
  • empty-string/null bridging
  • number/string conversion
  • date-only formatting

The low-level serializer engine is shared through hidden @reforgium/internal, but regula keeps a smaller form-oriented codec surface.

Compound Payload Mapping

Some UI fields represent one logical input but do not match backend payload shape.

Typical example:

  • PrimeNG date range keeps one field such as period: [Date | null, Date | null]
  • backend expects either:
    • one scalar string such as period: "2026-04-01..2026-04-09"
    • or a payload fragment such as { start: "2026-04-01", end: "2026-04-09" }

For this case, submit.as can choose between the normal field value path and root payload fragment merge:

period: {
  patch: ({ source }) => [source.start, source.end],
  submit: {
    include: true,
    as: 'fragment',
    map: ({ field }) => {
      const [start, end] = field.value as [string | null, string | null];

      return {
        start,
        end,
      };
    },
  },
}

Use as: 'value' for the normal payload[field] = value path. Use as: 'fragment' when one form field should emit multiple payload keys.

For backends that still want one scalar payload value, keep the normal field key path:

period: {
  submit: {
    include: true,
    as: 'value',
    map: ({ field }) => {
      const [start, end] = field.value as [string | null, string | null];

      return `${start ?? ''}-${end ?? ''}`;
    },
  },
}

Typing

For the standard new Regula(form, { rules }) path, regula now binds field APIs and rule keys to the host form shape.

That means:

  • regula.fields.email.normalized().value is inferred from the form field value type
  • rules keys are checked against known form keys
  • fields config keys are checked against known form keys
  • ctx.form.fields.product.value is typed inside short-constructor rule callbacks

If submit.canSubmit(...) is present, you do not need to repeat include: true. include already defaults to true when a submit block exists.

Scalar Fields

Typical scalar fields should not need manual type arguments:

const regula = new Regula(checkoutForm, {
  rules: {
    email: {
      normalize: {
        commit: ['trim', 'lowercase'],
      },
    },
  },
});

const email = regula.fields.email.normalized().value;
// string

Typed Rules Map

If you want an explicit rules object before constructing Regula, use RegulaTypedFieldRulesMap:

import type { RegulaTypedFieldRulesMap } from '@reforgium/regula';

type CheckoutForm = typeof checkoutForm;

const rules: RegulaTypedFieldRulesMap<CheckoutForm> = {
  email: {
    errorVisibility: 'dirty-or-submit',
  },
};

Typed Options Helper

new Regula(form, options) remains the main and only construction path.

If you want to keep typed options in a separate constant without repeating the full Regula generics at the call site, use defineRegulaOptions(...):

import { defineRegulaOptions, Regula } from '@reforgium/regula';

const options = defineRegulaOptions<CheckoutPatchSource, CheckoutPayload>(checkoutForm, {
  rules: {
    email: {
      patch: {
        from: 'profile.email',
        map: ({ source }) => source.profile.email.trim(),
      },
    },
  },
});

const regula = new Regula(checkoutForm, options);

Date Period Fields

Compound fields such as PrimeNG date ranges are still harder for TypeScript to infer perfectly, especially when the field value is a tuple or custom object.

For those cases, regula now helps with:

  • key-safe field rules
  • value inference for regula.fields.period
  • field-level patch/payload support

But a local cast inside submit.map(...) can still be reasonable when the UI field shape is complex:

period: {
  submit: {
    as: 'fragment',
    map: ({ field }) => {
      const [start, end] = field.value as [Date | null, Date | null];

      return {
        start: start ? formatDate(start) : null,
        end: end ? formatDate(end) : null,
      };
    },
  },
}

The goal is to keep as local to the compound field boundary instead of scattering casts across the whole form integration.

Not in v0

  • broad async orchestration
  • debounce policy framework
  • state machine for sections or whole forms
  • changed-only submission engine
  • advanced server derive/reconciliation model
  • renderer integration layer
  • schema-driven configuration

Package Status

regula is still early and intentionally narrow, but it is no longer only a package scaffold.

What exists now:

  • working Nx package
  • implemented Regula runtime
  • built-in Signal Forms host path
  • auto-connected resetOn relationship wiring for Signal Forms
  • sandbox page at test-routing/regula
  • initial re-form-field component
  • initial codec/patch/payload path

What still needs pressure:

  • real-form validation against more than one consumer
  • tighter relationship/reset cascade model
  • continued pressure to keep Regula a layer above the host form system

Go / No-Go Questions

Proceed only if the answers remain "yes":

  1. Can this stay a behavior layer instead of a form platform?
  2. Can the first version solve concrete cases with a small API?
  3. Can the cascade model stay deterministic and debuggable?
  4. Can Angular integration stay behind a narrow host boundary?
  5. Will more than one package or app in the repo realistically reuse it?