@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 +385 -236
- package/dist/index.cjs +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +446 -216
- package/dist/index.js +1554 -371
- package/package.json +29 -25
- package/scripts/setup-skills.js +78 -0
- package/skills/foormjs-vue/.placeholder +0 -0
- package/skills/foormjs-vue/SKILL.md +53 -0
- package/skills/foormjs-vue/composables.md +189 -0
- package/skills/foormjs-vue/core.md +279 -0
- package/skills/foormjs-vue/custom-components.md +677 -0
- package/skills/foormjs-vue/defaults.md +266 -0
- package/skills/foormjs-vue/rendering.md +175 -0
- package/dist/index.umd.cjs +0 -1
- package/dist/style.css +0 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Default Components — @foormjs/vue
|
|
2
|
+
|
|
3
|
+
> Built-in field components, what each renders, internal helpers, and how to use or override them.
|
|
4
|
+
|
|
5
|
+
## Using Default Components
|
|
6
|
+
|
|
7
|
+
### Option 1: Use `createDefaultTypes()` for all defaults
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createDefaultTypes } from '@foormjs/vue'
|
|
11
|
+
|
|
12
|
+
const types = createDefaultTypes()
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Option 2: Spread and override specific types
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createDefaultTypes } from '@foormjs/vue'
|
|
19
|
+
import MyInput from './components/MyInput.vue'
|
|
20
|
+
import MySelect from './components/MySelect.vue'
|
|
21
|
+
|
|
22
|
+
const types = { ...createDefaultTypes(), text: MyInput, select: MySelect }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Option 3: Build from scratch (full control)
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import type { TFoormTypeComponents } from '@foormjs/vue'
|
|
29
|
+
import {
|
|
30
|
+
OoInput,
|
|
31
|
+
OoSelect,
|
|
32
|
+
OoRadio,
|
|
33
|
+
OoCheckbox,
|
|
34
|
+
OoParagraph,
|
|
35
|
+
OoAction,
|
|
36
|
+
OoObject,
|
|
37
|
+
OoArray,
|
|
38
|
+
OoUnion,
|
|
39
|
+
OoTuple,
|
|
40
|
+
} from '@foormjs/vue'
|
|
41
|
+
|
|
42
|
+
const types: TFoormTypeComponents = {
|
|
43
|
+
text: OoInput,
|
|
44
|
+
password: OoInput,
|
|
45
|
+
number: OoInput,
|
|
46
|
+
select: OoSelect,
|
|
47
|
+
radio: OoRadio,
|
|
48
|
+
checkbox: OoCheckbox,
|
|
49
|
+
paragraph: OoParagraph,
|
|
50
|
+
action: OoAction,
|
|
51
|
+
object: OoObject,
|
|
52
|
+
array: OoArray,
|
|
53
|
+
union: OoUnion,
|
|
54
|
+
tuple: OoTuple,
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Option 4: Add custom type keys
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const types = { ...createDefaultTypes(), textarea: MyTextarea, date: MyDatePicker }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use `@foorm.type 'textarea'` or `@foorm.type 'date'` in the schema to route to your component.
|
|
65
|
+
|
|
66
|
+
### Option 5: Per-field named component override
|
|
67
|
+
|
|
68
|
+
Use `@foorm.component` in the schema + `components` prop on OoForm:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
// schema.as — @foorm.component takes priority over @foorm.type / auto-inferred type
|
|
72
|
+
@foorm.component 'StarRating'
|
|
73
|
+
rating?: number
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```vue
|
|
77
|
+
<OoForm :types="types" :components="{ StarRating: MyStarRating }" ... />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Default Type-to-Component Map
|
|
81
|
+
|
|
82
|
+
| Type key | Component | Schema trigger |
|
|
83
|
+
| ----------- | ----------- | -------------------------------------- |
|
|
84
|
+
| `text` | OoInput | `string` fields (default) |
|
|
85
|
+
| `password` | OoInput | `@foorm.type 'password'` |
|
|
86
|
+
| `number` | OoInput | `number` fields |
|
|
87
|
+
| `select` | OoSelect | `foorm.select` primitive |
|
|
88
|
+
| `radio` | OoRadio | `foorm.radio` primitive |
|
|
89
|
+
| `checkbox` | OoCheckbox | `boolean` / `foorm.checkbox` |
|
|
90
|
+
| `paragraph` | OoParagraph | `foorm.paragraph` phantom |
|
|
91
|
+
| `action` | OoAction | `foorm.action` phantom |
|
|
92
|
+
| `object` | OoObject | nested objects / `@foorm.title` groups |
|
|
93
|
+
| `array` | OoArray | `type[]` arrays |
|
|
94
|
+
| `union` | OoUnion | union types (`A \| B`) |
|
|
95
|
+
| `tuple` | OoTuple | tuple types `[A, B]` |
|
|
96
|
+
|
|
97
|
+
## Responsibility Matrix — What Each Default Handles
|
|
98
|
+
|
|
99
|
+
| | OoInput/Select/Radio/Checkbox | OoObject | OoArray | OoUnion | OoTuple | OoParagraph | OoAction |
|
|
100
|
+
| ----------------------- | ----------------------------------------- | ---------------------- | ------------------------------------- | -------------------- | ---------------------- | ----------- | -------------- |
|
|
101
|
+
| **Label** | via OoFieldShell | — | — | — | — | — | — |
|
|
102
|
+
| **Title** | — | OoStructuredHeader | OoStructuredHeader | — | OoStructuredHeader | — | — |
|
|
103
|
+
| **Description** | via OoFieldShell | — | — | — | — | — | — |
|
|
104
|
+
| **Hint** | via OoFieldShell | — | — | — | — | — | — |
|
|
105
|
+
| **Error** | via OoFieldShell | inline div | inline div | — | inline div | — | — |
|
|
106
|
+
| **Remove/clear button** | via OoFieldShell | OoStructuredHeader | — | — | OoStructuredHeader | — | — |
|
|
107
|
+
| **Variant picker** | via OoFieldShell | OoStructuredHeader | OoStructuredHeader | — (provides context) | OoStructuredHeader | — | — |
|
|
108
|
+
| **Optional N/A** | via OoFieldShell (OoNoData) | OoNoData | OoNoData | OoNoData | OoNoData | — | — |
|
|
109
|
+
| **Sub-field iteration** | — | OoIterator | OoField per item | OoField (inner) | OoField per pos | — | — |
|
|
110
|
+
| **Composables** | useConsumeUnionContext (via OoFieldShell) | useConsumeUnionContext | useFoormArray, useConsumeUnionContext | useFoormUnion | useConsumeUnionContext | — | — |
|
|
111
|
+
| **Change events** | — (OoField handles) | — | via useFoormArray | via useFoormUnion | — | — | emits `action` |
|
|
112
|
+
|
|
113
|
+
## Default Component Details
|
|
114
|
+
|
|
115
|
+
### OoInput (text, password, number)
|
|
116
|
+
|
|
117
|
+
Wraps `<input>` in OoFieldShell. The `type` prop determines the HTML input type.
|
|
118
|
+
|
|
119
|
+
- **Label/description/hint/error:** Delegated to OoFieldShell
|
|
120
|
+
- **Remove button:** Delegated to OoFieldShell (renders when `onRemove` provided)
|
|
121
|
+
- **Optional N/A:** Delegated to OoFieldShell
|
|
122
|
+
- **Variant picker:** Delegated to OoFieldShell (renders inline if union context present)
|
|
123
|
+
- **Bindings:** `v-model` on input, `@blur` → `onBlur`
|
|
124
|
+
- **Accessibility:** `aria-required`, `aria-invalid`, `aria-describedby` (via OoFieldShell)
|
|
125
|
+
|
|
126
|
+
### OoSelect
|
|
127
|
+
|
|
128
|
+
Wraps `<select>` with `<option>` per entry in OoFieldShell.
|
|
129
|
+
|
|
130
|
+
- **All chrome:** Delegated to OoFieldShell (same as OoInput)
|
|
131
|
+
- **Options:** Uses `optKey()`/`optLabel()` from `@foormjs/atscript`
|
|
132
|
+
- **Placeholder:** Disabled first `<option>` when `placeholder` prop present
|
|
133
|
+
- **Bindings:** `v-model` on select, `@change` and `@blur` both call `onBlur`
|
|
134
|
+
|
|
135
|
+
### OoRadio
|
|
136
|
+
|
|
137
|
+
Wraps radio group in OoFieldShell with custom header slot.
|
|
138
|
+
|
|
139
|
+
- **All chrome:** Delegated to OoFieldShell
|
|
140
|
+
- **Custom header:** Renders label + description in OoFieldShell's `#header` slot (not as default label)
|
|
141
|
+
- **Radios:** `v-for` over options, grouped by `name` prop, `role="radiogroup"`
|
|
142
|
+
|
|
143
|
+
### OoCheckbox
|
|
144
|
+
|
|
145
|
+
Wraps checkbox in OoFieldShell with custom header slot.
|
|
146
|
+
|
|
147
|
+
- **All chrome:** Delegated to OoFieldShell
|
|
148
|
+
- **Custom header:** Renders label only when optional && not enabled (in `#header` slot)
|
|
149
|
+
- **Label:** Wraps checkbox input for click area
|
|
150
|
+
- **Description:** In `#after-input` slot (after the checkbox label)
|
|
151
|
+
- **Binding:** `:checked` + `@change` manual update + `onBlur()`
|
|
152
|
+
|
|
153
|
+
### OoParagraph (phantom)
|
|
154
|
+
|
|
155
|
+
Simple `<p>` tag displaying `value` prop. No OoFieldShell, no model, no validation. Respects `v-show="!hidden"`.
|
|
156
|
+
|
|
157
|
+
### OoAction (phantom)
|
|
158
|
+
|
|
159
|
+
Button that emits `action` event with `altAction.id`. No OoFieldShell, no model, no validation. OoField catches the `action` emit and forwards to OoForm.
|
|
160
|
+
|
|
161
|
+
### OoObject
|
|
162
|
+
|
|
163
|
+
Container for nested object fields.
|
|
164
|
+
|
|
165
|
+
- **Title:** OoStructuredHeader (h2 at level 0, h3 at deeper levels)
|
|
166
|
+
- **Remove button:** OoStructuredHeader (when `onRemove` provided)
|
|
167
|
+
- **Variant picker:** OoStructuredHeader (when union context has multiple variants)
|
|
168
|
+
- **Optional N/A:** OoNoData placeholder
|
|
169
|
+
- **Sub-fields:** OoIterator with the nested `objectDef`
|
|
170
|
+
- **Composables:** `useConsumeUnionContext()` — reads and clears union context
|
|
171
|
+
- **Array index:** `formatIndexedLabel()` for titles like "Address #1"
|
|
172
|
+
|
|
173
|
+
### OoArray
|
|
174
|
+
|
|
175
|
+
List container with add/remove buttons.
|
|
176
|
+
|
|
177
|
+
- **Title:** OoStructuredHeader
|
|
178
|
+
- **Items:** `<OoField>` per item (NOT OoIterator) with stable keys from `useFoormArray`
|
|
179
|
+
- **Remove:** Passed as `onRemove` prop to each item's OoField
|
|
180
|
+
- **Add button:** Simple button for scalar items; dropdown with variant options for union items (via `useDropdown()`)
|
|
181
|
+
- **Variant picker:** OoStructuredHeader (when union context present)
|
|
182
|
+
- **Optional N/A:** OoNoData placeholder
|
|
183
|
+
- **Composables:** `useFoormArray()` (manages state + emits `array-add`/`array-remove`), `useConsumeUnionContext()`, `useDropdown()`
|
|
184
|
+
- **Constraints:** `canAdd` respects `@expect.maxLength`, `canRemove` respects `@expect.minLength`
|
|
185
|
+
|
|
186
|
+
### OoUnion
|
|
187
|
+
|
|
188
|
+
Variant selector that provides context to children.
|
|
189
|
+
|
|
190
|
+
- **Variant management:** `useFoormUnion()` — manages variant index, data stashing (saves/restores data on switch)
|
|
191
|
+
- **Provides:** `__foorm_union` context `{ variants, currentIndex, changeVariant }` — consumed by the inner field's component (object/tuple renders variant picker in its header)
|
|
192
|
+
- **Inner field:** Renders `<OoField :key="localUnionIndex">` for the selected variant
|
|
193
|
+
- **Optional N/A:** OoNoData with variant picker dropdown for multiple variants
|
|
194
|
+
- **Does NOT render:** title, label, variant picker, remove button (delegates all to inner field)
|
|
195
|
+
- **Passes through:** `onRemove`/`canRemove`/`removeLabel`/`arrayIndex` to the inner OoField
|
|
196
|
+
|
|
197
|
+
### OoTuple
|
|
198
|
+
|
|
199
|
+
Fixed-length field list.
|
|
200
|
+
|
|
201
|
+
- **Title:** OoStructuredHeader
|
|
202
|
+
- **Items:** `<OoField>` per position from `tupleField.itemFields` (fixed count, no add/remove)
|
|
203
|
+
- **Remove button:** OoStructuredHeader (when `onRemove` provided)
|
|
204
|
+
- **Variant picker:** OoStructuredHeader (when union context present)
|
|
205
|
+
- **Optional N/A:** OoNoData placeholder
|
|
206
|
+
- **Composables:** `useConsumeUnionContext()`
|
|
207
|
+
|
|
208
|
+
## Internal Components (Not Exported)
|
|
209
|
+
|
|
210
|
+
Used by default components. Custom components must handle these responsibilities themselves.
|
|
211
|
+
|
|
212
|
+
### OoFieldShell
|
|
213
|
+
|
|
214
|
+
Wrapper for leaf field components. Provides:
|
|
215
|
+
|
|
216
|
+
- Header row: label + required indicator + optional clear button + remove button
|
|
217
|
+
- Inline variant picker (when `__foorm_union` context exists with multiple variants)
|
|
218
|
+
- Optional N/A state (OoNoData)
|
|
219
|
+
- Input slot for the actual element
|
|
220
|
+
- Error/hint display
|
|
221
|
+
- Accessibility IDs: `inputId`, `errorId`, `descId`
|
|
222
|
+
- Slots: `#header`, `#default` (input), `#after-input`
|
|
223
|
+
|
|
224
|
+
### OoStructuredHeader
|
|
225
|
+
|
|
226
|
+
Header for structural components (object, array, tuple). Provides:
|
|
227
|
+
|
|
228
|
+
- Title: `<h2>` at level 0, `<h3>` at deeper levels
|
|
229
|
+
- Inline variant picker (OoVariantPicker) when union context has multiple variants
|
|
230
|
+
- Optional clear button
|
|
231
|
+
- Remove button (when `onRemove` provided)
|
|
232
|
+
- Flexbox layout: title left, buttons right
|
|
233
|
+
|
|
234
|
+
### OoNoData
|
|
235
|
+
|
|
236
|
+
Clickable placeholder for optional fields in N/A state.
|
|
237
|
+
|
|
238
|
+
- Dashed border with "No Data" text
|
|
239
|
+
- Hover state: "Edit" text
|
|
240
|
+
- Click/keyboard triggers `onEdit` callback
|
|
241
|
+
|
|
242
|
+
### OoVariantPicker
|
|
243
|
+
|
|
244
|
+
Dropdown for switching union variants.
|
|
245
|
+
|
|
246
|
+
- Three-dot icon button
|
|
247
|
+
- Dropdown menu listing all variants
|
|
248
|
+
- Active variant highlighted
|
|
249
|
+
- Uses `useDropdown()` for open/close + click-outside
|
|
250
|
+
|
|
251
|
+
## Styles
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import '@foormjs/vue/styles' // optional — provides CSS for default components
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
CSS classes use `oo-` prefix: `oo-form`, `oo-field`, `oo-object`, `oo-array`, etc.
|
|
258
|
+
|
|
259
|
+
## Gotchas
|
|
260
|
+
|
|
261
|
+
- Internal components (OoFieldShell, OoStructuredHeader, OoNoData, OoVariantPicker) are NOT exported — custom components must handle label/error/hint/N/A rendering themselves
|
|
262
|
+
- OoInput serves three type keys (`text`, `password`, `number`) — the `type` prop determines behavior
|
|
263
|
+
- OoObject without `@foorm.title` still renders as a container — just without a visible title
|
|
264
|
+
- OoArray uses `<OoField>` per item (not OoIterator) because it needs per-item remove props and stable keys
|
|
265
|
+
- OoUnion renders NO chrome — it provides context and delegates everything to the inner variant's component
|
|
266
|
+
- Default styles are optional but recommended when using default components
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Rendering Architecture — @foormjs/vue
|
|
2
|
+
|
|
3
|
+
> OoField internals, component resolution, nesting levels, provide/inject keys, and the allStatic optimization.
|
|
4
|
+
|
|
5
|
+
## Rendering Chain
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
OoForm (provides context, renders rootField)
|
|
9
|
+
└── OoField(def.rootField) → types['object'] → OoObject
|
|
10
|
+
└── OoIterator (iterates def.fields)
|
|
11
|
+
├── OoField (leaf) → types['text'] → OoInput / your component
|
|
12
|
+
├── OoField (object with @foorm.title) → types['object'] → OoObject
|
|
13
|
+
│ └── OoIterator → OoField ...
|
|
14
|
+
├── OoField (array) → types['array'] → OoArray
|
|
15
|
+
│ └── OoField (per item via useFoormArray) → ...
|
|
16
|
+
├── OoField (union) → types['union'] → OoUnion
|
|
17
|
+
│ └── OoField (selected variant) → ...
|
|
18
|
+
└── OoField (tuple) → types['tuple'] → OoTuple
|
|
19
|
+
└── OoField (per position) → ...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## OoField — Universal Renderer
|
|
23
|
+
|
|
24
|
+
`OoField` is the core of the rendering pipeline. For each field it:
|
|
25
|
+
|
|
26
|
+
1. **Injects** types, components, errors, action handler, change handler from parent OoForm
|
|
27
|
+
2. **Resolves component**: `@foorm.component` → `components[name]`, else → `types[field.type]`
|
|
28
|
+
3. **Resolves all field props** (label, placeholder, disabled, hidden, options, etc.)
|
|
29
|
+
4. **Tracks nesting level** via `__foorm_level` inject/provide
|
|
30
|
+
5. **Renders** via `<component :is="resolvedComponent" v-bind="componentProps" />`
|
|
31
|
+
|
|
32
|
+
### Two-Path Optimization
|
|
33
|
+
|
|
34
|
+
OoField detects whether a field has any computed annotations:
|
|
35
|
+
|
|
36
|
+
- **`allStatic` fast path** — no `@foorm.fn.*` or `@foorm.validate` annotations. All props resolved once at setup, no Vue `computed()` refs created. Significantly reduces reactive overhead.
|
|
37
|
+
- **Dynamic path** — per-property static/dynamic detection. Only properties with `@foorm.fn.*` become computed refs; static props remain plain values.
|
|
38
|
+
|
|
39
|
+
The `allStatic` flag is set by `createFoormDef` via `hasComputedAnnotations()`.
|
|
40
|
+
|
|
41
|
+
### Lazy Scope Construction
|
|
42
|
+
|
|
43
|
+
OoField builds scopes in two phases (dual-scope pattern):
|
|
44
|
+
|
|
45
|
+
1. **`baseScope`** — `{ v, data, context }` — resolves constraints (disabled, hidden, readonly)
|
|
46
|
+
2. **`fullScope`** — `{ v, data, context, entry }` — resolves display props (label, placeholder, options)
|
|
47
|
+
|
|
48
|
+
This prevents circular dependency: options can reference constraint state via `entry.disabled`.
|
|
49
|
+
|
|
50
|
+
### Component Resolution Order
|
|
51
|
+
|
|
52
|
+
1. `@foorm.component` annotation value → lookup in `components` prop
|
|
53
|
+
2. `types[field.type]` → lookup in `types` prop
|
|
54
|
+
3. If not found → renders error div: `[label] No component for type "X"`
|
|
55
|
+
|
|
56
|
+
## OoIterator
|
|
57
|
+
|
|
58
|
+
Iterates `def.fields` and renders `<OoField>` per field. Used by object and tuple components.
|
|
59
|
+
|
|
60
|
+
```vue
|
|
61
|
+
<OoIterator :def="objectDef" />
|
|
62
|
+
|
|
63
|
+
<!-- With path prefix (for array items) -->
|
|
64
|
+
<OoIterator :def="objectDef" path-prefix="[0]" />
|
|
65
|
+
|
|
66
|
+
<!-- With remove props (passed through to child fields) -->
|
|
67
|
+
<OoIterator
|
|
68
|
+
:def="objectDef"
|
|
69
|
+
:on-remove="onRemove"
|
|
70
|
+
:can-remove="canRemove"
|
|
71
|
+
:remove-label="removeLabel"
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Props:**
|
|
76
|
+
|
|
77
|
+
- `def: FoormDef` — form definition to iterate
|
|
78
|
+
- `pathPrefix?: string` — custom path segment (e.g., `[0]` for array items)
|
|
79
|
+
- `onRemove?`, `canRemove?`, `removeLabel?` — passed through to child `OoField`
|
|
80
|
+
|
|
81
|
+
## Nesting Level Tracking
|
|
82
|
+
|
|
83
|
+
`OoField` manages nesting depth via `__foorm_level` inject/provide:
|
|
84
|
+
|
|
85
|
+
- Root object = level 0 (inject default: -1, increments for object/array/tuple types)
|
|
86
|
+
- Each nested object/array/tuple increments the level
|
|
87
|
+
- **Union fields do NOT increment** — prevents double-counting (union → inner object would be +2 otherwise)
|
|
88
|
+
|
|
89
|
+
Level-aware rendering in default components:
|
|
90
|
+
|
|
91
|
+
- Level 0: title rendered as `<h2>`, no left border
|
|
92
|
+
- Level > 0: title rendered as `<h3>`, left border + padding
|
|
93
|
+
|
|
94
|
+
## Provide/Inject Keys
|
|
95
|
+
|
|
96
|
+
### Set by OoForm
|
|
97
|
+
|
|
98
|
+
| Key | Type | Description |
|
|
99
|
+
| ------------------------ | ---------------------------------------- | -------------------------------- |
|
|
100
|
+
| `__foorm_root_data` | `ComputedRef<TFormData>` | Reactive form data |
|
|
101
|
+
| `__foorm_path_prefix` | `ComputedRef<string>` | Current path prefix ('' at root) |
|
|
102
|
+
| `__foorm_types` | `ComputedRef<TFoormTypeComponents>` | Type-to-component map |
|
|
103
|
+
| `__foorm_components` | `ComputedRef<Record<string, Component>>` | Named component map |
|
|
104
|
+
| `__foorm_errors` | `ComputedRef<Record<string, string>>` | External errors |
|
|
105
|
+
| `__foorm_action_handler` | `(name, data) => void` | Action event forwarder |
|
|
106
|
+
| `__foorm_change_handler` | `(type, path, value) => void` | Change event emitter |
|
|
107
|
+
|
|
108
|
+
### Set by OoField
|
|
109
|
+
|
|
110
|
+
| Key | Type | Description |
|
|
111
|
+
| --------------------- | --------------------- | ------------------------- |
|
|
112
|
+
| `__foorm_path_prefix` | `ComputedRef<string>` | Updated path for children |
|
|
113
|
+
| `__foorm_level` | `number` | Incremented nesting level |
|
|
114
|
+
|
|
115
|
+
### Set by useFoormForm (from @foormjs/composables)
|
|
116
|
+
|
|
117
|
+
| Key | Type | Description |
|
|
118
|
+
| ---------------------- | ------------------------ | --------------------------------- |
|
|
119
|
+
| `__foorm_form` | `TFoormState` | Form state (validation, registry) |
|
|
120
|
+
| `__foorm_form_data` | `ComputedRef<TFormData>` | Form data wrapper |
|
|
121
|
+
| `__foorm_form_context` | `ComputedRef<TContext>` | External context |
|
|
122
|
+
|
|
123
|
+
### Set by useFoormUnion
|
|
124
|
+
|
|
125
|
+
| Key | Type | Description |
|
|
126
|
+
| --------------- | -------------------- | ------------------------------- |
|
|
127
|
+
| `__foorm_union` | `TFoormUnionContext` | Variant state + change function |
|
|
128
|
+
|
|
129
|
+
## useFoormContext (Internal)
|
|
130
|
+
|
|
131
|
+
Internal composable used by default components to access form state and build scopes.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const {
|
|
135
|
+
foormState, // TFoormState
|
|
136
|
+
rootFormData, // () => Record<string, unknown>
|
|
137
|
+
pathPrefix, // ComputedRef<string>
|
|
138
|
+
formContext, // ComputedRef<Record<string, unknown>>
|
|
139
|
+
joinPath, // (segment | fn) => ComputedRef<string>
|
|
140
|
+
buildPath, // (segment) => string
|
|
141
|
+
getByPath, // (path) => unknown
|
|
142
|
+
setByPath, // (path, value) => void
|
|
143
|
+
buildScope, // (v?, entry?) => TFoormFnScope
|
|
144
|
+
} = useFoormContext('MyComponent')
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Not publicly exported — use `getByPath`/`setByPath` from `@foormjs/atscript` directly in custom components.
|
|
148
|
+
|
|
149
|
+
## Change Events
|
|
150
|
+
|
|
151
|
+
OoField and composables emit change events via `__foorm_change_handler`:
|
|
152
|
+
|
|
153
|
+
| Source | Type | When |
|
|
154
|
+
| ------------- | ---------------- | ----------------------------------------- |
|
|
155
|
+
| OoField | `'update'` | On blur, if value changed since last blur |
|
|
156
|
+
| useFoormArray | `'array-add'` | After `addItem()` |
|
|
157
|
+
| useFoormArray | `'array-remove'` | After `removeItem()` |
|
|
158
|
+
| useFoormUnion | `'union-switch'` | After `changeVariant()` |
|
|
159
|
+
|
|
160
|
+
Note: `'update'` fires on blur (not on every keystroke) to reduce noise.
|
|
161
|
+
|
|
162
|
+
## Best Practices
|
|
163
|
+
|
|
164
|
+
- Use `OoIterator` in custom object components — don't manually iterate `objectDef.fields`
|
|
165
|
+
- Pass through `onRemove`/`canRemove`/`removeLabel` from your component to `OoField` or `OoIterator`
|
|
166
|
+
- Don't access provide/inject keys directly in custom components — use the composables (`useFoormArray`, etc.)
|
|
167
|
+
- The `allStatic` optimization is automatic — no action needed from custom components
|
|
168
|
+
|
|
169
|
+
## Gotchas
|
|
170
|
+
|
|
171
|
+
- Union fields provide `__foorm_path_prefix` but NOT an incremented `__foorm_level` — prevents double nesting visuals
|
|
172
|
+
- `OoIterator` provides its own `__foorm_path_prefix` — nested iterators correctly stack path segments
|
|
173
|
+
- `__foorm_level` defaults to `-1` when injected, so the first object/array increments it to `0`
|
|
174
|
+
- Change events are batched per-field — multiple rapid changes to the same field only emit once on blur
|
|
175
|
+
- Missing component for a field type renders an error div, not an exception — check console for warnings
|
package/dist/index.umd.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(function(p,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue"),require("foorm"),require("vuiless-forms"),require("@foormjs/atscript")):typeof define=="function"&&define.amd?define(["exports","vue","foorm","vuiless-forms","@foormjs/atscript"],e):(p=typeof globalThis<"u"?globalThis:p||self,e(p.index={},p.Vue,p.foorm,p.vuilessForms,p.atscript))})(this,function(p,e,V,F,q){"use strict";function A(r,o){if(!r)return;const n={};for(const[m,c]of Object.entries(r))n[m]=V.evalComputed(c,o);return n}const w=e.defineComponent({__name:"oo-field",props:{field:{},type:{},component:{},autocomplete:{},altAction:{},order:{},name:{},phantom:{type:Boolean},label:{type:[String,Function]},description:{type:[String,Function]},hint:{type:[String,Function]},placeholder:{type:[String,Function]},optional:{type:[Boolean,Function]},disabled:{type:[Boolean,Function]},hidden:{type:[Boolean,Function]},readonly:{type:[Boolean,Function]},classes:{type:[String,Object,Function]},styles:{type:[String,Object,Function]},options:{type:[Array,Function]},attrs:{},value:{type:Function},validators:{},maxLength:{},minLength:{},min:{},max:{},error:{}},setup(r){const o=r,n=e.inject("vuiless");function m(a,u,h){return typeof a=="function"?e.computed(()=>a(u.value)):a??h}const c=a=>typeof a=="object"&&a!==null&&"value"in a?a.value:a,k=e.computed(()=>({v:n.value.formData[o.field],data:n.value.formData,context:n.value.formContext??{},entry:void 0})),b=m(o.optional,k,!1),D=m(o.disabled,k,!1),B=m(o.hidden,k,!1),g=m(o.readonly,k,!1),x=o.phantom?void 0:e.computed(()=>!c(b)),i=e.computed(()=>({v:n.value.formData[o.field],data:n.value.formData,context:n.value.formContext??{},entry:{field:o.field,type:o.type,component:o.component,name:o.name||o.field,optional:c(b),disabled:c(D),hidden:c(B),readonly:c(g)}})),S=m(o.label,i,void 0),l=m(o.description,i,void 0),C=m(o.hint,i,void 0),d=m(o.placeholder,i,void 0),y=m(o.options,i,void 0),E=m(o.styles,i,void 0),t=e.computed(()=>{const a=typeof o.classes=="function"?o.classes(i.value):o.classes;return typeof a=="string"?{[a]:!0,disabled:c(D),required:!c(b)}:{...a,disabled:c(D),required:!c(b)}});function f(a,u){return{...t.value,error:!!a||!!u}}const N=e.computed(()=>A(o.attrs,i.value)),_=o.phantom?e.computed(()=>typeof o.value=="function"?o.value(i.value):o.value):void 0;if(typeof o.value=="function"&&!o.phantom){const a=e.computed(()=>{if(c(g))return o.value(i.value)});e.watch(a,u=>{u!==void 0&&c(g)&&(n.value.formData[o.field]=u)},{immediate:!0})}const s=o.validators.map(a=>(u,h,ne)=>a({v:u,data:h,context:ne??{},entry:i.value.entry}));return(a,u)=>(e.openBlock(),e.createBlock(e.unref(F.VuilessField),{modelValue:e.unref(n).formData[o.field],"onUpdate:modelValue":u[0]||(u[0]=h=>e.unref(n).formData[o.field]=h),rules:e.unref(s)},{default:e.withCtx(h=>[e.renderSlot(a.$slots,"default",{onBlur:h.onBlur,error:r.error||h.error,model:h.model,formData:e.unref(n).formData,formContext:e.unref(n).formContext,label:e.unref(S),description:e.unref(l),hint:e.unref(C),placeholder:e.unref(d),value:e.unref(_),classes:f(r.error,h.error),styles:e.unref(E),optional:e.unref(b),disabled:e.unref(D),hidden:e.unref(B),readonly:e.unref(g),type:r.type,altAction:r.altAction,component:r.component,vName:r.name,field:r.field,options:e.unref(y),maxLength:r.maxLength,required:e.unref(x),autocomplete:r.autocomplete,attrs:N.value})]),_:3},8,["modelValue","rules"]))}}),U={key:0},L={key:1},$={key:0},j=["onUpdate:modelValue","onBlur","placeholder","autocomplete","name","type","disabled","readonly"],P={class:"oo-error-slot"},O={key:4},T={key:0},z=["onUpdate:modelValue","onBlur","name","disabled","readonly"],M={key:0,value:"",disabled:""},R=["value"],K={class:"oo-error-slot"},G={class:"oo-field-label"},H={key:0},I={class:"oo-radio-group"},J=["value","onUpdate:modelValue","onBlur","name","disabled","readonly"],Q={class:"oo-error-slot"},W=["onUpdate:modelValue","onBlur","name","disabled","readonly"],X={key:0},Y={class:"oo-error-slot"},Z=["onClick"],v={key:9},ee=["disabled"],te=e.defineComponent({__name:"oo-form",props:{form:{},formData:{},formContext:{},firstValidation:{},components:{},types:{},errors:{}},emits:["submit","action","unsupported-action"],setup(r,{emit:o}){const n=r,m=e.ref({}),c=e.computed(()=>n.formData||m.value),k=e.computed(()=>({v:void 0,data:c.value,context:n.formContext??{},entry:void 0})),b=e.computed(()=>V.evalComputed(n.form.title,k.value)),D=e.computed(()=>V.evalComputed(n.form.submit.text,k.value)),B=e.computed(()=>V.evalComputed(n.form.submit.disabled,k.value));function g(l){V.supportsAltAction(n.form,l)?x("action",l,c.value):x("unsupported-action",l,c.value)}const x=o;function i(l){return typeof l=="string"?l:l.key}function S(l){return typeof l=="string"?l:l.label}return(l,C)=>(e.openBlock(),e.createBlock(e.unref(F.VuilessForm),{"first-validation":r.firstValidation,onSubmit:C[0]||(C[0]=d=>x("submit",d)),"form-data":c.value,"form-context":r.formContext},{default:e.withCtx(d=>[e.renderSlot(l.$slots,"form.header",{clearErrors:d.clearErrors,reset:d.reset,title:b.value,formContext:r.formContext,disabled:B.value},()=>[b.value?(e.openBlock(),e.createElementBlock("h2",U,e.toDisplayString(b.value),1)):e.createCommentVNode("",!0)]),e.renderSlot(l.$slots,"form.before",{clearErrors:d.clearErrors,reset:d.reset}),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(n.form.fields,y=>{var E;return e.openBlock(),e.createBlock(w,e.mergeProps({key:y.field},{ref_for:!0},y,{error:(E=r.errors)==null?void 0:E[y.field]}),{default:e.withCtx(t=>[e.renderSlot(l.$slots,`field:${t.type}`,e.mergeProps({ref_for:!0},t),()=>{var f,N,_;return[y.component&&((f=n.components)!=null&&f[y.component])?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(n.components[y.component]),e.mergeProps({key:0,"on-blur":t.onBlur,error:t.error,model:t.model,"form-data":t.formData,"form-context":t.formContext,label:t.label,description:t.description,hint:t.hint,placeholder:t.placeholder,class:t.classes,style:t.styles,optional:t.optional,required:!t.required,disabled:t.disabled,hidden:t.hidden,type:t.type,"alt-action":t.altAction,name:t.vName,field:t,options:t.options,"max-length":t.maxLength,autocomplete:t.autocomplete,onAction:g},{ref_for:!0},t.attrs,{modelValue:t.model.value,"onUpdate:modelValue":s=>t.model.value=s}),null,16,["on-blur","error","model","form-data","form-context","label","description","hint","placeholder","class","style","optional","required","disabled","hidden","type","alt-action","name","field","options","max-length","autocomplete","modelValue","onUpdate:modelValue"])):y.component&&!((N=n.components)!=null&&N[y.component])?(e.openBlock(),e.createElementBlock("div",L," ["+e.toDisplayString(t.label)+'] Component "'+e.toDisplayString(t.component)+'" not supplied ',1)):(_=n.types)!=null&&_[y.type]?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(n.types[y.type]),e.mergeProps({key:2,"on-blur":t.onBlur,error:t.error,model:t.model,"form-data":t.formData,"form-context":t.formContext,label:t.label,description:t.description,hint:t.hint,placeholder:t.placeholder,class:t.classes,style:t.styles,optional:t.optional,required:!t.required,disabled:t.disabled,hidden:t.hidden,type:t.type,"alt-action":t.altAction,name:t.vName,field:t,options:t.options,"max-length":t.maxLength,autocomplete:t.autocomplete,onAction:g},{ref_for:!0},t.attrs,{modelValue:t.model.value,"onUpdate:modelValue":s=>t.model.value=s}),null,16,["on-blur","error","model","form-data","form-context","label","description","hint","placeholder","class","style","optional","required","disabled","hidden","type","alt-action","name","field","options","max-length","autocomplete","modelValue","onUpdate:modelValue"])):["text","password","number"].includes(t.type)?e.withDirectives((e.openBlock(),e.createElementBlock("div",{key:3,class:e.normalizeClass(["oo-default-field",t.classes])},[e.createElementVNode("label",null,e.toDisplayString(t.label),1),t.description?(e.openBlock(),e.createElementBlock("span",$,e.toDisplayString(t.description),1)):e.createCommentVNode("",!0),e.withDirectives(e.createElementVNode("input",e.mergeProps({"onUpdate:modelValue":s=>t.model.value=s,onBlur:t.onBlur,placeholder:t.placeholder,autocomplete:t.autocomplete,name:t.vName,type:t.type,disabled:t.disabled,readonly:t.readonly},{ref_for:!0},t.attrs),null,16,j),[[e.vModelDynamic,t.model.value]]),e.createElementVNode("div",P,e.toDisplayString(t.error||t.hint),1)],2)),[[e.vShow,!t.hidden]]):t.type==="paragraph"?(e.openBlock(),e.createElementBlock("p",O,e.toDisplayString(t.value),1)):t.type==="select"?e.withDirectives((e.openBlock(),e.createElementBlock("div",{key:5,class:e.normalizeClass(["oo-default-field",t.classes])},[e.createElementVNode("label",null,e.toDisplayString(t.label),1),t.description?(e.openBlock(),e.createElementBlock("span",T,e.toDisplayString(t.description),1)):e.createCommentVNode("",!0),e.withDirectives(e.createElementVNode("select",e.mergeProps({"onUpdate:modelValue":s=>t.model.value=s,onBlur:t.onBlur,name:t.vName,disabled:t.disabled,readonly:t.readonly},{ref_for:!0},t.attrs),[t.placeholder?(e.openBlock(),e.createElementBlock("option",M,e.toDisplayString(t.placeholder),1)):e.createCommentVNode("",!0),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.options,s=>(e.openBlock(),e.createElementBlock("option",{key:i(s),value:i(s)},e.toDisplayString(S(s)),9,R))),128))],16,z),[[e.vModelSelect,t.model.value]]),e.createElementVNode("div",K,e.toDisplayString(t.error||t.hint),1)],2)),[[e.vShow,!t.hidden]]):t.type==="radio"?e.withDirectives((e.openBlock(),e.createElementBlock("div",{key:6,class:e.normalizeClass(["oo-default-field oo-radio-field",t.classes])},[e.createElementVNode("span",G,e.toDisplayString(t.label),1),t.description?(e.openBlock(),e.createElementBlock("span",H,e.toDisplayString(t.description),1)):e.createCommentVNode("",!0),e.createElementVNode("div",I,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.options,s=>(e.openBlock(),e.createElementBlock("label",{key:i(s)},[e.withDirectives(e.createElementVNode("input",e.mergeProps({type:"radio",value:i(s),"onUpdate:modelValue":a=>t.model.value=a,onBlur:t.onBlur,name:t.vName,disabled:t.disabled,readonly:t.readonly},{ref_for:!0},t.attrs),null,16,J),[[e.vModelRadio,t.model.value]]),e.createTextVNode(" "+e.toDisplayString(S(s)),1)]))),128))]),e.createElementVNode("div",Q,e.toDisplayString(t.error||t.hint),1)],2)),[[e.vShow,!t.hidden]]):t.type==="checkbox"?e.withDirectives((e.openBlock(),e.createElementBlock("div",{key:7,class:e.normalizeClass(["oo-default-field oo-checkbox-field",t.classes])},[e.createElementVNode("label",null,[e.withDirectives(e.createElementVNode("input",e.mergeProps({type:"checkbox","onUpdate:modelValue":s=>t.model.value=s,onBlur:t.onBlur,name:t.vName,disabled:t.disabled,readonly:t.readonly},{ref_for:!0},t.attrs),null,16,W),[[e.vModelCheckbox,t.model.value]]),e.createTextVNode(" "+e.toDisplayString(t.label),1)]),t.description?(e.openBlock(),e.createElementBlock("span",X,e.toDisplayString(t.description),1)):e.createCommentVNode("",!0),e.createElementVNode("div",Y,e.toDisplayString(t.error||t.hint),1)],2)),[[e.vShow,!t.hidden]]):t.type==="action"?(e.openBlock(),e.createElementBlock("div",{key:8,class:e.normalizeClass(["oo-default-field oo-action-field",t.classes])},[e.createElementVNode("button",{type:"button",onClick:s=>g(t.altAction)},e.toDisplayString(t.label),9,Z)],2)):(e.openBlock(),e.createElementBlock("div",v," ["+e.toDisplayString(t.label)+'] Not supported field type "'+e.toDisplayString(t.type)+'" '+e.toDisplayString(t.component),1))]})]),_:2},1040,["error"])}),128)),e.renderSlot(l.$slots,"form.after",{clearErrors:d.clearErrors,reset:d.reset,disabled:B.value,formContext:r.formContext}),e.renderSlot(l.$slots,"form.submit",{disabled:B.value,text:D.value,clearErrors:d.clearErrors,reset:d.reset,formContext:r.formContext},()=>[e.createElementVNode("button",{disabled:B.value},e.toDisplayString(D.value),9,ee)]),e.renderSlot(l.$slots,"form.footer",{disabled:B.value,clearErrors:d.clearErrors,reset:d.reset,formContext:r.formContext})]),_:3},8,["first-validation","form-data","form-context"]))}});function oe(r){const o=q.createFoorm(r),n=e.reactive(V.createFormData(o.fields));return{form:o,formData:n}}p.OoField=w,p.OoForm=te,p.useFoorm=oe,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})});
|
package/dist/style.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.oo-default-field{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.oo-default-field label{font-size:13px;font-weight:600;color:#374151}.oo-default-field.required label:after{content:" *";color:#ef4444}.oo-default-field span{font-size:12px;color:#6b7280}.oo-default-field input,.oo-default-field select{padding:8px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;color:#1d1d1f;background:#fff;transition:border-color .15s,box-shadow .15s;outline:none}.oo-default-field input::placeholder{color:#9ca3af}.oo-default-field input:focus,.oo-default-field select:focus{border-color:#6366f1;box-shadow:0 0 0 3px #6366f126}.oo-default-field input:disabled,.oo-default-field select:disabled{background:#f3f4f6;color:#9ca3af;cursor:not-allowed}.oo-default-field.error input,.oo-default-field.error select{border-color:#ef4444}.oo-default-field.error input:focus,.oo-default-field.error select:focus{box-shadow:0 0 0 3px #ef444426}.oo-default-field .oo-field-label{font-size:13px;font-weight:600;color:#374151}.oo-radio-group{display:flex;flex-direction:column;gap:6px}.oo-radio-group label{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:400;color:#1d1d1f;cursor:pointer}.oo-radio-group input[type=radio]{padding:0;border:none;box-shadow:none}.oo-checkbox-field>label{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:400;color:#1d1d1f;cursor:pointer}.oo-checkbox-field>label input[type=checkbox]{padding:0;border:none;box-shadow:none}.oo-default-field .oo-error-slot{min-height:16px;line-height:16px;font-size:12px;color:#6b7280}.oo-default-field.error .oo-error-slot{color:#ef4444}.oo-default-field.oo-action-field button{padding:8px 16px;border:1px solid #d1d5db;border-radius:6px;background:#fff;font-size:13px;font-weight:500;color:#374151;cursor:pointer;transition:background .15s,border-color .15s}.oo-default-field.oo-action-field button:hover{background:#f9fafb;border-color:#9ca3af}h2{margin:0 0 8px;font-size:20px;font-weight:700;color:#111827}p{margin:0 0 4px;font-size:14px;color:#6b7280}button[type=submit],button:not([type]){margin-top:8px;padding:10px 20px;border:none;border-radius:6px;background:#6366f1;color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:background .15s}button[type=submit]:hover,button:not([type]):hover{background:#4f46e5}button[type=submit]:disabled,button:not([type]):disabled{background:#c7d2fe;cursor:not-allowed}
|