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

@alexcarpenter/form

v0.1.0

Published

Tiny headless forms for Nano Stores

Downloads

99

Readme

Nano Stores Form

Tiny headless forms for Nano Stores.

  • Small. One core entrypoint and no runtime dependencies except Nano Stores.
  • Nano Stores native. A form is a store with get(), listen(), subscribe(), and form actions.
  • Headless. Use it with React, Preact, Vue, Svelte, Solid, or vanilla JS.
  • Flat by design. Field names are top-level keys. Schema adapters and framework helpers can live in separate entrypoints later.
import { form } from '@alexcarpenter/form'

export const $login = form({
  email: '',
  password: ''
}, {
  validate: ({ values }) => ({
    email: values.email.includes('@') ? undefined : 'Invalid email',
    password: values.password ? undefined : 'Required'
  }),
  onSubmit: async ({ values }) => {
    await api.login(values)
  }
})

$login.setValue('email', '[email protected]')
await $login.handleSubmit()

Install

pnpm add nanostores @alexcarpenter/form

Guide

TanStack-style config

If you prefer the TanStack Form shape, use createForm() with defaultValues and validators.

import { createForm } from '@alexcarpenter/form'

export const $login = createForm({
  defaultValues: {
    email: '',
    password: ''
  },
  validators: {
    onChange: ({ values }) => ({
      email: values.email.includes('@') ? undefined : 'Invalid email'
    }),
    onChangeAsync: async ({ value }) => {
      if (value && !(await api.isEmailAvailable(value))) return 'Email is taken'
    },
    onSubmit: ({ values }) => ({
      password: values.password ? undefined : 'Required'
    })
  },
  onChangeAsyncDebounceMs: 300,
  onSubmit: async ({ values }) => {
    await api.login(values)
  }
})

form(defaultValues, opts) is kept as the smaller direct style.

Form state

$form.get()
//=> {
//=>   values: { email: '' },
//=>   errors: {},
//=>   touched: {},
//=>   dirty: {},
//=>   isSubmitting: false,
//=>   isValidating: false,
//=>   submitCount: 0,
//=>   canSubmit: true
//=> }

Validation

A validator receives the form values. Return an object of field errors for form validation.

const $form = form({ email: '' }, {
  validate: ({ values }) => ({
    email: values.email ? undefined : 'Required'
  })
})

await $form.validate()

When a single field is validated, field and value are provided. Return the field error directly.

const $form = form({ email: '' }, {
  validate: ({ field, value }) => {
    if (field === 'email' && !value) return 'Required'
  }
})

await $form.validate('email', 'blur')

Validators can be async. Late async results are ignored if a newer validation started first. TanStack-style onChangeAsync, onBlurAsync, and onSubmitAsync validators are also supported, with onChangeAsyncDebounceMs, onBlurAsyncDebounceMs, onSubmitAsyncDebounceMs, or shared asyncDebounceMs options.

const $form = form({ username: '' }, {
  validate: async ({ field, value }) => {
    if (field === 'username') {
      let available = await api.isUsernameAvailable(value)
      if (!available) return 'Username is taken'
    }
  }
})

Fields

Fields are created lazily and are backed by the form store.

const email = $form.field('email')

email.set('[email protected]')
await email.blur()

email.get()
//=> {
//=>   name: 'email',
//=>   value: '[email protected]',
//=>   errors: [],
//=>   touched: true,
//=>   dirty: true
//=> }

Submit

const $form = form({ email: '' }, {
  validate: ({ values }) => ({
    email: values.email ? undefined : 'Required'
  }),
  onSubmit: async ({ values }) => {
    await fetch('/login', {
      body: JSON.stringify(values),
      method: 'POST'
    })
  }
})

let ok = await $form.handleSubmit()

handleSubmit(event?) prevents the event default when provided, touches all fields, validates the form, and only calls onSubmit when valid.

$form.resetField('email')
await $form.validateField('email')

Optional modules

Optional features live in separate entrypoints so they are not included by core imports.

Standard Schema

Use any validator that supports Standard Schema, such as Zod or Valibot.

import { standardSchema } from '@alexcarpenter/form/standard-schema'

const $form = form({ email: '' }, {
  validate: standardSchema(schema)
})

Debounce

import { debounce } from '@alexcarpenter/form/debounce'

const username = debounce($form.field('username'), 300)

username.set('alex')
username.cancel()
username.flush()

Path

import { path } from '@alexcarpenter/form/path'

const email = path($form, 'user.email')
email.set('[email protected]')

Array

import { array } from '@alexcarpenter/form/array'

const items = array($form, 'items')
items.push({ title: '' })
items.insert(0, { title: 'First' })
items.replace(0, { title: 'Updated' })
items.move(0, 1)
items.swap(0, 1)
items.remove(0)

Select

Create a small derived store for a slice of form state. Useful for React when a component should re-render only for one value.

import { select } from '@alexcarpenter/form/select'

const $canSubmit = select($form, form => form.canSubmit)
const $email = select($form, form => form.values.email)

Debug

import { debug } from '@alexcarpenter/form/debug'

let stop = debug($form, { name: 'login' })

React

Use the normal Nano Stores React binding:

import { useStore } from '@nanostores/react'
import { select } from '@alexcarpenter/form/select'
import { $login } from './stores/login'

const $canSubmit = select($login, form => form.canSubmit)

export function LoginForm() {
  let canSubmit = useStore($canSubmit)
  let email = useStore($login.field('email').$state)

  return <form onSubmit={$login.handleSubmit}>
    <input
      value={email.value}
      onBlur={() => $login.field('email').handleBlur()}
      onInput={event => $login.field('email').handleChange(event.currentTarget.value)}
    />
    {email.errors[0]}
    <button disabled={!canSubmit}>Submit</button>
  </form>
}