{"_id":"@einhasad-vue/vue-form","_rev":"2-fa5daaad08bfd82caac6df93af4c1970","name":"@einhasad-vue/vue-form","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@einhasad-vue/vue-form","version":"0.1.0","keywords":["vue","vue3","form","forms","validation","headless","composables","typescript","ant-design-vue"],"author":{"name":"Dima Popov"},"license":"MIT","_id":"@einhasad-vue/vue-form@0.1.0","maintainers":[{"name":"do-popov","email":"loss.of.loss@gmail.com"}],"homepage":"https://github.com/einhasad/vue-form#readme","bugs":{"url":"https://github.com/einhasad/vue-form/issues"},"dist":{"shasum":"33c018380ef1466c55b776a7da157de1c23a6385","tarball":"https://registry.npmjs.org/@einhasad-vue/vue-form/-/vue-form-0.1.0.tgz","fileCount":35,"integrity":"sha512-D6wTjJG5irlplKnufUAkcsjwneD4rYSbeImS6EaCJL31zDUeyUEKCwLqZR9dUe3dzouqNL9YkpZDcfPjhUBZAA==","signatures":[{"sig":"MEQCICa6wd/UzTU4+rmwu28P8U4krDwk2836f3uVkrd8eeq9AiB8ELjY6DHbGuQh/1X0fzuXMFRo/DWSXPqSWDgGTq/dng==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":49961},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"},"./headless":{"types":"./dist/headless/index.d.ts","import":"./dist/headless.mjs","require":"./dist/headless.cjs"},"./ant-design":{"types":"./dist/ant-design/index.d.ts","import":"./dist/ant-design.mjs","require":"./dist/ant-design.cjs"}},"gitHead":"da7a8789e1b40eee95ad669acdfd77a2f5be6844","scripts":{"test":"vitest run --passWithNoTests","build":"vite build","typecheck":"vue-tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"do-popov","email":"loss.of.loss@gmail.com"},"repository":{"url":"git+https://github.com/einhasad/vue-form.git","type":"git"},"workspaces":["examples"],"_npmVersion":"10.9.7","description":"Fully headless Vue 3 form library. Composables, headless classes, and validator factories — bring your own UI. Optional Ant Design Vue adapter at the ./ant-design subpath.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vue":"^3.4.0","vite":"^5.1.0","jsdom":"^24.0.0","vitest":"^1.2.0","vue-tsc":"^2.0.29","typescript":"^5.3.3","@vue/test-utils":"^2.4.4","vite-plugin-dts":"^3.7.2","@vitejs/plugin-vue":"^5.0.4","@vitest/coverage-v8":"^1.6.1"},"peerDependencies":{"vue":"^3.3","dayjs":"^1.11.0","ant-design-vue":"^4.0.0"},"peerDependenciesMeta":{"dayjs":{"optional":true},"ant-design-vue":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/vue-form_0.1.0_1777400789990_0.25649180966366103","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@einhasad-vue/vue-form","version":"0.2.0","description":"Fully headless Vue 3 form library. Composables, headless classes, and validator factories — bring your own UI. Optional Ant Design Vue adapter at the ./ant-design subpath.","author":{"name":"Dima Popov"},"license":"MIT","type":"module","main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.mjs","require":"./dist/index.cjs","types":"./dist/index.d.ts"},"./headless":{"import":"./dist/headless.mjs","require":"./dist/headless.cjs","types":"./dist/headless/index.d.ts"},"./ant-design":{"import":"./dist/ant-design.mjs","require":"./dist/ant-design.cjs","types":"./dist/ant-design/index.d.ts"}},"sideEffects":false,"keywords":["vue","vue3","form","forms","validation","headless","composables","typescript","ant-design-vue"],"repository":{"type":"git","url":"git+https://github.com/einhasad/vue-form.git"},"bugs":{"url":"https://github.com/einhasad/vue-form/issues"},"homepage":"https://github.com/einhasad/vue-form#readme","publishConfig":{"access":"public"},"engines":{"node":">=18.0.0"},"scripts":{"build":"vite build","typecheck":"vue-tsc --noEmit","test":"vitest run --passWithNoTests","test:watch":"vitest","prepublishOnly":"npm run typecheck && npm test && npm run build"},"peerDependencies":{"ant-design-vue":"^4.0.0","dayjs":"^1.11.0","vue":"^3.3"},"peerDependenciesMeta":{"ant-design-vue":{"optional":true},"dayjs":{"optional":true}},"devDependencies":{"@faker-js/faker":"^10.4.0","@vitejs/plugin-vue":"^5.0.4","@vitest/coverage-v8":"^1.6.1","@vue/test-utils":"^2.4.4","jsdom":"^24.0.0","typescript":"^5.3.3","vite":"^5.1.0","vite-plugin-dts":"^3.7.2","vitest":"^1.2.0","vue":"^3.4.0","vue-tsc":"^2.0.29"},"workspaces":["examples"],"_id":"@einhasad-vue/vue-form@0.2.0","gitHead":"949b5246580e1cec493588479906f4b22f6f4a05","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-+R/0M+5q2HU6g3xPsBDK32ttVW9Hm8Oh/yDzzaOFpiOd6/arOX1scnO3BCDTvhhdbfWz7mdd91BQ7ri2wT9QZQ==","shasum":"715bc3d4b6ba70e3cfc0aa9d3590fe0627aab3d4","tarball":"https://registry.npmjs.org/@einhasad-vue/vue-form/-/vue-form-0.2.0.tgz","fileCount":35,"unpackedSize":57078,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHHLKQj7F797aukejeqwTxqbUIs02/TEllWvp+DjtULIAiBBJ58SZQKaGFDOq8LQZXTF5tmOGFTiyntgUa5Oh7lnKg=="}]},"_npmUser":{"name":"do-popov","email":"loss.of.loss@gmail.com"},"directories":{},"maintainers":[{"name":"do-popov","email":"loss.of.loss@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/vue-form_0.2.0_1777404085230_0.1760541757388452"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-28T18:26:29.840Z","modified":"2026-04-28T19:21:25.530Z","0.1.0":"2026-04-28T18:26:30.139Z","0.2.0":"2026-04-28T19:21:25.375Z"},"bugs":{"url":"https://github.com/einhasad/vue-form/issues"},"author":{"name":"Dima Popov"},"license":"MIT","homepage":"https://github.com/einhasad/vue-form#readme","keywords":["vue","vue3","form","forms","validation","headless","composables","typescript","ant-design-vue"],"repository":{"type":"git","url":"git+https://github.com/einhasad/vue-form.git"},"description":"Fully headless Vue 3 form library. Composables, headless classes, and validator factories — bring your own UI. Optional Ant Design Vue adapter at the ./ant-design subpath.","maintainers":[{"name":"do-popov","email":"loss.of.loss@gmail.com"}],"readme":"# @einhasad-vue/vue-form\n\nA fully headless Vue 3 form library: form state, field state, validation, and a wildcard-pattern API for array-field rules. The main entry ships **no components, no widgets, no styling.** Optional adapter at `@einhasad-vue/vue-form/ant-design` if you're on Ant Design Vue and want the boilerplate already written.\n\n- **Headless core.** `Form` and `Field` are pure TypeScript classes. A thin Vue layer (`useProvideForm`, `useForm`, `useField`) binds them to reactivity. No DOM, no styles, no components.\n- **UI-library independent.** Bring your own UI kit. The optional `./ant-design` subpath is a thin opt-in adapter; everything else stays vanilla.\n- **Transport-agnostic.** Throws what your API throws; the library never inspects rejection shapes. You translate failures into per-field or form-level errors.\n- **Multiple errors per field.** Validation collects every failing rule, not stop-at-first.\n- **Validators are plain functions.** Sync or async, return `FieldError | undefined`. No rule-object indirection.\n- **Server-driven schemas.** External callers can install rules at runtime, including wildcard patterns for array fields with `addFieldValidatorsByPattern(\"parts.*.sku\", …)`.\n- **All strings overridable.** One call to `setStrings(partial)` retargets every built-in error message.\n\n## Install\n\n```sh\nnpm install @einhasad-vue/vue-form\n```\n\nPeer-deps: `vue ^3.3`.\n\n## Mental model\n\nThe library has **no `<Form>` component**. You create the form context yourself with `useProvideForm()`, then build the rest of the form however you want — `<a-form>`, plain `<form>`, multi-step wizard, anything.\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useProvideForm, rules } from '@einhasad-vue/vue-form'\nimport TextInput from './widgets/TextInput.vue'  // your widget\n\n// 1. Provide the form context to descendants. useField calls in child\n//    components find this via inject.\nconst formCtx = useProvideForm()\nconst { formErrors, loading } = formCtx\n\nconst data = ref({ name: '', email: '' })\n\nasync function send(payload: typeof data.value) {\n  const r = await fetch('/api/users', { method: 'POST', body: JSON.stringify(payload) })\n  if (!r.ok) throw { response: { status: r.status, data: await r.json() } }\n  return r.json()\n}\n\n// 2. You own the submit flow. Validate, send, translate errors.\nasync function onSubmit() {\n  formCtx.clearErrors()\n  if (!formCtx.validateAll()) return\n  if (!(await formCtx.validateAllAsync())) return\n  loading.value = true\n  try {\n    const result = await send(data.value)\n    // ... handle success\n  } catch (raw) {\n    const r = raw as { response?: { status?: number; data?: { result?: { field: string; message: string }[] } } }\n    if (r?.response?.status === 422 && Array.isArray(r.response.data?.result)) {\n      for (const e of r.response.data.result) {\n        formCtx.setFieldErrors(e.field, [{ message: e.message }])\n      }\n    } else {\n      formCtx.setFormErrors([{ message: 'Submit failed' }])\n    }\n  } finally {\n    loading.value = false\n  }\n}\n</script>\n\n<template>\n  <form @submit.prevent=\"onSubmit\">\n    <TextInput\n      v-model=\"data.name\"\n      attribute=\"name\"\n      :validators=\"[rules.required('Name'), rules.minLen(3, 'Name')]\"\n    />\n    <TextInput\n      v-model=\"data.email\"\n      attribute=\"email\"\n      :validators=\"[rules.required('Email'), rules.email('Email')]\"\n    />\n\n    <ul v-if=\"formErrors.length\">\n      <li v-for=\"(e, i) in formErrors\" :key=\"i\">{{ e.message }}</li>\n    </ul>\n\n    <button type=\"submit\" :disabled=\"loading\">Save</button>\n  </form>\n</template>\n```\n\nA widget is anything that calls `useField`:\n\n```vue\n<!-- TextInput.vue (your code, not shipped) -->\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useField, type Validator } from '@einhasad-vue/vue-form'\n\nconst props = defineProps<{\n  modelValue: string\n  attribute: string\n  validators?: Validator[]\n}>()\nconst emit = defineEmits<{ 'update:modelValue': [string] }>()\n\nconst value = computed<string>({\n  get: () => props.modelValue ?? '',\n  set: (v) => emit('update:modelValue', v),\n})\nconst f = useField({ attribute: props.attribute, modelValue: value, validators: props.validators })\n</script>\n\n<template>\n  <input v-model=\"value\" @blur=\"f.onBlur\" />\n  <span v-if=\"f.firstError.value\">{{ f.firstError.value.message }}</span>\n</template>\n```\n\n## API\n\n### Composables\n\n| Composable | Returns |\n| --- | --- |\n| `useProvideForm()` | A `FormContext`. Call once at the form root — `useField` finds it via `inject`. |\n| `useForm()` | The `FormContext` from the nearest `useProvideForm` ancestor. Throws if missing. |\n| `useField({ attribute, modelValue, validators? })` | `{ errors, isInvalid, firstError, validate, validateAsync, onBlur }`. Registers on mount, unregisters on unmount. |\n\n### `FormContext`\n\n| Member | Purpose |\n| --- | --- |\n| `validateAll()` / `validateAllAsync()` | Run sync / async validators across every registered field. Return `true` when no errors. |\n| `clearErrors()` | Clears every field's errors and the form-level errors list. |\n| `setFieldErrors(attribute, errors)` | Attach error messages to a specific attribute (incl. dotted paths like `parts.0.sku`). |\n| `setFormErrors(errors)` | Top-level form errors. Render them however you want. |\n| `setFieldValidators(attribute, validators)` | Replace a registered field's validators. |\n| `addFieldValidator(attribute, validator)` | Append one validator. |\n| `addFieldValidatorsByPattern(pattern, validators)` | Layer validators onto every field whose attribute matches a dotted wildcard (e.g. `parts.*.sku`). Applies to fields registered now AND any registered later. |\n| `getField(attribute)` | The raw `FieldHandle`. |\n| `formErrors: Ref<FieldError[]>` | Reactive form-level errors. |\n| `loading: Ref<boolean>` | Form-level loading flag. The library does not toggle this — your submit flow does. |\n| `fields: Ref<FieldHandle[]>` | Reactive list of registered fields. |\n\n### Validator factories (`rules`)\n\n```ts\nimport { rules } from '@einhasad-vue/vue-form'\n```\n\n`rules.required(attr?, override?)`, `rules.minLen(n, …)`, `rules.maxLen`, `rules.minNum`, `rules.maxNum`, `rules.pattern(re, …)`, `rules.email`, `rules.uniqueIn(siblings, currentIndex, key, override?)`.\n\nAll validators are plain functions:\n\n```ts\ntype Validator = (value: unknown) => FieldError | undefined | Promise<FieldError | undefined>\n```\n\nRoll your own:\n\n```ts\nconst vinUnique: Validator = async (v) => {\n  if (!v) return undefined\n  const taken = await fetch(`/api/vins/${v}`).then((r) => r.json())\n  return taken ? { message: 'VIN already registered', key: 'vinUnique' } : undefined\n}\nformCtx.addFieldValidator('vin', vinUnique)\n```\n\n### Strings / i18n\n\n```ts\nimport { setStrings } from '@einhasad-vue/vue-form'\nsetStrings({\n  required: '{attr} is required',\n  minLen: '{attr} must be at least {min} characters',\n})\n```\n\n`Strings = typeof en` — overrides are type-checked against the canonical shape.\n\n### Headless subpath\n\nFor pure-TS use (testing, server-side, non-Vue contexts):\n\n```ts\nimport { Form, Field, rules, setStrings } from '@einhasad-vue/vue-form/headless'\n```\n\nNo Vue, no DOM. Same `Form` / `Field` classes the Vue layer composes.\n\n### Ant Design Vue adapter\n\nOpt-in widget pack that wraps `<a-input>`, `<a-select>`, `<a-date-picker>`, etc. Each widget calls `useField` internally and renders an `<a-form-item>` for label + error layout — same component contract as a hand-rolled widget. **Not included unless you import from this subpath**, so the core lib stays free of antd code.\n\n```sh\nnpm install ant-design-vue dayjs   # peer-deps for the adapter\n```\n\n```ts\n// main.ts — register Antd globally so the adapter's templates resolve a-* tags.\nimport Antd from 'ant-design-vue'\nimport 'ant-design-vue/dist/reset.css'\nimport { createApp } from 'vue'\n\ncreateApp(App).use(Antd).mount('#app')\n```\n\n```ts\n// Form.vue — import widgets from the subpath.\nimport {\n  TextInput, NumberInput, TextareaInput, SelectInput,\n  CheckboxInput, CheckboxGroup, RadioGroup, SwitchInput,\n  CurrencyInput, PhoneInput,\n  DatePicker, DateRangePicker, TimePicker,\n  SearchSelect,\n} from '@einhasad-vue/vue-form/ant-design'\n```\n\nEach widget exposes the same prop surface: `v-model`, `attribute`, `label?`, `required?`, `disabled?`, `validators?`, plus widget-specific props (`items`, `searchCallback`, `step`, etc.). Use them inside `useProvideForm()` exactly like any other `useField`-based widget.\n\nThe adapter bundles to ~3 kB gzipped. `ant-design-vue` and `dayjs` are externalized — they're declared as **optional peer dependencies**, so the core install doesn't drag them in.\n\n## Server-driven validation schemas\n\nA common pattern: the backend is the source of truth for validation rules. Fetch a schema on mount and install validators — including ones for array items that don't exist yet.\n\n```ts\nonMounted(async () => {\n  const schema = await fetch('/api/validation/schema/vehicle').then((r) => r.json())\n  // {\n  //   \"name\":          [{ \"kind\": \"required\" }, { \"kind\": \"minLen\", \"n\": 3 }],\n  //   \"parts.*.sku\":   [{ \"kind\": \"required\" }, { \"kind\": \"maxLen\", \"n\": 40 }],\n  //   \"parts.*.qty\":   [{ \"kind\": \"required\" }, { \"kind\": \"minNum\", \"n\": 1 }],\n  // }\n  for (const [key, ruleList] of Object.entries(schema)) {\n    const validators = ruleList.map(buildValidator)  // your rule-kind → Validator mapper\n    if (key.includes('*')) formCtx.addFieldValidatorsByPattern(key, validators)\n    else                   formCtx.setFieldValidators(key, validators)\n  }\n})\n```\n\n`addFieldValidatorsByPattern` is **additive** (layers on top of inline `:validators`) and **forward-applying** — when the user clicks \"Add row\" and a new field registers with attribute `parts.7.sku`, the matching pattern's validators are applied automatically.\n\n`*` matches one dotted segment: `parts.*.sku` matches `parts.0.sku` but not `parts.0.attrs.sku`.\n\n## Array fields\n\nThere is no `<NestedForm>`. Render `v-for` with a dotted attribute path and you have it:\n\n```vue\n<button type=\"button\" @click=\"rows.push({ sku: '', qty: 1 })\">Add row</button>\n<table>\n  <thead><tr><th>SKU</th><th>Qty</th><th /></tr></thead>\n  <tbody>\n    <tr v-for=\"(row, i) in rows\" :key=\"i\">\n      <td>\n        <TextInput\n          v-model=\"row.sku\"\n          :attribute=\"`parts.${i}.sku`\"\n          :validators=\"[rules.uniqueIn(rows, i, 'sku')]\"\n        />\n      </td>\n      <td><NumberInput v-model=\"row.qty\" :attribute=\"`parts.${i}.qty`\" /></td>\n      <td><button type=\"button\" @click=\"rows.splice(i, 1)\">Delete</button></td>\n    </tr>\n  </tbody>\n</table>\n```\n\nPair with `addFieldValidatorsByPattern(\"parts.*.sku\", …)` and the schema-driven rules apply to every row, present and future. Server 422 errors with dotted field paths (`parts.0.sku`) attach to the right row out of the box via `setFieldErrors`.\n\n## Examples\n\n`examples/` is a runnable workspace with two demos that both consume `@einhasad-vue/vue-form/ant-design`:\n\n- **Ant Design (in-process mock)** — synthetic `services.ts`, no network.\n- **Ant Design (MSW server-driven)** — real `fetch()` calls intercepted by [MSW v2](https://mswjs.io/), schema fetched from `GET /api/validation/schema/vehicle`, 422 envelopes decoded into per-field errors (incl. nested `parts.0.sku`).\n\nThe MSW demo is the closest to a production wire-up. Both views render an event log below the form so you can see the lifecycle (`request` / `response` / `field-error` / `validator` / …) as you interact.\n\n```sh\nnpm install\nnpm run dev --workspace=examples   # http://localhost:5173\n```\n\nBoth demo views show the full submit-orchestration pattern:\n- `useProvideForm()` at the form root\n- Validate sync → validate async → `send()` → translate 422 envelopes via `setFieldErrors` (incl. nested paths)\n- Server-driven schema fetched on mount, with wildcard patterns for array rules\n\n## Development\n\n```sh\nnpm install\nnpm run dev --workspace=examples   # boots the demo at http://localhost:5173\nnpm test                           # 63 tests covering headless classes, rules, Vue bridge\nnpm run typecheck                  # vue-tsc, root + examples\nnpm run build                      # ESM + CJS bundles to dist/, with .d.ts via vite-plugin-dts\n```\n\n## License\n\nMIT.\n","readmeFilename":"README.md"}