attaform 0.26.0 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -11
- package/bin/attaform.mjs +125 -0
- package/dist/abstract.cjs +47 -0
- package/dist/abstract.cjs.map +1 -0
- package/dist/abstract.d.cts +42 -0
- package/dist/abstract.d.mts +42 -0
- package/dist/abstract.d.ts +42 -0
- package/dist/abstract.mjs +4 -0
- package/dist/abstract.mjs.map +1 -0
- package/dist/chunks/devtools.cjs +2 -2
- package/dist/chunks/devtools.cjs.map +1 -1
- package/dist/chunks/devtools.mjs +1 -1
- package/dist/chunks/fingerprint2.cjs +1 -1
- package/dist/chunks/fingerprint2.mjs +1 -1
- package/dist/esbuild.cjs +2 -2
- package/dist/esbuild.cjs.map +1 -1
- package/dist/esbuild.d.cts +3 -3
- package/dist/esbuild.d.mts +3 -3
- package/dist/esbuild.d.ts +3 -3
- package/dist/esbuild.mjs +2 -2
- package/dist/esbuild.mjs.map +1 -1
- package/dist/index.cjs +41 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +194 -282
- package/dist/index.d.mts +194 -282
- package/dist/index.d.ts +194 -282
- package/dist/index.mjs +4 -25
- package/dist/index.mjs.map +1 -1
- package/dist/nuxt.cjs +3 -1
- package/dist/nuxt.cjs.map +1 -1
- package/dist/nuxt.d.cts +17 -4
- package/dist/nuxt.d.mts +17 -4
- package/dist/nuxt.d.ts +17 -4
- package/dist/nuxt.mjs +4 -2
- package/dist/nuxt.mjs.map +1 -1
- package/dist/rollup.cjs +2 -2
- package/dist/rollup.cjs.map +1 -1
- package/dist/rollup.d.cts +3 -3
- package/dist/rollup.d.mts +3 -3
- package/dist/rollup.d.ts +3 -3
- package/dist/rollup.mjs +2 -2
- package/dist/rollup.mjs.map +1 -1
- package/dist/rspack.cjs +1 -1
- package/dist/rspack.d.cts +1 -1
- package/dist/rspack.d.mts +1 -1
- package/dist/rspack.d.ts +1 -1
- package/dist/rspack.mjs +1 -1
- package/dist/runtime/plugins/attaform.cjs +4 -5
- package/dist/runtime/plugins/attaform.cjs.map +1 -1
- package/dist/runtime/plugins/attaform.mjs +1 -2
- package/dist/runtime/plugins/attaform.mjs.map +1 -1
- package/dist/shared/{attaform.BRzKBj2g.cjs → attaform.8tFZu1Ub.cjs} +2 -2
- package/dist/shared/{attaform.BRzKBj2g.cjs.map → attaform.8tFZu1Ub.cjs.map} +1 -1
- package/dist/shared/{attaform.k9V0py0s.mjs → attaform.BERt5YMU.mjs} +2 -2
- package/dist/shared/{attaform.k9V0py0s.mjs.map → attaform.BERt5YMU.mjs.map} +1 -1
- package/dist/shared/{attaform.DSOIz08n.mjs → attaform.BceJrhQ7.mjs} +5 -5
- package/dist/shared/{attaform.DSOIz08n.mjs.map → attaform.BceJrhQ7.mjs.map} +1 -1
- package/dist/shared/{attaform.DBNjHgNj.d.cts → attaform.C3WnCDXz.d.cts} +11 -9
- package/dist/shared/{attaform.DBNjHgNj.d.mts → attaform.C3WnCDXz.d.mts} +11 -9
- package/dist/shared/{attaform.DBNjHgNj.d.ts → attaform.C3WnCDXz.d.ts} +11 -9
- package/dist/shared/{attaform.DuzQYscR.d.mts → attaform.C9fPTxjB.d.cts} +5 -5
- package/dist/shared/{attaform.DuzQYscR.d.ts → attaform.C9fPTxjB.d.mts} +5 -5
- package/dist/shared/{attaform.DuzQYscR.d.cts → attaform.C9fPTxjB.d.ts} +5 -5
- package/dist/shared/{attaform.Be5otUVw.d.ts → attaform.CC_y1_Ej.d.ts} +33 -33
- package/dist/shared/{attaform.ceYeVkjn.d.ts → attaform.CEXUlUkc.d.ts} +1579 -1192
- package/dist/shared/{attaform.tZ0YKisf.cjs → attaform.C__F01CE.cjs} +15 -15
- package/dist/shared/{attaform.tZ0YKisf.cjs.map → attaform.C__F01CE.cjs.map} +1 -1
- package/dist/shared/{attaform.BPl-ECRT.cjs → attaform.Co5LNRnv.cjs} +162 -129
- package/dist/shared/attaform.Co5LNRnv.cjs.map +1 -0
- package/dist/shared/{attaform.B71rfNL5.d.mts → attaform.CpXHK60W.d.cts} +33 -33
- package/dist/shared/{attaform.BFfY7HRI.d.cts → attaform.CvVYOvZF.d.mts} +33 -33
- package/dist/shared/{attaform.BoYqnIPI.mjs → attaform.CyQxTOdX.mjs} +55 -36
- package/dist/shared/attaform.CyQxTOdX.mjs.map +1 -0
- package/dist/shared/{attaform.CEY_WLRJ.d.mts → attaform.D99pd7JG.d.cts} +1579 -1192
- package/dist/shared/{attaform.CXVmZtFq.mjs → attaform.DDAmSFnl.mjs} +4 -4
- package/dist/shared/{attaform.CXVmZtFq.mjs.map → attaform.DDAmSFnl.mjs.map} +1 -1
- package/dist/shared/{attaform.Du9RZp8m.cjs → attaform.DQT7_-FO.cjs} +272 -252
- package/dist/shared/attaform.DQT7_-FO.cjs.map +1 -0
- package/dist/shared/{attaform.DGhIix5D.mjs → attaform.Da8EdI2t.mjs} +160 -129
- package/dist/shared/attaform.Da8EdI2t.mjs.map +1 -0
- package/dist/shared/{attaform.Cs0UZ7q6.cjs → attaform.DhVgG2jh.cjs} +16 -16
- package/dist/shared/{attaform.Cs0UZ7q6.cjs.map → attaform.DhVgG2jh.cjs.map} +1 -1
- package/dist/shared/{attaform.sHkHv_98.mjs → attaform.DhnPb6nK.mjs} +6 -3
- package/dist/shared/attaform.DhnPb6nK.mjs.map +1 -0
- package/dist/shared/attaform.DkD0DR7R.mjs +42 -0
- package/dist/shared/attaform.DkD0DR7R.mjs.map +1 -0
- package/dist/shared/{attaform.CSXDW2tO.mjs → attaform.DlSu08F5.mjs} +4 -4
- package/dist/shared/{attaform.CSXDW2tO.mjs.map → attaform.DlSu08F5.mjs.map} +1 -1
- package/dist/shared/{attaform.LEWUFqUw.cjs → attaform.DoQm_Vkd.cjs} +7 -3
- package/dist/shared/attaform.DoQm_Vkd.cjs.map +1 -0
- package/dist/shared/{attaform.BQflyVOL.cjs → attaform.Drsi43GG.cjs} +19 -19
- package/dist/shared/{attaform.BQflyVOL.cjs.map → attaform.Drsi43GG.cjs.map} +1 -1
- package/dist/shared/attaform.K6Thc6By.cjs +46 -0
- package/dist/shared/attaform.K6Thc6By.cjs.map +1 -0
- package/dist/shared/{attaform.Vp2-CWZB.mjs → attaform.KvlEedlF.mjs} +3 -3
- package/dist/shared/{attaform.Vp2-CWZB.mjs.map → attaform.KvlEedlF.mjs.map} +1 -1
- package/dist/shared/{attaform.BRGIpZo4.cjs → attaform.RaTzp1WF.cjs} +3 -3
- package/dist/shared/attaform.RaTzp1WF.cjs.map +1 -0
- package/dist/shared/{attaform.MgaYKlPP.d.cts → attaform.kBkYlKP_.d.mts} +1579 -1192
- package/dist/shared/{attaform.C3Doa9Pt.mjs → attaform.v0fQtWyA.mjs} +3 -3
- package/dist/shared/attaform.v0fQtWyA.mjs.map +1 -0
- package/dist/shared/{attaform.ZXZ6dH72.cjs → attaform.yUrz7C2z.cjs} +3 -3
- package/dist/shared/{attaform.ZXZ6dH72.cjs.map → attaform.yUrz7C2z.cjs.map} +1 -1
- package/dist/transforms.cjs +1 -1
- package/dist/transforms.mjs +1 -1
- package/dist/vite.cjs +21 -3
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.cts +70 -17
- package/dist/vite.d.mts +70 -17
- package/dist/vite.d.ts +70 -17
- package/dist/vite.mjs +20 -4
- package/dist/vite.mjs.map +1 -1
- package/dist/webpack.cjs +1 -1
- package/dist/webpack.d.cts +1 -1
- package/dist/webpack.d.mts +1 -1
- package/dist/webpack.d.ts +1 -1
- package/dist/webpack.mjs +1 -1
- package/dist/zod-v3.cjs +41 -10
- package/dist/zod-v3.cjs.map +1 -1
- package/dist/zod-v3.d.cts +5 -5
- package/dist/zod-v3.d.mts +5 -5
- package/dist/zod-v3.d.ts +5 -5
- package/dist/zod-v3.mjs +4 -2
- package/dist/zod-v3.mjs.map +1 -1
- package/dist/zod-v4.cjs +41 -10
- package/dist/zod-v4.cjs.map +1 -1
- package/dist/zod-v4.d.cts +7 -7
- package/dist/zod-v4.d.mts +7 -7
- package/dist/zod-v4.d.ts +7 -7
- package/dist/zod-v4.mjs +4 -2
- package/dist/zod-v4.mjs.map +1 -1
- package/dist/zod.cjs +44 -49
- package/dist/zod.cjs.map +1 -1
- package/dist/zod.d.cts +6 -248
- package/dist/zod.d.mts +6 -248
- package/dist/zod.d.ts +6 -248
- package/dist/zod.mjs +4 -42
- package/dist/zod.mjs.map +1 -1
- package/package.json +12 -2
- package/skills/attaform/SKILL.md +128 -0
- package/skills/attaform/references/custom-components.md +72 -0
- package/skills/attaform/references/errors.md +62 -0
- package/skills/attaform/references/ssr.md +26 -0
- package/skills/attaform/references/validation.md +40 -0
- package/skills/attaform/references/wizards.md +104 -0
- package/dist/shared/attaform.55hFR_cl.d.ts +0 -97
- package/dist/shared/attaform.BPl-ECRT.cjs.map +0 -1
- package/dist/shared/attaform.BRGIpZo4.cjs.map +0 -1
- package/dist/shared/attaform.BoYqnIPI.mjs.map +0 -1
- package/dist/shared/attaform.C3Doa9Pt.mjs.map +0 -1
- package/dist/shared/attaform.D51IwDtP.cjs +0 -39
- package/dist/shared/attaform.D51IwDtP.cjs.map +0 -1
- package/dist/shared/attaform.DGhIix5D.mjs.map +0 -1
- package/dist/shared/attaform.DJRz_bzM.d.cts +0 -97
- package/dist/shared/attaform.Du9RZp8m.cjs.map +0 -1
- package/dist/shared/attaform.JPsaqjLl.d.mts +0 -97
- package/dist/shared/attaform.LEWUFqUw.cjs.map +0 -1
- package/dist/shared/attaform.NqYEzRcM.mjs +0 -35
- package/dist/shared/attaform.NqYEzRcM.mjs.map +0 -1
- package/dist/shared/attaform.sHkHv_98.mjs.map +0 -1
- package/dist/shared/{attaform.DdjDqTah.d.ts → attaform.DRUT703x.d.cts} +14 -14
- package/dist/shared/{attaform.DdjDqTah.d.cts → attaform.DRUT703x.d.mts} +14 -14
- package/dist/shared/{attaform.DdjDqTah.d.mts → attaform.DRUT703x.d.ts} +14 -14
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: attaform
|
|
3
|
+
description: Build type-safe, schema-driven forms in Vue 3 and Nuxt with Attaform (first-class Zod). Use when creating, editing, or debugging a form (inputs, validation, submission, multistep wizards, or SSR) in a project that has the `attaform` package installed. Covers the correct import surface, the useForm handle, the v-register directive, reading validation state, handleSubmit, server-error routing, and wizards.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Building forms with Attaform
|
|
8
|
+
|
|
9
|
+
Attaform turns a Zod schema into a reactive, typed Vue form: one schema declares the shape, the defaults, the validation, and the per-field errors, and Attaform builds the reactive surface around it. Use this skill when writing or editing a form in a project that depends on `attaform`.
|
|
10
|
+
|
|
11
|
+
This file covers the common case end to end. For depth on one area, load the matching file in `references/` (indexed at the bottom).
|
|
12
|
+
|
|
13
|
+
## Imports
|
|
14
|
+
|
|
15
|
+
Everything comes from the `attaform` barrel:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
useForm,
|
|
20
|
+
useWizard,
|
|
21
|
+
injectForm,
|
|
22
|
+
injectWizard,
|
|
23
|
+
useRegister,
|
|
24
|
+
fieldMeta,
|
|
25
|
+
withMeta,
|
|
26
|
+
lazy,
|
|
27
|
+
} from 'attaform'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **Do not import `useForm` from `attaform/abstract`.** That entry is the bring-your-own-adapter escape hatch: it exports `useAbstractForm`, which needs a schema adapter wired by hand. A Zod project wants `useForm` from `attaform`. This mismatch is the single most common first-try mistake.
|
|
31
|
+
- Types come from the same barrel: `import type { AnyForm, WizardCtx, Json, ValidationError } from 'attaform'`.
|
|
32
|
+
- `attaform/zod` is the same surface named explicitly; `attaform/zod-v3` and `attaform/zod-v4` pin a Zod major. Any of these works; default to `attaform`.
|
|
33
|
+
- In **Nuxt** with the module installed (`attaform/nuxt`), this surface auto-imports and the `v-register` directive is registered globally, so a component needs no import lines at all. In **plain Vite**, add the Attaform plugin from `attaform/vite` plus the auto-import preset it exports.
|
|
34
|
+
|
|
35
|
+
## Build a form
|
|
36
|
+
|
|
37
|
+
Three moves: hoist the schema, hand it to `useForm`, bind each input with `v-register`. Submit through `form.handleSubmit`.
|
|
38
|
+
|
|
39
|
+
```vue
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
import { useForm } from 'attaform'
|
|
42
|
+
import { z } from 'zod'
|
|
43
|
+
|
|
44
|
+
const schema = z.object({
|
|
45
|
+
email: z.email('Enter a valid email'),
|
|
46
|
+
password: z.string().min(8, 'At least 8 characters'),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const form = useForm({ schema, key: 'sign-in' })
|
|
50
|
+
|
|
51
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
52
|
+
await fetch('/api/sign-in', { method: 'POST', body: JSON.stringify(values) })
|
|
53
|
+
})
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<form @submit="onSubmit">
|
|
58
|
+
<label>
|
|
59
|
+
Email
|
|
60
|
+
<input v-register="form.register('email')" autocomplete="email" />
|
|
61
|
+
<em v-if="form.fields.email.showErrors">{{ form.fields.email.firstError?.message }}</em>
|
|
62
|
+
</label>
|
|
63
|
+
<label>
|
|
64
|
+
Password
|
|
65
|
+
<input v-register="form.register('password')" type="password" autocomplete="off" />
|
|
66
|
+
<em v-if="form.fields.password.showErrors">{{ form.fields.password.firstError?.message }}</em>
|
|
67
|
+
</label>
|
|
68
|
+
<button :disabled="form.meta.submitting" type="submit">Sign in</button>
|
|
69
|
+
</form>
|
|
70
|
+
</template>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Prefer an explicit `key` (`useForm({ schema, key: 'sign-in' })`): it makes the form reachable through `injectForm('sign-in')`, survives parent refactors, and reads clearly at the call site. Anonymous `useForm({ schema })` is the fallback for a single throwaway form.
|
|
74
|
+
|
|
75
|
+
## Rules
|
|
76
|
+
|
|
77
|
+
- **Use the form handle. Don't destructure.** Keep `const form = useForm(...)` and reach for `form.register(...)`, `form.setValue(...)`, `form.meta`. Destructuring loses the reactive bindings.
|
|
78
|
+
- **Bind with `v-register`; never write through `form.values`.** `form.values` is a read view and a callable proxy (`typeof form.values === 'function'`), so `schema.safeParse(form.values)` compiles but fails at runtime. Read `form.meta.valid` for validity; call `form.values()` for a plain snapshot (`form.values('a.b')` for a subtree).
|
|
79
|
+
- **Let `handleSubmit` do the work.** It validates, calls `onSubmit` with the parsed values, and focuses the first offending field on an invalid submit. No manual focus and no `novalidate` ceremony. Bind it with `@submit`, not `@submit.prevent`: the handler calls `event.preventDefault()` for you, so `.prevent` only prevents twice.
|
|
80
|
+
- **Never disable the submit button on validity.** Let the user click; the failed submit focuses the first error and reveals it. Gate only on `form.meta.submitting` or `!form.meta.dirty`. If a field's only guard was the disabled button, move the requirement into the schema (`.min(1)`, `.refine(...)`). See `references/validation.md`.
|
|
81
|
+
- **Read errors through `field.showErrors` and `field.firstError?.message`.** The visibility heuristic lives in `showErrors`; reaching into `field.errors[0]` bypasses it. The async "checking" indicator is `field.showPending`.
|
|
82
|
+
- **Route server errors through `form.setErrors(...)` inside the callback.** `setErrors` replaces the whole user-error layer, so call `form.clearErrors()` at the top of the submit for a fresh attempt. See `references/errors.md`.
|
|
83
|
+
- **Banners read `form.meta.firstOwnError`,** the form's own error bucket, not `form.errors([])` (the whole-form aggregate). See `references/errors.md`.
|
|
84
|
+
- **Accessibility is automatic.** `v-register` keeps `aria-invalid`, `aria-busy`, `aria-required`, and `aria-describedby` in sync and emits them during SSR. Put `form.fields.<path>.aria.errorId` on the error element so `aria-describedby` resolves. Author any ARIA attribute yourself to take it over, or pass `autoAria: false` to opt out.
|
|
85
|
+
- **Reach with `?.` on injected forms.** `injectForm()` and `injectWizard()` return `T | null`; chain optional access (`form?.register('email')`) at every consumption site.
|
|
86
|
+
- **Put labels on the schema, not the template.** `z.string().register(fieldMeta, { label: 'Email' })`; read it back through `form.fields.email.label` (resolved, with a humanized-path fallback).
|
|
87
|
+
- **Native inputs first.** Bind `<input>`, `<select>`, `<textarea>` with `v-register`. Reach for `useRegister` only inside a custom input component. See `references/custom-components.md`.
|
|
88
|
+
- **`v-register` alone does binding, SSR value injection, and ARIA.** Do not stack `@change` handlers, reset-signal props, or watchers on top of it. If a control seems to need that scaffolding, the idiomatic shape is being missed.
|
|
89
|
+
|
|
90
|
+
## Wizards
|
|
91
|
+
|
|
92
|
+
Compose multiple `useForm` instances under `useWizard`. Navigation and submission are separate verbs: `wizard.tryNext()` is the gated Next (validates the active step, advances on a clean pass), and `wizard.handleSubmit(onSubmit)` validates every step from any position and calls `onSubmit` once with all values, but **never advances**.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { useForm, useWizard } from 'attaform'
|
|
96
|
+
|
|
97
|
+
const account = useForm({ schema: accountSchema, key: 'account' })
|
|
98
|
+
const profile = useForm({ schema: profileSchema, key: 'profile' })
|
|
99
|
+
|
|
100
|
+
const wizard = useWizard({ steps: [account, 'review', profile] })
|
|
101
|
+
|
|
102
|
+
const onComplete = wizard.handleSubmit(async (ctx) => {
|
|
103
|
+
await fetch('/onboarding', { method: 'POST', body: JSON.stringify(ctx.values) })
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A step is a `useForm` reference, a bare string (an informational step with no schema), `null` / `undefined` (filtered out), or a function returning one of those. Full patterns, the declarative step registry, and the keyed-injection gotchas live in `references/wizards.md`.
|
|
108
|
+
|
|
109
|
+
## SSR
|
|
110
|
+
|
|
111
|
+
SSR is automatic under the Nuxt module or the Vite plugin: field values, `checked`, and `selected` are injected into the first paint and ARIA is emitted, with no extra wiring. Confirm any SSR behavior through the real build (curl the rendered HTML), not a bare unit render. The value injection is a build-time compiler transform that a plain Vitest render does not run. See `references/ssr.md`.
|
|
112
|
+
|
|
113
|
+
## Reference files
|
|
114
|
+
|
|
115
|
+
Load one when the task reaches its area:
|
|
116
|
+
|
|
117
|
+
- **`references/wizards.md`**: multistep flows. The declarative step registry, `tryNext` vs `handleSubmit`, using a wizard as a flat multi-form coordinator, and how keyed `injectForm` / `injectWizard` resolve.
|
|
118
|
+
- **`references/errors.md`**: routing server and API errors. The `ValidationError` envelope, `setErrors` / `clearErrors`, the own-vs-subtree error axis, banners, and the one-normalizer pattern.
|
|
119
|
+
- **`references/custom-components.md`**: wrapping an input or binding a third-party component. `useRegister`, the three orthogonal primitives, attribute fallthrough, and the optional `form-key` prop.
|
|
120
|
+
- **`references/ssr.md`**: debugging SSR and hydration. Why the value injection is a build-time transform, how to confirm it faithfully, and the test traps to avoid.
|
|
121
|
+
- **`references/validation.md`**: designing the schema. Client-is-UX / server-is-truth, keeping closed sets in sync, and why a clearable edit field is a required string.
|
|
122
|
+
|
|
123
|
+
## Going deeper
|
|
124
|
+
|
|
125
|
+
- `llms.txt` (curated index): https://attaform.dev/llms.txt
|
|
126
|
+
- `llms-full.txt` (every doc, concatenated): https://attaform.dev/llms-full.txt
|
|
127
|
+
- Docs: https://attaform.dev
|
|
128
|
+
- Source and issues: https://github.com/attaform/Attaform
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Custom components
|
|
2
|
+
|
|
3
|
+
Reach for these only when a native `<input>`, `<select>`, or `<textarea>` bound with `v-register` will not do. Most fields never need a wrapper.
|
|
4
|
+
|
|
5
|
+
## Three orthogonal primitives
|
|
6
|
+
|
|
7
|
+
There is deliberately no `useField(path)` that returns `{ register, fields, errors }` in one call. The three concerns are kept separate on purpose:
|
|
8
|
+
|
|
9
|
+
- **`register(path)` returns an instance, not a path abstraction.** Two inputs can register the same path and move in sync: they share the path's field _state_, but each `register()` call has its own transforms and DOM lifecycle. A single bundled binding would have to pretend there is one binding per path.
|
|
10
|
+
- **`fields(path)` is path-keyed leaf state**: `value`, `touched`, `dirty`, `blank`, `errors`, `label`, `aria`, the display signals. The noun is the path.
|
|
11
|
+
- **`errors` is a related but separate concern**, not the same thing as field state.
|
|
12
|
+
|
|
13
|
+
The three lines are the idiom, not a bundle. Small primitives age better than convenient bundles.
|
|
14
|
+
|
|
15
|
+
## A single-field wrapper
|
|
16
|
+
|
|
17
|
+
The parent binds with `v-register`; the wrapper re-forwards that same instance and reads state from it:
|
|
18
|
+
|
|
19
|
+
```vue
|
|
20
|
+
<!-- parent -->
|
|
21
|
+
<UiTextField v-register="form.register('email')" type="email" />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```vue
|
|
25
|
+
<!-- UiTextField.vue -->
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import { computed } from 'vue'
|
|
28
|
+
import { injectForm, useRegister } from 'attaform'
|
|
29
|
+
|
|
30
|
+
const rv = useRegister() // the forwarded instance
|
|
31
|
+
const form = injectForm(rv?.formKey) // the owning form, by the instance's key
|
|
32
|
+
const field = computed(() => form?.fields(rv?.segments ?? [])) // reactive leaf state
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<label>
|
|
37
|
+
<span>{{ field?.label }}</span>
|
|
38
|
+
<input v-register="rv" />
|
|
39
|
+
<em v-if="field?.showErrors">{{ field.firstError?.message }}</em>
|
|
40
|
+
</label>
|
|
41
|
+
</template>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`rv` and `field` may be `undefined` until the parent directive attaches, so defend every read with `?.`. `rv.path`, `rv.segments`, and `rv.formKey` pierce directly in script setup without `.value`.
|
|
45
|
+
|
|
46
|
+
For a **compound** component that binds _multiple_ paths (a date range exposing start and end, an address subform), skip `useRegister` (it assumes a single binding) and reach for `injectForm<Form>()`, then call `form.register(path)` for each field.
|
|
47
|
+
|
|
48
|
+
## Let Attaform own display and ARIA
|
|
49
|
+
|
|
50
|
+
- Read the display signals straight in the template: `field.showErrors` gates the error row, `field.showPending` gates an async "checking" indicator (it is anti-flash timed), `field.firstError?.message` is the text.
|
|
51
|
+
- `v-register`'s `autoAria` (on by default) keeps `aria-invalid`, `aria-busy`, and `aria-required` in sync. It wires the _error_ id, and only while the field is in its error state, so author `aria-describedby` yourself if a _static_ hint should stay associated too.
|
|
52
|
+
- **`v-register` alone does binding, SSR value injection, and ARIA.** Do not stack `@change` handlers, `:reset-signal` props, redundant directive imports, or watchers on top of it. If a control seems to need that scaffolding, find the idiomatic shape rather than hand-rolling around it.
|
|
53
|
+
|
|
54
|
+
## Third-party components
|
|
55
|
+
|
|
56
|
+
`v-register` binds a third-party component host, not just a native element, as long as the component renders a real form control and forwards attributes to it. The directive marks the host and injects the same binding, SSR, and ARIA it gives a native input.
|
|
57
|
+
|
|
58
|
+
## Do not re-declare native attributes as props
|
|
59
|
+
|
|
60
|
+
Attribute fallthrough already delivers `id`, `aria-*`, `class`, and the like to the root element. Declare a Vue prop only when script needs to _consume_ the value; otherwise let it fall through. If the component root is not the control, set `inheritAttrs: false` and retarget the attributes onto the control with `v-bind="$attrs"`.
|
|
61
|
+
|
|
62
|
+
## An optional `form-key` prop
|
|
63
|
+
|
|
64
|
+
When the parent form has a key, give the wrapper an optional `form-key` prop and resolve through it, falling back to ambient injection for the single-throwaway-form case:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const form = formKey ? injectForm(formKey) : injectForm()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Extend the surface, do not shrink it
|
|
71
|
+
|
|
72
|
+
A wrapper composable should expose Attaform's full surface and add derived accessors on top, not return a hand-picked subset that strands state Attaform already computed. Spreading the raw handle plus your derived fields keeps everything reachable.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
Attaform holds three error layers (schema validation, blank-required, and user-set) and reads them back through one surface. Server and API errors go through the user-set layer via `form.setErrors`; you rarely touch the other two directly.
|
|
4
|
+
|
|
5
|
+
## Routing server errors
|
|
6
|
+
|
|
7
|
+
Inside the submit callback, clear the stale user layer, then route the failure through `setErrors`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
const onSubmit = form.handleSubmit(async (values) => {
|
|
11
|
+
form.clearErrors()
|
|
12
|
+
try {
|
|
13
|
+
await save(values)
|
|
14
|
+
} catch (err) {
|
|
15
|
+
form.setErrors(normalizeErrors(err))
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `setErrors(errors | updater | (path, errors))` is a **whole-layer replace**, so `clearErrors()` at the top of a fresh attempt drops errors a previous submit set. `clearErrors(path?)` scopes the clear.
|
|
21
|
+
- `handleSubmit` focuses the first offending field for user-set errors exactly as it does for client-invalid fields, so a `setErrors` call inside the callback needs no separate focus step.
|
|
22
|
+
|
|
23
|
+
## The `ValidationError` envelope
|
|
24
|
+
|
|
25
|
+
If your backend emits Attaform's `ValidationError` shape, it pipes straight into `setErrors` with no translation layer:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import type { ValidationError, Json } from 'attaform'
|
|
29
|
+
|
|
30
|
+
// { message: string; path: (string | number)[]; code?: string; data?: Json | null }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- One entry per message at `path: [field]`; a form-level error is `path: []`; a dotted key splits into segments.
|
|
34
|
+
- A structured non-field payload (a lockout timestamp, a challenge token) is **one form-level error** whose `.data` carries the payload, never exploded into phantom field errors.
|
|
35
|
+
- **Do not build a translation layer** to adapt a payload that already is Attaform's type. If the only friction is a type mismatch (your `data` typed `Record<string, unknown>` rather than Attaform's `Json`), fix the _type_, not with code: `data: z.custom<Json>().nullish()`. Messages come from one source (the server), not a frontend copy map.
|
|
36
|
+
|
|
37
|
+
A thrown `onSubmit` is also caught and surfaces as a single form-level error, so a bare `throw` is the shortcut when you have no per-field array to distribute. Reach for `setErrors` when the server hands you an array that should target individual fields.
|
|
38
|
+
|
|
39
|
+
## The own vs subtree axis
|
|
40
|
+
|
|
41
|
+
Every `FieldState` node (leaves, containers, and the form root) exposes two error axes:
|
|
42
|
+
|
|
43
|
+
| Accessor | Scope |
|
|
44
|
+
| --------------------------------------- | ----------------------------------------------- |
|
|
45
|
+
| `node.ownErrors` / `node.firstOwnError` | errors at **this path exactly**, no descendants |
|
|
46
|
+
| `node.errors` / `form.errors(path)` | the **subtree**: this path plus descendants |
|
|
47
|
+
|
|
48
|
+
On a leaf the two axes coincide. They diverge on containers and on the form root.
|
|
49
|
+
|
|
50
|
+
## Banners
|
|
51
|
+
|
|
52
|
+
A form-level banner wants the root's **own** bucket, not the aggregate:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const banner = computed(() => form.meta.firstOwnError)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Do not use `form.errors([])` for a banner: `errors(path)` uniformly means "path plus descendants", so `errors([])` is the whole-form aggregate and would surface individual field errors in the summary. `form.meta.firstOwnError` is the correct root-only read. The same own axis surfaces a container-level `.refine()` error, for example `form.fields.address.firstOwnError`.
|
|
59
|
+
|
|
60
|
+
## One normalizer, always non-empty
|
|
61
|
+
|
|
62
|
+
Route every form's error handling through a single normalizer that sits at the error-reading layer, above the transport, and **always returns a non-empty error set**. The fallback is load-bearing: a network drop, a non-standard 500, or a contract violation whose error array came back empty should still yield one generic form-level error, so the user never sees a submit that silently no-ops. Keep this one chokepoint rather than digging the envelope at each call site.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SSR
|
|
2
|
+
|
|
3
|
+
SSR is automatic under the Nuxt module or the Vite plugin. Field values, `checked`, and `selected` are rendered into the first paint and ARIA is emitted, with no extra wiring. You do not opt in and you do not configure it. What follows matters mostly when you are _debugging_ SSR or writing a test that touches it.
|
|
4
|
+
|
|
5
|
+
## The value injection is a build-time transform
|
|
6
|
+
|
|
7
|
+
On the compiled template path, `v-register`'s SSR value / `checked` / `selected` injection is a **build-time Vue-compiler transform** (from `attaform/vite`, wired by the `attaform/nuxt` module), not a runtime `getSSRProps`. On that path `getSSRProps` emits only the `aria-*` attributes; the value rides on the compile-time transform. On the runtime render-function path (a hand-written `h()` component), the value instead comes through `getSSRProps`. Both paths reach the same rendered HTML; they differ only in _which_ mechanism carries the value.
|
|
8
|
+
|
|
9
|
+
One consequence worth knowing: a `<input :type="...">` with a _dynamic_ type bails the input transform, so give an input a static `type` when you want the compile-time SSR value.
|
|
10
|
+
|
|
11
|
+
## Confirm SSR through the real build
|
|
12
|
+
|
|
13
|
+
Always confirm an Attaform SSR claim through the **real build**: curl or inspect the server-rendered HTML of a real (or throwaway) page. Never judge SSR behavior from a bare unit render.
|
|
14
|
+
|
|
15
|
+
The reason is the transform above. A plain Vitest render (bare `vue()`, no Attaform compiler transform) **cannot** see SSR-injected values for a compiled form component, so a check written there fails even when production is correct. A naive isolated Node render experiment false-negatives for the same reason: the transform is not present, so even the correct shape looks broken.
|
|
16
|
+
|
|
17
|
+
## Test traps
|
|
18
|
+
|
|
19
|
+
- **Do not hand-wire `attaform/vite` into `vitest.config.ts`** to force a green. That fakes a third compile pipeline that matches neither production nor a clean unit test, and it hides the real reason a bare test cannot see the value.
|
|
20
|
+
- A faithful SSR test goes through the real build: a Nuxt-level test with `@nuxt/test-utils`. The one lower-level exception is a test that itself invokes `@vue/compiler-sfc`'s `compileTemplate` with Attaform's real node transforms, which reproduces the compiled path honestly.
|
|
21
|
+
|
|
22
|
+
## One concern that stays in your app
|
|
23
|
+
|
|
24
|
+
Attaform handles the SSR-adjacent details itself: the value injection above, and error-focus (which requests `focusVisible: true`, so the ring paints even when focus moves programmatically to a radio or checkbox). One SSR pattern is still the app's responsibility:
|
|
25
|
+
|
|
26
|
+
- **Seeding a page-owned form from async data races SSR.** If an ancestor lifts a form (so it can be injected downward) and seeds it from an async query through an immediate `watch`, the server's watch runs before the query resolves and does not re-fire, so the server renders empty while the client hydrates filled: a hydration mismatch. Seed **both legs**: a client immediate `watch` and a server `onServerPrefetch` that awaits the query, then seeds. A form created lazily behind a resolved gate never hits this.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Validation
|
|
2
|
+
|
|
3
|
+
Attaform validates against the form's own Zod schema and drives the display signals from it. The schema is the contract; design it deliberately.
|
|
4
|
+
|
|
5
|
+
## Client is UX, server is truth
|
|
6
|
+
|
|
7
|
+
The client schema is a **UX gate only**. A direct request bypasses it entirely, so the server must validate every input independently. Client validation exists to give fast inline feedback, not to protect the backend. Write the schema for the person filling the form; never lean on it as a security boundary.
|
|
8
|
+
|
|
9
|
+
## Keep closed sets in sync
|
|
10
|
+
|
|
11
|
+
Where a field is a closed set on both sides (a country, a size band, a status), derive the client `z.enum` from the same option list the control renders, and mirror the server's set. The client should reject _exactly_ what the server would, never stricter on a value the server accepts. A drift between the two shows up as a form that blocks a submit the backend would have taken.
|
|
12
|
+
|
|
13
|
+
## Reflect server truth, not optimism
|
|
14
|
+
|
|
15
|
+
On a successful mutation, reset the form from the returned resource or re-read a server-truth source, rather than trusting the values you submitted. A success message should sit on server-confirmed state, not on the optimistic input. This keeps the form honest when the server normalizes, defaults, or rejects part of what you sent.
|
|
16
|
+
|
|
17
|
+
## A clearable edit field is a required string
|
|
18
|
+
|
|
19
|
+
A field the user must be able to **clear** should be a required, possibly-empty string, not `.optional()`:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const schema = z.object({
|
|
23
|
+
// Wrong for a clearable edit field: a cleared value drops from the payload,
|
|
24
|
+
// and a partial-update backend reads the omission as "unchanged".
|
|
25
|
+
// nickname: z.string().optional(),
|
|
26
|
+
|
|
27
|
+
// Right: an empty string still transmits, so the clear reaches the server.
|
|
28
|
+
nickname: z.string(),
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A cleared `.optional()` field drops out of the payload, and a partial-update backend that treats an absent key as "leave unchanged" silently ignores the clear. Use `z.string()` (allowed empty) so the cleared value is sent and the update takes.
|
|
33
|
+
|
|
34
|
+
## Move real requirements into the schema
|
|
35
|
+
|
|
36
|
+
If a field's only guard was a disabled submit button, that requirement is not enforced once you stop disabling the button (which you should, see the core rules). Put the requirement in the schema (`.min(1)`, `.refine(...)`) so `handleSubmit` actually fails on it and focuses the field. A field left as a bare `z.string()` or `z.boolean()` accepts empty or `false`, which is often not what an "acknowledge" checkbox or a required text field means.
|
|
37
|
+
|
|
38
|
+
## The display signals
|
|
39
|
+
|
|
40
|
+
The per-field `showErrors` / `showSuccess` / `showPending` signals already encode a reward-early, punish-late display policy: an error is revealed once the user has engaged with the field, success is shown when earned, and the async pending state is anti-flash timed. Read those signals; do not rebuild the visibility heuristic from raw `errors` and `touched`.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Wizards
|
|
2
|
+
|
|
3
|
+
`useWizard` composes existing `useForm` instances into a multistep flow with navigation, per-step status, and one aggregate submit. Navigation and submission are separate verbs; keep them separate.
|
|
4
|
+
|
|
5
|
+
## Step slots
|
|
6
|
+
|
|
7
|
+
A step slot is one of:
|
|
8
|
+
|
|
9
|
+
- a `useForm` reference,
|
|
10
|
+
- a **bare string**: an always-valid noop step (key = the string), the native primitive for an informational or affordance screen with no schema,
|
|
11
|
+
- `null` / `undefined`: filtered out of the flow,
|
|
12
|
+
- a **function** returning any of those, for runtime branching,
|
|
13
|
+
- a `lazy((ctx) => ...)`-wrapped function, which memoizes its resolution and re-fires only when its own tracked reads change.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { useForm, useWizard } from 'attaform'
|
|
17
|
+
|
|
18
|
+
const account = useForm({ schema: accountSchema, key: 'account' })
|
|
19
|
+
const profile = useForm({ schema: profileSchema, key: 'profile' })
|
|
20
|
+
|
|
21
|
+
const wizard = useWizard({ key: 'onboarding', steps: [account, 'review', profile] })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Navigation vs submission
|
|
25
|
+
|
|
26
|
+
- **`wizard.tryNext(): Promise<boolean>`** is the gated Next. It validates the active step and advances only on a clean pass, revealing that step's errors in place otherwise. It resolves to whether it advanced, and it is inline-bindable: `@click="wizard.tryNext()"`.
|
|
27
|
+
- **`wizard.next()` / `wizard.back()` / `wizard.goTo(key)`** are positional moves with no validation gate. Use them for a Back button or a jump; use `tryNext` for a forward move that should validate.
|
|
28
|
+
- **`wizard.handleSubmit(onSubmit, onError?)`** validates **every** step from any position and calls `onSubmit` once with all forms' values. It **never advances**. Wire it to the final Submit.
|
|
29
|
+
|
|
30
|
+
For a forward move that runs a custom callback before advancing, compose the step form's own submit with `next`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const onStepDone = account.handleSubmit(async (values) => {
|
|
34
|
+
await saveDraft(values)
|
|
35
|
+
wizard.next()
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`handleSubmit` validating the whole list from any step means there is no "only the last step validates everything" caveat: a user who steps back, edits, and submits from the middle still gets the whole flow validated, and `done` still latches on success. `ctx.isFinal` reports only _where_ the submit fired, never _what_ was validated.
|
|
40
|
+
|
|
41
|
+
## The submit context
|
|
42
|
+
|
|
43
|
+
The `onSubmit` callback receives a context, not a bare values object:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const onComplete = wizard.handleSubmit(async (ctx) => {
|
|
47
|
+
// ctx.values is the aggregate keyed by form key (mirrors wizard.allValues)
|
|
48
|
+
// ctx.get(form) returns one form's typed parsed output
|
|
49
|
+
// ctx.currentKey is the step that fired the submit
|
|
50
|
+
// ctx.isFinal is positional: whether currentKey is the last step
|
|
51
|
+
await fetch('/onboarding', { method: 'POST', body: JSON.stringify(ctx.values) })
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`ctx.get(account)` is the type-safe read for one form: it returns that form's parsed output typed from its schema, which survives across a component graph because the form ref carries its schema.
|
|
56
|
+
|
|
57
|
+
## Reading aggregate state
|
|
58
|
+
|
|
59
|
+
The wizard handle exposes the flow's rolled-up state, all reactive:
|
|
60
|
+
|
|
61
|
+
- `wizard.forms[key]`: the live form handle for a step (a facade typed as a form).
|
|
62
|
+
- `wizard.allValues` / `wizard.allErrors`: every form's values / aggregate errors, keyed.
|
|
63
|
+
- `wizard.activeForm`: the currently active step's form facade.
|
|
64
|
+
- `wizard.statuses`: per-form status. Plus `wizard.progress`, `wizard.canAdvance`, `wizard.canGoBack`, `wizard.isFinalStep`, `wizard.visited`.
|
|
65
|
+
|
|
66
|
+
## A declarative step registry
|
|
67
|
+
|
|
68
|
+
When steps carry metadata (title, a visibility predicate, persisted keys), drive the whole wizard from one registry rather than scattering the shape. `WizardCtx` is `{ forms, currentKey }` and reactive, so a `when` predicate can branch on live form values and re-evaluate as they change:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { useWizard } from 'attaform'
|
|
72
|
+
import type { AnyForm, WizardCtx } from 'attaform'
|
|
73
|
+
|
|
74
|
+
interface StepDef {
|
|
75
|
+
key: string
|
|
76
|
+
title: string
|
|
77
|
+
form: AnyForm | null // null becomes a bare-string noop slot
|
|
78
|
+
when?: (ctx: WizardCtx) => boolean // omitted means always shown
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const registry: StepDef[] = [
|
|
82
|
+
{ key: 'intent', title: 'Get started', form: null },
|
|
83
|
+
{ key: 'account', title: 'Your account', form: account },
|
|
84
|
+
{ key: 'profile', title: 'Your profile', form: profile },
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
const wizard = useWizard({
|
|
88
|
+
key: 'onboarding',
|
|
89
|
+
steps: registry.map(
|
|
90
|
+
(s) => (ctx: WizardCtx) => ((s.when?.(ctx) ?? true) ? (s.form ?? s.key) : undefined)
|
|
91
|
+
),
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Everything derives from the registry: the step slots, the titles, any persisted key list. Add, remove, reorder, or gate a step by editing the registry alone. Do not add speculative registry fields with no consumer; add a hook when the second consumer arrives.
|
|
96
|
+
|
|
97
|
+
## Keyed injection resolves by tree and timing
|
|
98
|
+
|
|
99
|
+
A string key passed to `injectForm('account')` or `injectWizard('onboarding')` _reads_ like a global address but resolves by the **caller's component-tree position and mount timing**, not as a global lookup:
|
|
100
|
+
|
|
101
|
+
- `provide` / `inject` is descendant-only, so a form created in a **leaf** is unreachable from an **ancestor**.
|
|
102
|
+
- A keyed form is not resolvable until the creating component's `setup` has run, so mount order matters.
|
|
103
|
+
|
|
104
|
+
The consequence: a purely structural refactor can silently break a lookup. The stable shape is to **lift a shared form's creation to the coordinating ancestor** (the component that owns the wizard) and have leaves `injectForm(key)` it downward by a prop key. Always chain `?.` on the result, which is `T | null`.
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import './attaform.DBNjHgNj.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Typed error classes thrown by the form library. Each one signals a
|
|
5
|
-
* distinct misuse so calling code can branch on `instanceof` instead
|
|
6
|
-
* of pattern-matching error messages.
|
|
7
|
-
*
|
|
8
|
-
* Every class extends `AttaformError`, so consumers can write a single
|
|
9
|
-
* polymorphic catch (`catch (e) { if (e instanceof AttaformError) ... }`)
|
|
10
|
-
* instead of OR-chaining checks for each subclass. `AttaformError` itself
|
|
11
|
-
* extends the standard `Error`, so existing `instanceof Error` usage
|
|
12
|
-
* keeps working.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Base for every error class thrown by `attaform`. Sets
|
|
17
|
-
* `this.name` from the constructor's `new.target.name`, so subclasses
|
|
18
|
-
* don't have to redeclare their own name override.
|
|
19
|
-
*/
|
|
20
|
-
declare class AttaformError extends Error {
|
|
21
|
-
constructor(message: string, options?: ErrorOptions);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Thrown when a path string is malformed — typically a dotted path
|
|
25
|
-
* with empty segments (e.g. `'a..b'`, leading or trailing dots).
|
|
26
|
-
* Use array form (`['a', 'b']`) for keys that contain literal dots.
|
|
27
|
-
*/
|
|
28
|
-
declare class InvalidPathError extends AttaformError {
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Thrown when `useForm` receives an invalid configuration — most often
|
|
32
|
-
* a schema passed directly as the first argument, or no argument at
|
|
33
|
-
* all. The configuration is an options bag; the schema is one of
|
|
34
|
-
* several fields, even when it's the only one in use.
|
|
35
|
-
*
|
|
36
|
-
* ```ts
|
|
37
|
-
* // ✗ Crashes deep inside the validator with an opaque message:
|
|
38
|
-
* const form = useForm(z.object({ ... }))
|
|
39
|
-
* // ✗ Same:
|
|
40
|
-
* const form = useForm()
|
|
41
|
-
* // ✓ Pass the schema as the `schema` field:
|
|
42
|
-
* const form = useForm({ schema: z.object({ ... }) })
|
|
43
|
-
* ```
|
|
44
|
-
*
|
|
45
|
-
* The same shape applies to every entry point: `attaform/zod`,
|
|
46
|
-
* `attaform/zod-v3`, `attaform/zod-v4`, and the schema-agnostic
|
|
47
|
-
* `attaform` root.
|
|
48
|
-
*/
|
|
49
|
-
declare class InvalidUseFormConfigError extends AttaformError {
|
|
50
|
-
constructor();
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Thrown when a `handleSubmit`-supplied `onError` callback itself
|
|
54
|
-
* throws or rejects. Wraps the inner failure so both the original
|
|
55
|
-
* cause (via `error.cause`) and the propagation site are visible.
|
|
56
|
-
*/
|
|
57
|
-
declare class SubmitErrorHandlerError extends AttaformError {
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Thrown when an `attaform` API needs the registry attached to a Vue
|
|
61
|
-
* app but it isn't there yet. Component-level entry points (`useForm`,
|
|
62
|
-
* `injectForm`, `useRegister`) lazy-install the registry on first use,
|
|
63
|
-
* so this error is mostly reached by SSR helpers — `renderAttaformState`
|
|
64
|
-
* and `hydrateAttaformState` — which run outside a setup context and
|
|
65
|
-
* have no current instance to install against.
|
|
66
|
-
*
|
|
67
|
-
* Fix: add `app.use(createAttaform())` (or `app.use(createAttaform({
|
|
68
|
-
* ssr: true }))` on the server) to your SSR entry, before
|
|
69
|
-
* `renderToString` / hydration. Under Nuxt, `attaform/nuxt` already
|
|
70
|
-
* does this; the error usually points at a non-Nuxt SSR setup that
|
|
71
|
-
* hasn't installed explicitly.
|
|
72
|
-
*/
|
|
73
|
-
declare class RegistryNotInstalledError extends AttaformError {
|
|
74
|
-
constructor();
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Thrown when `useForm` / `injectForm` is called outside of a
|
|
78
|
-
* Vue `setup()` function — typically from an event handler, watcher,
|
|
79
|
-
* or async callback that runs after mount.
|
|
80
|
-
*
|
|
81
|
-
* Fix: move the call into `setup()`, or trigger it from a child
|
|
82
|
-
* component whose `setup()` runs the composable.
|
|
83
|
-
*/
|
|
84
|
-
declare class OutsideSetupError extends AttaformError {
|
|
85
|
-
constructor();
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Thrown when a `useForm({ key })` call uses a key starting with
|
|
89
|
-
* `__atta:`. That prefix is reserved for keys the library generates
|
|
90
|
-
* internally (e.g. for anonymous `useForm()` calls without an
|
|
91
|
-
* explicit key). Pick a different prefix for your form.
|
|
92
|
-
*/
|
|
93
|
-
declare class ReservedFormKeyError extends AttaformError {
|
|
94
|
-
constructor(key: string);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export { AttaformError as A, InvalidPathError as I, OutsideSetupError as O, RegistryNotInstalledError as R, SubmitErrorHandlerError as S, InvalidUseFormConfigError as a, ReservedFormKeyError as b };
|