Skip to content

Releases: pavelivanov/formular

v4.0.0-beta.3

v4.0.0-beta.3 Pre-release
Pre-release

Choose a tag to compare

@pavelivanov pavelivanov released this 22 Apr 11:40

4.0.0-beta.3 β€” unreleased

Added

  • formular/testing subpath export with a createTestForm<T>(options)
    helper. Returns { form, Provider } β€” a Form instance you can read
    and mutate directly from test code, plus a minimal React wrapper that
    puts that form on context for rendered components. Renderer-agnostic
    (works with @testing-library/react, Enzyme, bare react-dom, etc.).
  • FormContextProvider now accepts an optional form prop. When
    provided, the Provider puts the caller-controlled instance on context
    and skips its internal lifecycle (no setOptions/setInitialValues
    re-seed/destroy-on-unmount). Additive β€” existing usage is unchanged.
    Primarily for tests and advanced users who want to own the form's
    lifetime.

Bundle

Bundle Limit (brotli) Actual
ESM entry 6.5 kB 5.99 kB (+30 B for the provider split)
CJS entry 7 kB 6.25 kB
Tree-shaken core 5 kB 4.35 kB
Devtools subpath 5 kB 3.12 kB
Testing subpath 5 kB 4.21 kB (new)

v4.0.0-beta.2

v4.0.0-beta.2 Pre-release
Pre-release

Choose a tag to compare

@pavelivanov pavelivanov released this 22 Apr 11:26

4.0.0-beta.2 β€” unreleased

Added

  • formular/devtools subpath export with a floating
    <FormularDevtools /> inspection panel. Three tabs: state (values,
    errors, flags), fields (registered paths + per-field flags), events
    (rolling log of every form event). Self-contained inline styles, no
    portals, no global CSS, opt-in via explicit render. Props for
    position, default open/tab, and an enabled kill-switch.
  • Shipping as a subpath export of the same package (not a separate
    npm name) β€” single version, and tree-shakes out of the main bundle
    when not imported. The devtools bundle weighs 3.1 KB brotli;
    main bundle is unchanged.
  • Integrated into examples/vite-react so you can poke at it via
    npm run dev.

Changed

  • tsdown.config.ts switched from a single multi-entry build to two
    independent build passes (dist/index.js + dist/devtools/index.js).
    The multi-entry approach produced a shared chunk that every entry
    had to load, inflating the main bundle for consumers that don't use
    devtools. Two passes produce self-contained bundles β€” devtools pays
    a small code-duplication cost, main bundle stays lean.

Bundle

Bundle Limit (brotli) Actual
ESM entry 6.5 kB 5.96 kB (unchanged)
CJS entry 7 kB 6.22 kB (unchanged)
Tree-shaken core 5 kB 4.32 kB (unchanged)
Devtools subpath 5 kB 3.12 kB (new)

v4.0.0-beta.1

v4.0.0-beta.1 Pre-release
Pre-release

Choose a tag to compare

@pavelivanov pavelivanov released this 21 Apr 20:00

4.0.0-beta.1

First published release of the v4 line. The pre-release line under the
next dist-tag; latest remains on v3 until external adopters
confirm stability. Rollup of everything built across six in-tree alphas.

Summary

  • Ground-up rewrite. The public API from v3 is not preserved β€” see
    MIGRATION.md for the v3 β†’ v4 mapping.
  • Class-based Form<T> + FieldManager<T> pair. FormContextProvider
    puts a single form on React context; hooks subscribe to it via
    useSyncExternalStore. Components re-render only on the slice they
    read.
  • Peer dep: react >=18. Bundle: 5.96 KB brotli (ESM, everything),
    4.32 KB (tree-shaken core). Under CI size budgets.

What's in this release

Core

  • Form<T> / FieldManager<T> replace v3's Form / Field /
    FormGroup trio. Nesting is expressed via field names; FormGroup
    is not ported.
  • FormContextProvider<T> + useFormContext<T> for an ambient form.
  • useSyncExternalStore-based hooks β€” useForm, useFormState,
    useFieldRegister, useField, useFormValidation β€” each
    subscribes to a specific slice so siblings don't thrash.
  • Async validation sequenced by a monotonic counter (replaces v3's
    CancelablePromise). Stale runs discard themselves.
  • handleSubmit(onSuccess?) returns a React-safe submit handler.
    Distinct submit and submit error events with a
    phase: 'submit' | 'onSuccess' tag on errors.
  • setValues / setInitialValues buffer writes for fields that
    haven't registered yet, so async-hydrated data doesn't race field
    mounts.

Typed API (the headline)

  • createForm<T>() factory closes the form shape over a namespace
    of hooks β€” useFieldRegister, useFieldArray, useField, plus the
    Provider β€” so literal path arguments narrow at the call site:

    const contact = createForm<ContactForm>()
    const street = contact.useFieldRegister('address.street')
    //    ^? FieldManager<string>

    TypeScript's partial-explicit-generic inference doesn't narrow
    const type parameters in hook signatures; the factory sidesteps
    that by keeping only one generic in play at call-sites.

  • Typed nested paths: Path<T>, PathValue<T, P>, ArrayPath<T>,
    and DeepPartial<T> are exported. setValues / setInitialValues
    take DeepPartial<T> and walk down to each registered field.
    getValues() reconstructs the nested shape.

Validation

  • Standard Schema v1 accepted directly at the field level
    (Zod 3.24+, Valibot 0.40+, ArkType, …). The FieldOptions.schema
    option runs the schema before validators; the first issue message
    becomes the field error. No @hookform/resolvers-style adapter
    package required β€” the interface is inlined at
    src/standard-schema.ts and re-exported as StandardSchemaV1.
  • Pipeline order: required β†’ schema β†’ validators. Each stage
    short-circuits on the first error.
  • 15 built-in function validators kept for BYO (minLength,
    maxLength, pattern, email, url, phoneNumber, numeric,
    min, max, creditCard, dateFormat, minAge, confirmField,
    asyncValidator, compose). Recommendation in the README: prefer
    schema for new projects.

Field arrays

  • useFieldArray(path) with the usual list operations: append,
    prepend, insert, remove, swap, move, replace, clear.
    Returns { fields, ...ops } where fields[i].id is stable across
    mutations so React keys survive reorders.
  • Per-row field reindexing: sub-fields registered at
    items.<N>.<rest> are atomically renamed when list operations run.
    A field's state (value, error, touched, subscriptions) carries over
    to its new index rather than being orphaned. replace and clear
    destroy the sub-fields; new rows register fresh. Observable via the
    field renamed event.

Components

  • Headless FieldLabel and FieldError. Plain <label> and an
    aria-live="polite" div. No styling opinions β€” pass your own
    classNames.

Tooling

  • Build: tsdown. ESM + CJS output with a proper exports map and
    sideEffects: false.

  • Test: Vitest. 74 tests colocated under src/**/*.spec.ts.

  • Lint: ESLint 9 flat config.

  • CI matrix: Node 18 / 20 / 22 running typecheck + lint + build

    • test + size.
  • Bundle-size budget in CI via size-limit:

    Bundle Limit (brotli) Actual
    ESM entry 6.5 kB 5.96 kB
    CJS entry 7 kB 6.22 kB
    Tree-shaken core 5 kB 4.32 kB

Notable fixes discovered during the alpha series

  • setByPath used to mutate caller-owned arrays and objects when
    reconstructing form state, which silently corrupted field-array
    internals (out.rows[2] = {} on a 2-item field would expand the
    live array to 3 items). setByPath now shallow-copies each
    container on descend.
  • unregisterField now takes an optional expectedField reference
    and skips destruction if a different FieldManager occupies the
    path β€” otherwise a stale useEffect cleanup could destroy a field
    that a concurrent array reindex moved there.
  • FieldManager.validate no longer hangs when a validator throws
    (v3 bug that survived into alpha.1; fixed in alpha.2).

Example app

examples/vite-react is a runnable Vite + React 19 contact form
exercising nested paths, useFieldArray, and Zod schemas (including
an async .refine()). See its README.

v3.0.0

Choose a tag to compare

@pavelivanov pavelivanov released this 02 Apr 15:56

Form.submit and FormGroup.submit result format changed. Beware updating to this version!

< 3.0.0

form.submit()
  .then((values) => {

  })
  .catch((errors) => {
    // validation errors
  })

or

try {
  const values = await form.submit()
}
catch (errors) {
  // validation errors
}

>= 3.0.0

form.submit()
  .then(({ values, errors }) => {
    if (errors) {
       // handle errors
    }
    else {
       // do smth
    }
  })

or

const { values, errors } = await form.submit()

The Reason

try {
   const values = await form.submit()

   // other code that can cause an error
}
catch (errors) {
   // errors can contain not only form validation errors but any type of error from code after form.submit()
}

v2.6.0

Choose a tag to compare

@pavelivanov pavelivanov released this 24 Mar 16:25
v2.6.0