@foormjs/vue 0.2.4 → 0.2.5

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 CHANGED
@@ -1,28 +1,51 @@
1
1
  # @foormjs/vue
2
2
 
3
- Vue 3 components for rendering foorm models with built-in validation.
4
-
5
- Most form libraries force you into their component system. `@foormjs/vue` works the other way: it gives you a form component with sensible defaults for every field type, but lets you replace any part of the rendering with your own components. Use the built-in inputs to get started, then gradually swap in your design system components -- per field, per type, or via scoped slots.
6
-
7
- Forms are defined in ATScript `.as` files (via `@foormjs/atscript`), converted to a model at runtime, and rendered by `OoForm`. Validation, computed properties, and reactive state are handled automatically.
3
+ Renderless Vue components for ATScript-defined forms. Bring your own UI components and wrap them in `OoForm` / `OoField` to get automatic validation, computed properties, and reactive form state — all driven by `.as` schema files.
8
4
 
9
5
  ## Install
10
6
 
11
7
  ```bash
12
- npm install @foormjs/vue
8
+ pnpm add @foormjs/vue @foormjs/atscript vue @atscript/core @atscript/typescript
13
9
  # or
14
- pnpm add @foormjs/vue
10
+ npm install @foormjs/vue @foormjs/atscript vue @atscript/core @atscript/typescript
15
11
  ```
16
12
 
17
- You'll also need the ATScript tooling in your dev dependencies:
13
+ You also need the ATScript Vite plugin for `.as` file support:
18
14
 
19
15
  ```bash
20
- pnpm add -D @foormjs/atscript @atscript/core @atscript/typescript unplugin-atscript
16
+ pnpm add -D unplugin-atscript @vitejs/plugin-vue
21
17
  ```
22
18
 
23
19
  ## Quick Start
24
20
 
25
- ### 1. Define a form in ATScript
21
+ ### 1. Configure ATScript
22
+
23
+ ```ts
24
+ // atscript.config.ts
25
+ import { defineConfig } from '@atscript/core'
26
+ import ts from '@atscript/typescript'
27
+ import { foormPlugin } from '@foormjs/atscript/plugin'
28
+
29
+ export default defineConfig({
30
+ rootDir: 'src',
31
+ plugins: [ts(), foormPlugin()],
32
+ })
33
+ ```
34
+
35
+ ### 2. Configure Vite
36
+
37
+ ```ts
38
+ // vite.config.ts
39
+ import { defineConfig } from 'vite'
40
+ import vue from '@vitejs/plugin-vue'
41
+ import atscript from 'unplugin-atscript/vite'
42
+
43
+ export default defineConfig({
44
+ plugins: [atscript(), vue()],
45
+ })
46
+ ```
47
+
48
+ ### 3. Define a form schema
26
49
 
27
50
  ```
28
51
  // src/forms/login.as
@@ -32,352 +55,478 @@ export interface LoginForm {
32
55
  @meta.label 'Email'
33
56
  @meta.placeholder 'you@example.com'
34
57
  @foorm.autocomplete 'email'
35
- @foorm.validate '(v) => !!v || "Email is required"'
36
- email: string
58
+ @meta.required 'Email is required'
59
+ @foorm.order 1
60
+ email: string.email
37
61
 
38
62
  @meta.label 'Password'
63
+ @meta.placeholder 'Enter password'
39
64
  @foorm.type 'password'
40
- @foorm.validate '(v) => !!v || "Password is required"'
65
+ @meta.required 'Password is required'
66
+ @foorm.order 2
41
67
  password: string
42
-
43
- @meta.label 'Remember me'
44
- rememberMe?: foorm.checkbox
45
68
  }
46
69
  ```
47
70
 
48
- ### 2. Render it
71
+ ### 4. Use in a Vue component
49
72
 
50
73
  ```vue
51
74
  <script setup lang="ts">
52
75
  import { OoForm, useFoorm } from '@foormjs/vue'
53
76
  import { LoginForm } from './forms/login.as'
54
77
 
55
- const { form, formData } = useFoorm(LoginForm)
78
+ const { def, formData } = useFoorm(LoginForm)
56
79
 
57
80
  function handleSubmit(data: typeof formData) {
58
- console.log('Submitted:', data)
81
+ console.log('submitted', data)
59
82
  }
60
83
  </script>
61
84
 
62
85
  <template>
63
- <OoForm :form="form" :form-data="formData" first-validation="on-blur" @submit="handleSubmit" />
86
+ <OoForm :def="def" :form-data="formData" :types="{}" @submit="handleSubmit" />
64
87
  </template>
65
88
  ```
66
89
 
67
- That's it. The form renders with labels, placeholders, validation, and a submit button -- all derived from the `.as` file annotations.
90
+ `OoForm` renders default HTML inputs for all standard field types out of the box. For production use, you'll want to supply your own components via the `types` prop.
68
91
 
69
- ## Practical Examples
92
+ ## AI Agent Skills
70
93
 
71
- ### Passing context for dynamic options
94
+ `@foormjs/vue` ships an AI agent skill for Claude Code, Cursor, Windsurf, Codex, and other compatible agents. The skill teaches your agent the library's APIs, patterns, and best practices so it can help you write correct code without hallucinating.
72
95
 
73
- Backend-provided data (option lists, user info, locale) goes through the `formContext` prop and becomes available to `@foorm.fn.*` annotations:
96
+ **Install the skill into your agent:**
74
97
 
98
+ ```bash
99
+ # Project-local (recommended — version-locked, commits with your repo)
100
+ npx @foormjs/vue setup-skills
101
+
102
+ # Global (available across all your projects)
103
+ npx @foormjs/vue setup-skills --global
75
104
  ```
76
- // preferences.as
77
- export interface PreferencesForm {
78
- @meta.label 'City'
79
- @meta.placeholder 'Select a city'
80
- @foorm.fn.options '(v, data, context) => context.cityOptions || []'
81
- city?: foorm.select
105
+
106
+ Restart your agent after installing.
107
+
108
+ **Auto-update on install** — to keep the skill in sync whenever you upgrade the package, add this to your project's `package.json`:
109
+
110
+ ```jsonc
111
+ {
112
+ "scripts": {
113
+ "postinstall": "npx @foormjs/vue setup-skills --postinstall",
114
+ },
82
115
  }
83
116
  ```
84
117
 
85
- ```vue
86
- <script setup lang="ts">
87
- import { OoForm, useFoorm } from '@foormjs/vue'
88
- import { PreferencesForm } from './forms/preferences.as'
118
+ ## Advanced Usage
89
119
 
90
- const { form, formData } = useFoorm(PreferencesForm)
120
+ ### Custom Components by Type
91
121
 
92
- const formContext = {
93
- cityOptions: [
94
- { key: 'nyc', label: 'New York' },
95
- { key: 'la', label: 'Los Angeles' },
96
- { key: 'chi', label: 'Chicago' },
97
- ],
122
+ Map field types to your UI components:
123
+
124
+ ```vue
125
+ <script setup lang="ts">
126
+ import { OoForm } from '@foormjs/vue'
127
+ import MyTextInput from './MyTextInput.vue'
128
+ import MySelect from './MySelect.vue'
129
+ import MyCheckbox from './MyCheckbox.vue'
130
+
131
+ const typeComponents = {
132
+ text: MyTextInput,
133
+ select: MySelect,
134
+ checkbox: MyCheckbox,
98
135
  }
99
136
  </script>
100
137
 
101
138
  <template>
102
- <OoForm :form="form" :form-data="formData" :form-context="formContext" @submit="handleSubmit" />
139
+ <OoForm :def="def" :form-data="formData" :types="typeComponents" @submit="onSubmit" />
103
140
  </template>
104
141
  ```
105
142
 
106
- ### Custom components by name
143
+ Every field with `type: 'text'` will render `MyTextInput`, every `type: 'select'` renders `MySelect`, etc.
107
144
 
108
- Use `@foorm.component` in your `.as` file and pass the component map:
145
+ ### Custom Components by Name
146
+
147
+ Use `@foorm.component` in your schema to assign a named component to a specific field:
109
148
 
110
149
  ```
111
- // profile.as
112
- export interface ProfileForm {
113
- @meta.label 'Birthday'
114
- @foorm.component 'DatePicker'
115
- birthday?: string
116
- }
150
+ @meta.label 'Rating'
151
+ @foorm.component 'StarRating'
152
+ @foorm.order 5
153
+ rating?: number
117
154
  ```
118
155
 
156
+ Then pass named components via the `components` prop:
157
+
119
158
  ```vue
120
159
  <template>
121
160
  <OoForm
122
- :form="form"
161
+ :def="def"
123
162
  :form-data="formData"
124
- :components="{ DatePicker: MyDatePicker }"
125
- @submit="handleSubmit"
163
+ :types="typeComponents"
164
+ :components="{ StarRating: MyStarRating }"
165
+ @submit="onSubmit"
126
166
  />
127
167
  </template>
128
168
  ```
129
169
 
130
- ### Custom components by type
170
+ Named components take priority over type-based components.
171
+
172
+ ### Building a Custom Component
131
173
 
132
- Replace the default renderer for all fields of a given type:
174
+ Custom components receive `TFoormComponentProps` as their props:
133
175
 
134
176
  ```vue
177
+ <script setup lang="ts">
178
+ import type { TFoormComponentProps } from '@foormjs/vue'
179
+
180
+ const props = defineProps<TFoormComponentProps<string>>()
181
+ </script>
182
+
135
183
  <template>
136
- <OoForm
137
- :form="form"
138
- :form-data="formData"
139
- :types="{ text: MyTextInput, select: MySelect }"
140
- @submit="handleSubmit"
141
- />
184
+ <div :class="{ disabled, error: !!error }">
185
+ <label>{{ label }}</label>
186
+ <input
187
+ :value="model.value"
188
+ @input="model.value = ($event.target as HTMLInputElement).value"
189
+ @blur="onBlur"
190
+ :placeholder="placeholder"
191
+ :disabled="disabled"
192
+ :readonly="readonly"
193
+ />
194
+ <span v-if="error" class="error">{{ error }}</span>
195
+ <span v-else-if="hint" class="hint">{{ hint }}</span>
196
+ </div>
142
197
  </template>
143
198
  ```
144
199
 
145
- ### Scoped slots
200
+ Key props available to your component:
201
+
202
+ | Prop | Type | Description |
203
+ | ------------------ | ----------------------------- | ---------------------------------------------------------- |
204
+ | `model` | `{ value: V }` | Reactive model — bind with `v-model="model.value"` |
205
+ | `value` | `unknown?` | Phantom display value (`@foorm.value` / `@foorm.fn.value`) |
206
+ | `onBlur` | `(e: FocusEvent) => void` | Triggers validation on blur |
207
+ | `error` | `string?` | Validation error message |
208
+ | `label` | `string?` | Resolved field label |
209
+ | `description` | `string?` | Resolved field description |
210
+ | `hint` | `string?` | Resolved hint text |
211
+ | `placeholder` | `string?` | Resolved placeholder |
212
+ | `disabled` | `boolean?` | Whether the field is disabled |
213
+ | `hidden` | `boolean?` | Whether the field is hidden |
214
+ | `readonly` | `boolean?` | Whether the field is read-only |
215
+ | `optional` | `boolean?` | Whether the field is optional |
216
+ | `required` | `boolean?` | Whether the field is required |
217
+ | `type` | `string` | The field input type |
218
+ | `altAction` | `TFoormAltAction?` | Alternate action `{ id, label }` from `@foorm.altAction` |
219
+ | `options` | `TFoormEntryOptions[]?` | Options for select/radio fields |
220
+ | `name` | `string?` | Field name |
221
+ | `maxLength` | `number?` | Max length constraint |
222
+ | `autocomplete` | `string?` | HTML autocomplete value |
223
+ | `field` | `FoormFieldDef?` | Full field definition for advanced use |
224
+ | `title` | `string?` | Resolved title for object/array fields |
225
+ | `level` | `number?` | Nesting level (root=0, increments per nested object/array) |
226
+ | `class` | `string \| object?` | CSS class(es) from `@foorm.fn.classes` |
227
+ | `style` | `string \| object?` | Inline styles from `@foorm.fn.styles` |
228
+ | `onRemove` | `() => void?` | Callback to remove this item from its parent array |
229
+ | `canRemove` | `boolean?` | Whether removal is allowed (respects minLength) |
230
+ | `removeLabel` | `string?` | Label for the remove button |
231
+ | `arrayIndex` | `number?` | Zero-based index when rendered as an array item |
232
+ | `onToggleOptional` | `(enabled: boolean) => void?` | Toggle an optional field on/off |
233
+
234
+ ### Arrays
235
+
236
+ Array fields are handled automatically. Define them in your `.as` schema:
146
237
 
147
- Override rendering for specific field types or form sections:
238
+ ```
239
+ @meta.label 'Tags'
240
+ @foorm.array.add.label 'Add tag'
241
+ @foorm.array.remove.label 'x'
242
+ @expect.maxLength 5, 'Maximum 5 tags'
243
+ tags: string[]
244
+
245
+ @meta.label 'Addresses'
246
+ @foorm.title 'Addresses'
247
+ @foorm.array.add.label 'Add address'
248
+ addresses: {
249
+ @meta.label 'Street'
250
+ @meta.required 'Street is required'
251
+ street: string
252
+
253
+ @meta.label 'City'
254
+ city: string
255
+ }[]
256
+ ```
257
+
258
+ `OoForm` renders arrays with add/remove buttons, inline editing for primitives, and sub-form cards for objects. Array-level validation (`@expect.minLength`, `@expect.maxLength`) is displayed below the add button.
259
+
260
+ Union arrays (`(ObjectType | string)[]`) render a variant selector per item and offer one add button per variant.
261
+
262
+ The remove button is the responsibility of the wrapping component:
263
+
264
+ - **Object array items**: The object component receives `onRemove`, `canRemove`, and `removeLabel` via `TFoormComponentProps`.
265
+ - **Primitive array items**: The field component receives `onRemove`, `canRemove`, and `removeLabel` as props (since there is no object wrapper around them).
266
+
267
+ ### Nested Groups
268
+
269
+ Use `@foorm.title` on a nested object field to render it as a titled, visually distinct section:
270
+
271
+ ```
272
+ @foorm.title 'Settings'
273
+ settings: {
274
+ @meta.label 'Notify by email'
275
+ emailNotify: foorm.checkbox
276
+
277
+ @meta.label 'Page size'
278
+ @foorm.type 'number'
279
+ pageSize?: number
280
+ }
281
+ ```
282
+
283
+ Without `@foorm.title`, nested object fields flatten into the parent form. With it, they render as an indented group with a title header. Groups can be nested to any depth.
284
+
285
+ ### Scoped Slots
286
+
287
+ `OoForm` provides scoped slots for full layout control:
148
288
 
149
289
  ```vue
150
290
  <template>
151
- <OoForm :form="form" :form-data="formData" @submit="handleSubmit">
152
- <!-- Custom header -->
153
- <template #form.header="{ title }">
154
- <h1 class="my-title">{{ title }}</h1>
291
+ <OoForm :def="def" :form-data="formData" :types="typeComponents" @submit="onSubmit">
292
+ <!-- Form header (rendered before fields) -->
293
+ <template #form.header="{ clearErrors, reset, setErrors, formContext, disabled }">
294
+ <h1>Form Header</h1>
295
+ </template>
296
+
297
+ <!-- Content before/after fields -->
298
+ <template #form.before="{ clearErrors, reset, setErrors }">
299
+ <p>All fields are required unless marked optional.</p>
155
300
  </template>
156
301
 
157
- <!-- Custom text field renderer -->
158
- <template #field:text="field">
159
- <div class="my-field">
160
- <label>{{ field.label }}</label>
161
- <input
162
- v-model="field.model.value"
163
- @blur="field.onBlur"
164
- :placeholder="field.placeholder"
165
- :disabled="field.disabled"
166
- />
167
- <span v-if="field.error" class="error">{{ field.error }}</span>
168
- </div>
302
+ <template #form.after="{ clearErrors, reset, setErrors, disabled, formContext }">
303
+ <p v-if="disabled">Please fill out all required fields.</p>
169
304
  </template>
170
305
 
171
306
  <!-- Custom submit button -->
172
- <template #form.submit="{ disabled, text }">
173
- <button class="my-button" :disabled="disabled">{{ text }}</button>
307
+ <template #form.submit="{ text, disabled, clearErrors, reset, setErrors, formContext }">
308
+ <button type="submit" :disabled="disabled" class="my-btn">{{ text }}</button>
309
+ </template>
310
+
311
+ <!-- Footer (rendered after submit) -->
312
+ <template #form.footer="{ disabled, clearErrors, reset, setErrors, formContext }">
313
+ <p>By submitting, you agree to our terms.</p>
174
314
  </template>
175
315
  </OoForm>
176
316
  </template>
177
317
  ```
178
318
 
179
- ### Handling actions
319
+ ### Form Context
180
320
 
181
- Action fields emit events instead of submitting the form:
321
+ Pass runtime data (user session, feature flags, dynamic options) to computed functions and validators:
182
322
 
183
- ```
184
- // wizard.as
185
- export interface WizardStep {
186
- @meta.label 'Name'
187
- name: string
188
-
189
- @meta.label 'Reset Form'
190
- @foorm.altAction 'reset'
191
- resetBtn: foorm.action
323
+ ```vue
324
+ <script setup lang="ts">
325
+ const formContext = {
326
+ cityOptions: [
327
+ { key: 'nyc', label: 'New York' },
328
+ { key: 'la', label: 'Los Angeles' },
329
+ ],
330
+ user: { role: 'admin' },
192
331
  }
193
- ```
332
+ </script>
194
333
 
195
- ```vue
196
334
  <template>
197
- <OoForm :form="form" :form-data="formData" @submit="handleSubmit" @action="handleAction" />
335
+ <OoForm
336
+ :def="def"
337
+ :form-data="formData"
338
+ :form-context="formContext"
339
+ :types="typeComponents"
340
+ @submit="onSubmit"
341
+ />
198
342
  </template>
343
+ ```
199
344
 
200
- <script setup lang="ts">
201
- function handleAction(name: string, data: unknown) {
202
- if (name === 'reset') {
203
- // Reset the form
204
- }
205
- }
206
- </script>
345
+ Context is accessible in ATScript function strings as the third argument:
346
+
347
+ ```
348
+ @foorm.fn.options '(v, data, ctx) => ctx.cityOptions || []'
349
+ city?: foorm.select
207
350
  ```
208
351
 
209
- ### Server-side validation errors
352
+ ### Server-side Errors
210
353
 
211
- Pass external errors (e.g., from an API response) via the `errors` prop:
354
+ Pass server-side validation errors directly to fields:
212
355
 
213
356
  ```vue
214
- <template>
215
- <OoForm :form="form" :form-data="formData" :errors="serverErrors" @submit="handleSubmit" />
216
- </template>
217
-
218
357
  <script setup lang="ts">
219
358
  import { ref } from 'vue'
220
359
 
221
360
  const serverErrors = ref<Record<string, string>>({})
222
361
 
223
- async function handleSubmit(data: unknown) {
224
- const response = await api.submit(data)
225
- if (response.errors) {
226
- serverErrors.value = response.errors
227
- // e.g. { email: 'Email already exists' }
362
+ async function handleSubmit(data: any) {
363
+ const result = await api.submit(data)
364
+ if (result.errors) {
365
+ serverErrors.value = result.errors // e.g. { email: 'Already taken' }
228
366
  }
229
367
  }
230
368
  </script>
231
- ```
232
-
233
- ## API
234
369
 
235
- ### `useFoorm(Type)`
370
+ <template>
371
+ <OoForm
372
+ :def="def"
373
+ :form-data="formData"
374
+ :errors="serverErrors"
375
+ :types="typeComponents"
376
+ @submit="handleSubmit"
377
+ />
378
+ </template>
379
+ ```
236
380
 
237
- Composable that creates a form model and reactive data from an ATScript type.
381
+ ### Actions
238
382
 
239
- ```ts
240
- import { useFoorm } from '@foormjs/vue'
241
- import { MyForm } from './my-form.as'
383
+ Define alternate submit actions using the `foorm.action` primitive:
242
384
 
243
- const { form, formData } = useFoorm(MyForm)
385
+ ```
386
+ @meta.label 'Reset Password'
387
+ @foorm.altAction 'reset-password', 'Reset Password'
388
+ resetBtn: foorm.action
244
389
  ```
245
390
 
246
- Returns:
247
-
248
- - `form` -- `TFoormModel` object with fields, title, and submit config
249
- - `formData` -- Vue `reactive()` object initialized from field defaults
250
-
251
- ### `OoForm` Props
252
-
253
- | Prop | Type | Description |
254
- | ----------------- | --------------------------- | -------------------------------------------- |
255
- | `form` | `TFoormModel` | Form model (required) |
256
- | `formData` | `object` | Reactive form data (auto-created if omitted) |
257
- | `formContext` | `object` | External context for computed functions |
258
- | `firstValidation` | `'on-blur' \| 'on-submit'` | When to trigger first validation |
259
- | `components` | `Record<string, Component>` | Named component map for `@foorm.component` |
260
- | `types` | `Record<string, Component>` | Type-based component map |
261
- | `errors` | `Record<string, string>` | External validation errors |
262
-
263
- ### `OoForm` Events
264
-
265
- | Event | Payload | Description |
266
- | -------------------- | ---------------- | -------------------------------------------------------------- |
267
- | `submit` | `formData` | Emitted when the form passes validation and is submitted |
268
- | `action` | `name, formData` | Emitted when an action button is clicked |
269
- | `unsupported-action` | `name, formData` | Emitted when an action is clicked but not defined in the model |
270
-
271
- ### `OoForm` Slots
272
-
273
- | Slot | Props | Description |
274
- | -------------- | -------------------------------------------------- | ---------------------------------------------- |
275
- | `form.header` | `title, clearErrors, reset, formContext, disabled` | Before all fields (default: `<h2>` with title) |
276
- | `form.before` | `clearErrors, reset` | After header, before fields |
277
- | `field:{type}` | All field props (see below) | Override renderer for a field type |
278
- | `form.after` | `clearErrors, reset, disabled, formContext` | After fields, before submit |
279
- | `form.submit` | `disabled, text, clearErrors, reset, formContext` | Submit button |
280
- | `form.footer` | `disabled, clearErrors, reset, formContext` | After submit button |
281
-
282
- ### Field Slot Props
283
-
284
- Every field slot (`#field:text`, `#field:select`, etc.) receives:
285
-
286
- | Prop | Type | Description |
287
- | -------------- | ------------------------- | ----------------------------------------------------------- |
288
- | `model` | `{ value: V }` | Two-way binding (use `v-model="field.model.value"`) |
289
- | `onBlur` | `Function` | Call on blur to trigger validation |
290
- | `error` | `string \| undefined` | Current validation error |
291
- | `label` | `string` | Evaluated label |
292
- | `description` | `string` | Evaluated description |
293
- | `hint` | `string` | Evaluated hint |
294
- | `placeholder` | `string` | Evaluated placeholder |
295
- | `options` | `TFoormEntryOptions[]` | Options for select/radio |
296
- | `classes` | `Record<string, boolean>` | CSS class object (includes `error`, `disabled`, `required`) |
297
- | `styles` | `string \| Record` | Inline styles |
298
- | `disabled` | `boolean` | Evaluated disabled state |
299
- | `hidden` | `boolean` | Evaluated hidden state |
300
- | `readonly` | `boolean` | Evaluated readonly state |
301
- | `optional` | `boolean` | Evaluated optional state |
302
- | `required` | `boolean` | Inverse of optional |
303
- | `type` | `string` | Field type |
304
- | `vName` | `string` | HTML `name` attribute |
305
- | `field` | `string` | Field identifier |
306
- | `altAction` | `string` | Action name (for action fields) |
307
- | `autocomplete` | `string` | HTML autocomplete value |
308
- | `maxLength` | `number` | Max length constraint |
309
- | `formData` | `object` | Full form data |
310
- | `formContext` | `object` | Form context |
311
- | `attrs` | `Record` | Custom attributes (evaluated, can be used with `v-bind`) |
391
+ Handle the action event:
312
392
 
313
- ### `OoField`
393
+ ```vue
394
+ <template>
395
+ <OoForm
396
+ :def="def"
397
+ :form-data="formData"
398
+ :types="typeComponents"
399
+ @submit="onSubmit"
400
+ @action="onAction"
401
+ />
402
+ </template>
314
403
 
315
- Renderless field wrapper (used internally by `OoForm`, but available for advanced usage). Wraps `VuilessField` from `vuiless-forms`, evaluating all computed properties and exposing resolved values through its default slot.
404
+ <script setup lang="ts">
405
+ function onAction(name: string, data: any) {
406
+ if (name === 'reset-password') {
407
+ // handle reset password
408
+ }
409
+ }
410
+ </script>
411
+ ```
316
412
 
317
- ### `TFoormComponentProps`
413
+ ### Paragraphs
318
414
 
319
- TypeScript interface for custom field components. Implement this when building reusable field components:
415
+ Display static or computed text using the `foorm.paragraph` primitive:
320
416
 
321
- ```ts
322
- import type { TFoormComponentProps } from '@foormjs/vue'
417
+ ```
418
+ @foorm.value 'Please fill out all required fields.'
419
+ info: foorm.paragraph
323
420
 
324
- // Your component receives these props:
325
- defineProps<TFoormComponentProps<string, MyFormData, MyContext>>()
421
+ @foorm.fn.value '(v, data) => "Hello, " + (data.firstName || "guest") + "!"'
422
+ greeting: foorm.paragraph
326
423
  ```
327
424
 
328
- ## Built-in Field Renderers
425
+ Paragraphs are phantom fields — they are excluded from form data, validation, and TypeScript types. They only exist for display purposes.
329
426
 
330
- `OoForm` includes default renderers for all standard field types:
427
+ ## API Reference
331
428
 
332
- | Type | Renders as | Notes |
333
- | ---------------------------- | ------------------------- | ----------------------------------------- |
334
- | `text`, `password`, `number` | `<input>` | With label, description, error/hint |
335
- | `select` | `<select>` | With placeholder as disabled first option |
336
- | `radio` | Radio group | Vertical layout with labels |
337
- | `checkbox` | `<input type="checkbox">` | Label beside the checkbox |
338
- | `paragraph` | `<p>` | Description text, no input |
339
- | `action` | `<button>` | Emits action event on click |
429
+ ### `useFoorm(type)`
340
430
 
341
- Includes minimal CSS that you can override or replace entirely.
431
+ Creates a reactive form definition and data object from an ATScript annotated type.
342
432
 
343
- ## Rendering Priority
433
+ ```ts
434
+ const { def, formData } = useFoorm(MyFormType)
435
+ ```
344
436
 
345
- For each field, `OoForm` tries renderers in this order:
437
+ - **`def`** `FoormDef` with root field, ordered fields, the source type, and a flatMap
438
+ - **`formData`** — Vue `reactive()` object with default values from the schema
346
439
 
347
- 1. **Named component** -- `@foorm.component` + `components` prop
348
- 2. **Type component** -- field type + `types` prop
349
- 3. **Scoped slot** -- `#field:{type}` slot
350
- 4. **Built-in default** -- standard HTML renderer
351
- 5. **Error message** -- "Not supported field type" fallback
440
+ ### `OoForm`
352
441
 
353
- ## Vite Configuration
442
+ Renderless form wrapper component.
354
443
 
355
- Add ATScript support to your Vite config:
444
+ **Props:**
356
445
 
357
- ```ts
358
- // vite.config.ts
359
- import { defineConfig } from 'vite'
360
- import vue from '@vitejs/plugin-vue'
361
- import atscript from 'unplugin-atscript'
446
+ | Prop | Type | Required | Description |
447
+ | ----------------- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------- |
448
+ | `def` | `FoormDef` | Yes | Form definition from `useFoorm()` or `createFoormDef()` |
449
+ | `formData` | `object` | No | Reactive form data (created internally if omitted) |
450
+ | `formContext` | `object` | No | External context for computed functions and validators |
451
+ | `firstValidation` | `'on-change' \| 'touched-on-blur' \| 'on-blur' \| 'on-submit' \| 'none'` | No | When to trigger first validation |
452
+ | `components` | `Record<string, Component>` | No | Named components (matched by `@foorm.component`) |
453
+ | `types` | `Record<string, Component>` | Yes | Type-based components (matched by field type) |
454
+ | `errors` | `Record<string, string>` | No | External error messages (e.g. server-side) |
362
455
 
363
- export default defineConfig({
364
- plugins: [atscript.vite(), vue()],
365
- })
366
- ```
456
+ **Events:**
367
457
 
368
- And configure ATScript:
458
+ | Event | Payload | Description |
459
+ | -------------------- | ----------------------------------------------- | --------------------------------------------------------------- |
460
+ | `submit` | `formData` | Emitted on valid form submission |
461
+ | `error` | `{ path: string; message: string }[]` | Emitted when validation fails on submit |
462
+ | `action` | `name, formData` | Emitted when an action button is clicked (supported alt action) |
463
+ | `unsupported-action` | `name, formData` | Emitted for unrecognized action names |
464
+ | `change` | `type: TFoormChangeType, path, value, formData` | Emitted on field update, array add/remove, or union switch |
369
465
 
370
- ```ts
371
- // atscript.config.ts
372
- import { defineConfig } from '@atscript/core'
373
- import ts from '@atscript/typescript'
374
- import { foormPlugin } from '@foormjs/atscript/plugin'
466
+ **Slots:**
375
467
 
376
- export default defineConfig({
377
- rootDir: 'src',
378
- plugins: [ts(), foormPlugin()],
379
- })
380
- ```
468
+ | Slot | Scope | Description |
469
+ | ------------- | ---------------------------------------------------------------- | --------------------------- |
470
+ | `form.header` | `{ clearErrors, reset, setErrors, formContext, disabled }` | Before fields |
471
+ | `form.before` | `{ clearErrors, reset, setErrors }` | After header, before fields |
472
+ | `form.after` | `{ clearErrors, reset, setErrors, disabled, formContext }` | After fields, before submit |
473
+ | `form.submit` | `{ text, disabled, clearErrors, reset, setErrors, formContext }` | Submit button |
474
+ | `form.footer` | `{ disabled, clearErrors, reset, setErrors, formContext }` | After submit |
475
+
476
+ ### `OoField`
477
+
478
+ Universal field renderer. Resolves component, props, validation, and nesting for any field type.
479
+
480
+ **Props:**
481
+
482
+ | Prop | Type | Description |
483
+ | ------------- | --------------- | -------------------------------------------------------------- |
484
+ | `field` | `FoormFieldDef` | Field definition from `def.fields` or `def.rootField` |
485
+ | `error` | `string?` | External error message |
486
+ | `onRemove` | `() => void?` | Callback to remove this item from its parent array |
487
+ | `canRemove` | `boolean?` | Whether removal is allowed (respects minLength) |
488
+ | `removeLabel` | `string?` | Label for the remove button (from `@foorm.array.remove.label`) |
489
+ | `arrayIndex` | `number?` | Zero-based index when rendered as an array item |
490
+
491
+ ### Default Components
492
+
493
+ All default type components are exported and can be used as-is or as reference implementations:
494
+
495
+ | Component | Field Type | Description |
496
+ | ------------- | ----------- | -------------------------------------------------- |
497
+ | `OoInput` | `text`, etc | Text/password/number input with OoFieldShell |
498
+ | `OoSelect` | `select` | Dropdown select with OoFieldShell |
499
+ | `OoRadio` | `radio` | Radio button group with OoFieldShell |
500
+ | `OoCheckbox` | `checkbox` | Boolean checkbox with OoFieldShell |
501
+ | `OoParagraph` | `paragraph` | Read-only text display |
502
+ | `OoAction` | `action` | Action button with altAction support |
503
+ | `OoObject` | `object` | Object container (title + OoIterator) |
504
+ | `OoArray` | `array` | Array container (add/remove + OoIterator per item) |
505
+ | `OoUnion` | `union` | Union variant picker + selected variant rendering |
506
+ | `OoTuple` | `tuple` | Fixed-length tuple via OoIterator |
507
+
508
+ ### Composables
509
+
510
+ | Export | Description |
511
+ | --------------------------------- | --------------------------------------------------------------------- |
512
+ | `useFoorm(type)` | Returns `{ def, formData }` from an ATScript annotated type |
513
+ | `useFoormArray(field, disabled?)` | Array state management (keys, add/remove, constraints) |
514
+ | `useFoormUnion(props)` | Union variant state management |
515
+ | `useConsumeUnionContext()` | Consume and clear the `__foorm_union` injection |
516
+ | `formatIndexedLabel()` | Format label with array index prefix (e.g. `"Address #1"`) |
517
+ | `createDefaultTypes()` | Returns a `TFoormTypeComponents` map with all default type components |
518
+
519
+ ### Types
520
+
521
+ | Export | Description |
522
+ | -------------------------- | ------------------------------------------------------------- |
523
+ | `TFoormBaseComponentProps` | Shared base props (disabled, hidden) |
524
+ | `TFoormComponentProps` | Unified props interface for ALL custom field components |
525
+ | `TFoormTypeComponents` | Required shape for the `types` prop on `OoForm` |
526
+ | `TFoormChangeType` | `'update' \| 'array-add' \| 'array-remove' \| 'union-switch'` |
527
+ | `TFoormUnionContext` | Union context provided via `__foorm_union` inject |
528
+
529
+ For ATScript documentation, see [atscript.moost.org](https://atscript.moost.org).
381
530
 
382
531
  ## License
383
532