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

@thomasfosterau/effect-forms-svelte

v0.1.0

Published

Svelte 5 (runes) bindings for @thomasfosterau/effect-forms — a reactive form store, an enhance action, tainted tracking, and client-side validation.

Readme

@thomasfosterau/effect-forms-svelte

Svelte 5 (runes) bindings for @thomasfosterau/effect-forms. A reactive form store with progressive-enhancement submission, tainted tracking, lifecycle events, and optional client-side validation against the same Effect Schema the server uses.

This package depends only on svelte (a peer) and the framework-neutral core — not SvelteKit — so it works in any Svelte 5 app.

Usage

Server (+page.server.ts or any action) produces a FormState with the core's makeForm, and returns it to the page. The component hydrates it:

<script lang="ts">
  import { createForm } from "@thomasfosterau/effect-forms-svelte";
  import { Signup } from "$lib/schemas";

  let { data } = $props(); // data.form is the server FormState

  const form = createForm(data.form, {
    schema: Signup,        // enables client-side validation
    resetForm: true,
    taintedMessage: "You have unsaved changes.",
    onUpdated: ({ form }) => form.valid && toast("Saved"),
  });
</script>

<form method="post" use:form.enhance>
  <input name="name" bind:value={form.data.name} />
  {#each form.errorsAt(["name"]) as message}<small>{message}</small>{/each}

  <button disabled={form.submitting}>
    {form.delayed ? "Saving…" : "Save"}
  </button>
</form>

The store

createForm(source, options) returns a FormStore. source is a server-produced FormState, a { schema, defaults } seed, or a FormBuilder — the builder derives its schema, blank defaults, and array-item defaults for you:

import { createForm } from "@thomasfosterau/effect-forms-svelte";
import { signup } from "$lib/forms"; // a FormBuilder

const form = createForm(signup, { mode: { validation: "onChange", debounce: 300 } });

Reactive statedata (mutable, bind inputs to it), errors (merged client + server), clientErrors / serverErrors (per-source views), message, constraints, submitting, delayed, posted, valid, canSubmit, isValidating, isSubmitted, isSubmitSuccessful, submissionAttempts, tainted, isTainted, and (against the last submission) changedSinceSubmit / hasChangedSinceSubmit.

Methodsenhance(node) (the form action), validate() / validateField(path) (client-side, needs schema; also runs a builder's refineEffect refinements), errorsAt(path) / taintedAt(path) / isValidatingAt(path), setValue(path, v) / valueAt(path), handleBlur(path) / setTouched(path) / isTouchedAt(path), isDirtyAt(path) / isDefaultValueAt(path) (persistent per-field dirty tracking — see Field handles), setFieldValidators(path, validators) (register a field's validators dynamically), shouldShowError(path) / visibleErrorsAt(path) (mode-aware display gating), constraintAt(path), revertToLastSubmit(), reset(), capture() / restore() (SvelteKit snapshots), setServerState(data, errors) (apply a result received some other way), toFormData().

Field arraysappend(path, value?), insertItem(path, i, value?), replaceItem(path, i, value), removeItem(path, i), clearItems(path), swap(path, a, b), move(path, from, to) mutate the array at path immutably. append / insertItem derive a blank item by walking the bound schema's AST at path (any nesting depth, e.g. authors[0].emails), falling back to the top-level FormBuilder field when no schema is bound. Every index-shifting mutation (insertItem / removeItem / swap / move / clearItems) remaps per-index touched flags and errors so they keep following their row — replaceItem drops only the replaced index's own touched flag / errors.

Field handles

form.field(path) (modelled on TanStack Form's FieldApi) returns a typed, reactive FieldHandle bundling everything one input needs, instead of wiring bind:value / name / handleBlur / visibleErrorsAt / constraints against the store separately:

<script lang="ts">
  const name = form.field("author.name"); // deep paths are type-checked
</script>

<input
  name={name.name}
  bind:value={name.value}
  onblur={name.handleBlur}
  {...name.constraints}
/>
{#each name.visibleErrors as message}<small>{message}</small>{/each}

path is checked against the form data's shape via Path.FieldPath<A> — a typo'd segment ("authr.name") or a path into a field that doesn't exist is a compile error, and field.value is the leaf's actual type, not unknown. This replaced the core's unconsumed FieldRef seam.

field.meta exposes TanStack-style field state: isBlurred (lost focus at least once), isTouched (isBlurred || isDirty), isDirty (changed away from the default at some point — persistent, unlike taintedAt it does not clear on reverting to the default; only reset() clears it), and isDefaultValue (current value equals the default — flips back on revert). isDirty only tracks writes made through field.value = … / setValue / the array mutators (append / removeItem / swap / move) — a form that still binds bind:value={form.data.x} directly, bypassing field(), never marks that field dirty.

Validation modes — the mode option (see Mode) drives when the form validates: onSubmit (default), onBlur (call handleBlur from an input's blur), or onChange (debounced). onBlur / onChange may autoSubmit, turning the form into a live-saving surface. revalidate ("onChange" default / "onBlur" / "never") governs what happens after a failed post in onSubmit mode: fixing a field re-validates just that field, clearing its error without a resubmit. Errors are computed for the whole form; gate their display per field with visibleErrorsAt(path) (shown only once the field is tainted / touched, or after a submit).

Per-field validators — the validators option (or setFieldValidators) attaches TanStack-Form-style { onChange?, onBlur?, onChangeAsync?, onBlurAsync?, asyncDebounceMs? } per field, keyed by Path.toString (e.g. "author.name", "tags[0]"). Sync validators return a Schema.FilterOutput; async ones are Effects, run as a fiber per field — a new value interrupts the field's in-flight fiber before forking the next run, which is what gives the debounce + "latest wins" cancellation (no timer bookkeeping needed). Pass layer when a validator (or a builder's refineEffect refinement) needs services on its R channel:

const form = createForm(
  { schema: Signup, defaults: { username: "" } },
  {
    validators: {
      username: {
        onChangeAsync: (value) => Api.checkUsernameAvailable(value as string),
        asyncDebounceMs: 300,
      },
    },
    layer: ApiClient.layer,
  },
);

Error sources — a field's server-reported error (from the last submission) survives a client-side revalidation of some other field; it clears once that field's own value changes (or the server clears it on the next submit). errors is the merged view; clientErrors / serverErrors expose the two trees separately for a UI that wants to style them differently.

Lifecycle optionsonSubmit (with cancel()), onResult, onUpdate, onUpdated, onError, onRedirect, plus resetForm, taintedMessage, delayMs, and a submit transport override (defaults to fetch to the form's action, parsing a JSON FormActionResult or a bare FormState).

Layout

The reactive store lives in FormStore.svelte.ts (rune-compiled, checked by svelte-check). Its logic is delegated to pure, unit-tested modules:

| Module | Purpose | | ----------------------------------------- | ------------------------------------------------------------------------- | | Tainted | Deep-diff the data to compute the tainted tree. | | ClientValidate | Validate the form / one field against the schema. | | EnhanceCore | Serialise data → form body, parse the response, compute the store update. |

The store itself is mostly exercised only by svelte-check, but the submitter-aware enhanced submit path (enhance#runSubmit → the posted FormData) is directly unit-tested in FormStore.svelte.test.ts — a .svelte.ts rune file needs the Svelte compiler to run at all, hence the .svelte.test.ts extension and the svelte() plugin in vite.config.ts (scoped to a happy-dom DOM via a per-file @vitest-environment docblock, not the package default).

Development

vp exec svelte-check --tsconfig ./tsconfig.json   # typecheck (incl. runes)
vp test run tests                                 # unit tests (pure modules + the submit path)