{"_id":"@alexcarpenter/form","_rev":"2-970546a893b0a47e789a9e873fc76315","name":"@alexcarpenter/form","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@alexcarpenter/form","version":"0.1.0","keywords":["forms","form","nanostores","state"],"author":{"name":"Alex Carpenter"},"license":"MIT","_id":"@alexcarpenter/form@0.1.0","maintainers":[{"name":"alexcarpenter","email":"im.alexcarpenter@gmail.com"}],"homepage":"https://github.com/alexcarpenter/form#readme","bugs":{"url":"https://github.com/alexcarpenter/form/issues"},"dist":{"shasum":"a71530b26b584a8768cd37356a3169cbb301ea5b","tarball":"https://registry.npmjs.org/@alexcarpenter/form/-/form-0.1.0.tgz","fileCount":18,"integrity":"sha512-g7o5WyjWHkzI4OwQeCvGMW01KLf7EeCEbV7qqivCvnUpW+nf4R4g6IgHzKS0ZHCerdowqx7sRpfPuGDTfQ8HmQ==","signatures":[{"sig":"MEUCIQCMWcSk5BQyIDJqwe8UF9Z7Vc4w7HAMVD6AS+7HJNOjvwIgeGfk6rtjeAU84Tsy5BP97CMWrbC5J/tOZnxOVNjJXoE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":26662},"type":"module","types":"./index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./path":{"types":"./path/index.d.ts","default":"./path/index.js"},"./array":{"types":"./array/index.d.ts","default":"./array/index.js"},"./debug":{"types":"./debug/index.d.ts","default":"./debug/index.js"},"./select":{"types":"./select/index.d.ts","default":"./select/index.js"},"./debounce":{"types":"./debounce/index.d.ts","default":"./debounce/index.js"},"./package.json":"./package.json","./standard-schema":{"types":"./standard-schema/index.d.ts","default":"./standard-schema/index.js"}},"gitHead":"393d34dd3929bbeeee5c7ba5614b90ac8bcc92f7","scripts":{"test":"pnpm run /^test:/","test:lint":"oxlint","test:size":"size-limit","test:types":"check-dts","test:runtime":"node --test test/*.test.js"},"_npmUser":{"name":"alexcarpenter","email":"im.alexcarpenter@gmail.com"},"repository":{"url":"git+https://github.com/alexcarpenter/form.git","type":"git"},"size-limit":[{"name":"Core","limit":"2 KB","import":{"./index.js":"{ form }"}},{"name":"Debug","limit":"250 B","import":{"./debug/index.js":"{ debug }"}},{"name":"Debounce","limit":"250 B","import":{"./debounce/index.js":"{ debounce }"}},{"name":"Select","limit":"250 B","import":{"./select/index.js":"{ select }"}},{"name":"Standard Schema","limit":"350 B","import":{"./standard-schema/index.js":"{ standardSchema }"}},{"name":"Path","limit":"550 B","import":{"./path/index.js":"{ path }"}},{"name":"Array","limit":"350 B","import":{"./array/index.js":"{ array }"}}],"_npmVersion":"11.10.1","description":"Tiny headless forms for Nano Stores","directories":{},"sideEffects":false,"_nodeVersion":"25.7.0","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxlint":"^1.69.0","check-dts":"^1.0.0","nanostores":"^1.0.0","size-limit":"^12.0.0","typescript":"^5.0.0","@size-limit/preset-small-lib":"^12.0.0"},"peerDependencies":{"nanostores":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/form_0.1.0_1782649822958_0.37391683981729407","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"_id":"@alexcarpenter/form@0.1.1","bugs":{"url":"https://github.com/alexcarpenter/form/issues"},"dist":{"shasum":"66e36ac3f7e46937fca137c1640cda627d0a771b","tarball":"https://registry.npmjs.org/@alexcarpenter/form/-/form-0.1.1.tgz","integrity":"sha512-f819+2FmfhCQIQUz8HssWX+qe8c25OeZ2KegbRIwr/wMhAz01IT7zoxRNutb8aqqJSKMN0EgCb5kBQU5kaXEIQ==","fileCount":18,"unpackedSize":26710,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@alexcarpenter%2fform@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICdDcADBWZV+wYN3VVM3wO7LxrgyjLJeL0/5Yn4eujwDAiEA6432d8uSbcXty6oq03NNYb9ijWHN+1RIyaE4wYd1yGA="}]},"name":"@alexcarpenter/form","type":"module","types":"./index.d.ts","author":{"name":"Alex Carpenter"},"engines":{"node":">=20"},"exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./path":{"types":"./path/index.d.ts","default":"./path/index.js"},"./array":{"types":"./array/index.d.ts","default":"./array/index.js"},"./debug":{"types":"./debug/index.d.ts","default":"./debug/index.js"},"./select":{"types":"./select/index.d.ts","default":"./select/index.js"},"./debounce":{"types":"./debounce/index.d.ts","default":"./debounce/index.js"},"./package.json":"./package.json","./standard-schema":{"types":"./standard-schema/index.d.ts","default":"./standard-schema/index.js"}},"gitHead":"fddb37c88efed9b1bf79c672f6c5cd9c67d8c7f0","license":"MIT","scripts":{"test":"pnpm run /^test:/","test:lint":"oxlint","test:size":"size-limit","test:types":"check-dts","test:runtime":"node --test test/*.test.js"},"version":"0.1.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8bf61bf5-0f43-4002-80fa-3cc0718ef60c"},"approver":{"name":"alexcarpenter","email":"im.alexcarpenter@gmail.com"}},"homepage":"https://github.com/alexcarpenter/form#readme","keywords":["forms","form","nanostores","state"],"repository":{"url":"git+https://github.com/alexcarpenter/form.git","type":"git"},"size-limit":[{"name":"Core","limit":"2 KB","import":{"./index.js":"{ form }"}},{"name":"Debug","limit":"250 B","import":{"./debug/index.js":"{ debug }"}},{"name":"Debounce","limit":"250 B","import":{"./debounce/index.js":"{ debounce }"}},{"name":"Select","limit":"250 B","import":{"./select/index.js":"{ select }"}},{"name":"Standard Schema","limit":"350 B","import":{"./standard-schema/index.js":"{ standardSchema }"}},{"name":"Path","limit":"550 B","import":{"./path/index.js":"{ path }"}},{"name":"Array","limit":"350 B","import":{"./array/index.js":"{ array }"}}],"_npmVersion":"11.17.0","description":"Tiny headless forms for Nano Stores","directories":{},"maintainers":[{"name":"alexcarpenter","email":"im.alexcarpenter@gmail.com"}],"sideEffects":false,"_nodeVersion":"26.5.0","publishConfig":{"access":"public","provenance":true},"packageManager":"pnpm@10.32.1","devDependencies":{"oxlint":"^1.69.0","check-dts":"^1.0.0","nanostores":"^1.0.0","size-limit":"^12.0.0","typescript":"^5.0.0","@size-limit/preset-small-lib":"^12.0.0"},"peerDependencies":{"nanostores":"^1.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/form_0.1.1_1785329379438_0.3371222345175162"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-28T12:30:22.850Z","modified":"2026-07-29T12:49:39.841Z","0.1.0":"2026-06-28T12:30:23.116Z","0.1.1":"2026-07-29T12:49:39.520Z"},"bugs":{"url":"https://github.com/alexcarpenter/form/issues"},"author":{"name":"Alex Carpenter"},"license":"MIT","homepage":"https://github.com/alexcarpenter/form#readme","keywords":["forms","form","nanostores","state"],"repository":{"url":"git+https://github.com/alexcarpenter/form.git","type":"git"},"description":"Tiny headless forms for Nano Stores","maintainers":[{"name":"alexcarpenter","email":"im.alexcarpenter@gmail.com"}],"readme":"# Nano Stores Form\n\nTiny headless forms for [Nano Stores](https://github.com/nanostores/nanostores).\n\n* **Small.** One core entrypoint and no runtime dependencies except Nano Stores.\n* **Nano Stores native.** A form is a store with `get()`, `listen()`,\n  `subscribe()`, and form actions.\n* **Headless.** Use it with React, Preact, Vue, Svelte, Solid, or vanilla JS.\n* **Flat by design.** Field names are top-level keys. Schema adapters and\n  framework helpers can live in separate entrypoints later.\n\n```js\nimport { form } from '@alexcarpenter/form'\n\nexport const $login = form({\n  email: '',\n  password: ''\n}, {\n  validate: ({ values }) => ({\n    email: values.email.includes('@') ? undefined : 'Invalid email',\n    password: values.password ? undefined : 'Required'\n  }),\n  onSubmit: async ({ values }) => {\n    await api.login(values)\n  }\n})\n\n$login.setValue('email', 'alex@example.com')\nawait $login.handleSubmit()\n```\n\n## Install\n\n```sh\npnpm add nanostores @alexcarpenter/form\n```\n\n## Guide\n\n### TanStack-style config\n\nIf you prefer the TanStack Form shape, use `createForm()` with\n`defaultValues` and `validators`.\n\n```js\nimport { createForm } from '@alexcarpenter/form'\n\nexport const $login = createForm({\n  defaultValues: {\n    email: '',\n    password: ''\n  },\n  validators: {\n    onChange: ({ values }) => ({\n      email: values.email.includes('@') ? undefined : 'Invalid email'\n    }),\n    onChangeAsync: async ({ value }) => {\n      if (value && !(await api.isEmailAvailable(value))) return 'Email is taken'\n    },\n    onSubmit: ({ values }) => ({\n      password: values.password ? undefined : 'Required'\n    })\n  },\n  onChangeAsyncDebounceMs: 300,\n  onSubmit: async ({ values }) => {\n    await api.login(values)\n  }\n})\n```\n\n`form(defaultValues, opts)` is kept as the smaller direct style.\n\n### Form state\n\n```js\n$form.get()\n//=> {\n//=>   values: { email: '' },\n//=>   errors: {},\n//=>   touched: {},\n//=>   dirty: {},\n//=>   isSubmitting: false,\n//=>   isValidating: false,\n//=>   submitCount: 0,\n//=>   canSubmit: true\n//=> }\n```\n\n### Validation\n\nA validator receives the form values. Return an object of field errors for form\nvalidation.\n\n```js\nconst $form = form({ email: '' }, {\n  validate: ({ values }) => ({\n    email: values.email ? undefined : 'Required'\n  })\n})\n\nawait $form.validate()\n```\n\nWhen a single field is validated, `field` and `value` are provided. Return the\nfield error directly.\n\n```js\nconst $form = form({ email: '' }, {\n  validate: ({ field, value }) => {\n    if (field === 'email' && !value) return 'Required'\n  }\n})\n\nawait $form.validate('email', 'blur')\n```\n\nValidators can be async. Late async results are ignored if a newer validation\nstarted first. TanStack-style `onChangeAsync`, `onBlurAsync`, and\n`onSubmitAsync` validators are also supported, with\n`onChangeAsyncDebounceMs`, `onBlurAsyncDebounceMs`, `onSubmitAsyncDebounceMs`,\nor shared `asyncDebounceMs` options.\n\n```js\nconst $form = form({ username: '' }, {\n  validate: async ({ field, value }) => {\n    if (field === 'username') {\n      let available = await api.isUsernameAvailable(value)\n      if (!available) return 'Username is taken'\n    }\n  }\n})\n```\n\n### Fields\n\nFields are created lazily and are backed by the form store.\n\n```js\nconst email = $form.field('email')\n\nemail.set('alex@example.com')\nawait email.blur()\n\nemail.get()\n//=> {\n//=>   name: 'email',\n//=>   value: 'alex@example.com',\n//=>   errors: [],\n//=>   touched: true,\n//=>   dirty: true\n//=> }\n```\n\n### Submit\n\n```js\nconst $form = form({ email: '' }, {\n  validate: ({ values }) => ({\n    email: values.email ? undefined : 'Required'\n  }),\n  onSubmit: async ({ values }) => {\n    await fetch('/login', {\n      body: JSON.stringify(values),\n      method: 'POST'\n    })\n  }\n})\n\nlet ok = await $form.handleSubmit()\n```\n\n`handleSubmit(event?)` prevents the event default when provided, touches all\nfields, validates the form, and only calls `onSubmit` when valid.\n\n```js\n$form.resetField('email')\nawait $form.validateField('email')\n```\n\n## Optional modules\n\nOptional features live in separate entrypoints so they are not included by\ncore imports.\n\n### Standard Schema\n\nUse any validator that supports Standard Schema, such as Zod or Valibot.\n\n```js\nimport { standardSchema } from '@alexcarpenter/form/standard-schema'\n\nconst $form = form({ email: '' }, {\n  validate: standardSchema(schema)\n})\n```\n\n### Debounce\n\n```js\nimport { debounce } from '@alexcarpenter/form/debounce'\n\nconst username = debounce($form.field('username'), 300)\n\nusername.set('alex')\nusername.cancel()\nusername.flush()\n```\n\n### Path\n\n```js\nimport { path } from '@alexcarpenter/form/path'\n\nconst email = path($form, 'user.email')\nemail.set('alex@example.com')\n```\n\n### Array\n\n```js\nimport { array } from '@alexcarpenter/form/array'\n\nconst items = array($form, 'items')\nitems.push({ title: '' })\nitems.insert(0, { title: 'First' })\nitems.replace(0, { title: 'Updated' })\nitems.move(0, 1)\nitems.swap(0, 1)\nitems.remove(0)\n```\n\n### Select\n\nCreate a small derived store for a slice of form state. Useful for React when a\ncomponent should re-render only for one value.\n\n```js\nimport { select } from '@alexcarpenter/form/select'\n\nconst $canSubmit = select($form, form => form.canSubmit)\nconst $email = select($form, form => form.values.email)\n```\n\n### Debug\n\n```js\nimport { debug } from '@alexcarpenter/form/debug'\n\nlet stop = debug($form, { name: 'login' })\n```\n\n### React\n\nUse the normal Nano Stores React binding:\n\n```tsx\nimport { useStore } from '@nanostores/react'\nimport { select } from '@alexcarpenter/form/select'\nimport { $login } from './stores/login'\n\nconst $canSubmit = select($login, form => form.canSubmit)\n\nexport function LoginForm() {\n  let canSubmit = useStore($canSubmit)\n  let email = useStore($login.field('email').$state)\n\n  return <form onSubmit={$login.handleSubmit}>\n    <input\n      value={email.value}\n      onBlur={() => $login.field('email').handleBlur()}\n      onInput={event => $login.field('email').handleChange(event.currentTarget.value)}\n    />\n    {email.errors[0]}\n    <button disabled={!canSubmit}>Submit</button>\n  </form>\n}\n```\n","readmeFilename":"README.md"}