@whatsaaaa/form-builder-vue 0.3.0 → 0.3.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.
Files changed (2) hide show
  1. package/README.md +30 -322
  2. package/package.json +5 -3
package/README.md CHANGED
@@ -1,20 +1,26 @@
1
1
  # @whatsaaaa/form-builder-vue
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@whatsaaaa/form-builder-vue)](https://www.npmjs.com/package/@whatsaaaa/form-builder-vue)
4
-
5
3
  Render JSON form definitions with [Formisch](https://formisch.dev) and your own Vue components.
6
4
 
7
- It's headless: it reads the definition, connects every field to Formisch and turns input into the values the schema expects, but it renders nothing of its own. You give it a component per field type, so it works with plain HTML, Nuxt UI or any other UI library.
5
+ [![npm version](https://img.shields.io/npm/v/@whatsaaaa/form-builder-vue)](https://www.npmjs.com/package/@whatsaaaa/form-builder-vue)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@whatsaaaa/form-builder-vue)](https://www.npmjs.com/package/@whatsaaaa/form-builder-vue)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/whatsaaaa/form-builder/ci.yml?branch=main&label=CI)](https://github.com/whatsaaaa/form-builder/actions/workflows/ci.yml)
8
+ [![Coverage](https://codecov.io/gh/whatsaaaa/form-builder/graph/badge.svg?flag=vue)](https://codecov.io/gh/whatsaaaa/form-builder)
9
+ [![Vue](https://img.shields.io/npm/dependency-version/@whatsaaaa/form-builder-vue/peer/vue?logo=vuedotjs)](https://vuejs.org)
10
+ [![Types](https://img.shields.io/npm/types/@whatsaaaa/form-builder-vue)](https://www.npmjs.com/package/@whatsaaaa/form-builder-vue)
11
+ [![License](https://img.shields.io/npm/l/@whatsaaaa/form-builder-vue)](https://github.com/whatsaaaa/form-builder/blob/main/LICENSE)
12
+
13
+ This is the Vue adapter of [form-builder](https://github.com/whatsaaaa/form-builder). It is headless: it reads the definition, connects every field to Formisch and turns input into the values the schema expects, but renders nothing of its own. You give it a component per field type, so it works with plain HTML, Nuxt UI or any other UI library.
8
14
 
9
15
  ## Install
10
16
 
11
17
  ```sh
12
- pnpm add @whatsaaaa/form-builder-vue @formisch/vue valibot
18
+ pnpm add @whatsaaaa/form-builder-vue @whatsaaaa/form-builder-schema @formisch/vue valibot
13
19
  ```
14
20
 
15
- Vue 3.5, `@formisch/vue` 1.1 and Valibot 1.3 or later are required. `@whatsaaaa/form-builder-schema` comes with it.
21
+ Vue 3.5, `@formisch/vue` 1.1 and Valibot 1.3 or later are required. Install `@whatsaaaa/form-builder-schema` too: the examples import its types, such as `JsonForm`.
16
22
 
17
- ## Usage
23
+ ## Example
18
24
 
19
25
  ```vue
20
26
  <script setup lang="ts">
@@ -40,58 +46,7 @@ function save(output: unknown) {
40
46
  </template>
41
47
  ```
42
48
 
43
- - `useJsonForm(definition, options?)` builds the schema and the initial input, and creates the Formisch form. `jsonForm.form` is a normal Formisch form, so every Formisch method works on it.
44
- - `<JsonFields>` renders the fields inside your `<Form>`. The form element, the submit button and anything around them are yours.
45
- - A definition with issues throws the builder's `DefinitionError`. Run `validate()` first when the definition comes from outside your code.
46
- - The definition is read once. To show another one, give the component that calls `useJsonForm` a new `:key`.
47
-
48
- ### Options
49
-
50
- ```ts
51
- useJsonForm(definition, {
52
- builder: createSchemaBuilder({ fieldTypes: { money } }), // your own types, rules and messages
53
- components, // your component per field type, see below
54
- layoutComponents, // your component per layout type, see Layout
55
- validate: 'blur', // when a field is validated the first time, as in Formisch
56
- revalidate: 'input', // when it is validated again
57
- })
58
- ```
59
-
60
- ## Components
61
-
62
- `components` has one component per field type, by type name. Pass it to `useJsonForm`, or to `<JsonFields>` and `<JsonField>` as a prop, which wins. Several types can share one:
63
-
64
- ```ts
65
- import type { FieldComponents } from '@whatsaaaa/form-builder-vue'
66
-
67
- export const components: FieldComponents = {
68
- text: TextInput,
69
- email: TextInput,
70
- number: TextInput,
71
- select: SelectInput,
72
- group: Fieldset,
73
- array: Repeater,
74
- heading: Heading,
75
- // …one for every type your definitions use
76
- }
77
- ```
78
-
79
- A type that a definition uses but `components` doesn't have throws a `TypeError` that names every missing type, when `<JsonFields>` or `<JsonField>` sets up. Only the fields it renders count.
80
-
81
- What a component gets depends on the field's role:
82
-
83
- | Role | Which fields | Props | Slot |
84
- | ------- | ------------------------------ | -------------------------------------------------------------------------------- | -------------------------------------------- |
85
- | value | every field with a value | `field`, `path`, `kind`, `required`, `input`, `errors`, `inputProps`, `setInput` | — |
86
- | group | a container of kind `object` | `field`, `path` | default: the group's fields |
87
- | array | a container of kind `array` | `field`, `path`, `items`, `errors`, `add`, `remove`, `move` | default, with `{ index }`: one item's fields |
88
- | display | a display element, kind `none` | `field` | — |
89
-
90
- The props are typed as `ValueFieldProps`, `GroupFieldProps`, `ArrayFieldProps` and `DisplayFieldProps`, and the slots as `GroupFieldSlots` and `ArrayFieldSlots`. `defineProps<ValueFieldProps>()` works in `<script setup>`.
91
-
92
- Your own container types render through the group or array role too, by their kind.
93
-
94
- ### A value field
49
+ `components` maps each field type to a component of yours. A text input looks like this:
95
50
 
96
51
  ```vue
97
52
  <script setup lang="ts">
@@ -115,275 +70,28 @@ defineProps<ValueFieldProps>()
115
70
  </template>
116
71
  ```
117
72
 
118
- - `field` is the field from the definition, with its `label`, `placeholder`, `options` and `ui`.
119
- - `path` is its Formisch path with real indexes, like `['lines', 0, 'quantity']`.
120
- - `inputProps` are Formisch's element props: the field's name, a ref, and focus, change and blur handlers. Bind them to the input element, so Formisch knows when the field is touched and can focus it after a failed submit.
121
- - `required` is true when the definition makes the field required.
122
-
123
- ### setInput
124
-
125
- `setInput` sets the field's input after turning what an input element gives into what the schema expects, by the field's kind:
126
-
127
- | Kind | Input | Value set |
128
- | ------------------- | ----------------------- | ------------------------------ |
129
- | `number` | `''` or only spaces | `undefined` |
130
- | `number` | a numeric string, `'5'` | `5` |
131
- | `file` | a `FileList` | its first file, or `undefined` |
132
- | `array` of `file` | a `FileList` | an array of its files |
133
- | `array` of `number` | numeric strings | numbers |
134
-
135
- Anything else is set as it is, and the schema reports it. For a choice, like a radio or a select, set the option's value from `field.options` rather than the element's value: that keeps `2` a number and `true` a boolean.
136
-
137
- ### A group
138
-
139
- ```vue
140
- <script setup lang="ts">
141
- import type { GroupFieldProps, GroupFieldSlots } from '@whatsaaaa/form-builder-vue'
142
-
143
- defineProps<GroupFieldProps>()
144
- defineSlots<GroupFieldSlots>()
145
- </script>
146
-
147
- <template>
148
- <fieldset>
149
- <legend>{{ field.label }}</legend>
150
- <slot />
151
- </fieldset>
152
- </template>
153
- ```
154
-
155
- ### An array
156
-
157
- ```vue
158
- <script setup lang="ts">
159
- import type { ArrayFieldProps, ArrayFieldSlots } from '@whatsaaaa/form-builder-vue'
160
-
161
- defineProps<ArrayFieldProps>()
162
- defineSlots<ArrayFieldSlots>()
163
- </script>
164
-
165
- <template>
166
- <fieldset>
167
- <legend>{{ field.label }}</legend>
168
- <div v-for="(item, index) in items" :key="item">
169
- <slot :index="index" />
170
- <button type="button" @click="remove(index)">Remove</button>
171
- </div>
172
- <button type="button" @click="add()">Add</button>
173
- <small v-if="errors">{{ errors[0] }}</small>
174
- </fieldset>
175
- </template>
176
- ```
177
-
178
- - `items` has one id per item. Use it as the `key`, so each item keeps its inputs when one is removed or moved.
179
- - `add()` adds an item at the end, with the defaults its fields have in the definition.
180
- - `errors` are the array's own messages, like a required array's or `maxLength`'s. Its items' fields show their own.
181
-
182
- ### A display element
183
-
184
- ```vue
185
- <script setup lang="ts">
186
- import type { DisplayFieldProps } from '@whatsaaaa/form-builder-vue'
187
-
188
- defineProps<DisplayFieldProps>()
189
- </script>
190
-
191
- <template>
192
- <h2>{{ field.text }}</h2>
193
- </template>
194
- ```
195
-
196
- Declare the props even when a component doesn't use them, like a divider, or Vue adds `field` to its element as an attribute.
197
-
198
- ## Layout
199
-
200
- A definition's [`layout`](https://github.com/whatsaaaa/form-builder/tree/main/packages/schema#layout) puts its fields in sections, rows of a 12-column grid and steps, and leaves slots for content of yours. `<JsonFields>` renders the current step of it. Give `useJsonForm` a component for each layout type the definition uses, and `<JsonFields>` a named slot for each of its slots:
201
-
202
- ```vue
203
- <!-- Row.vue: one line of a 12-column grid. On a narrow screen, give each cell the whole row. -->
204
- <script setup lang="ts">
205
- import type { RowProps, RowSlots } from '@whatsaaaa/form-builder-vue'
206
-
207
- defineProps<RowProps>()
208
- defineSlots<RowSlots>()
209
- </script>
210
-
211
- <template>
212
- <div class="row">
213
- <div v-for="(span, index) in spans" :key="index" :style="{ gridColumn: `span ${span}` }">
214
- <slot :index="index" />
215
- </div>
216
- </div>
217
- </template>
218
-
219
- <style scoped>
220
- .row {
221
- display: grid;
222
- grid-template-columns: repeat(12, 1fr);
223
- gap: 1rem;
224
- }
225
- </style>
226
- ```
227
-
228
- ```vue
229
- <!-- Section.vue -->
230
- <script setup lang="ts">
231
- import type { SectionProps, SectionSlots } from '@whatsaaaa/form-builder-vue'
73
+ `jsonForm.form` is a normal Formisch form, so every Formisch method works on it.
232
74
 
233
- defineProps<SectionProps>()
234
- defineSlots<SectionSlots>()
235
- </script>
75
+ ## Highlights
236
76
 
237
- <template>
238
- <fieldset>
239
- <legend>{{ section.label }}</legend>
240
- <slot />
241
- </fieldset>
242
- </template>
243
- ```
77
+ - Headless: one component of yours per field type, and per layout type
78
+ - `setInput` turns what an element gives into the value the schema expects
79
+ - Groups, arrays and display elements, with stable keys for array items
80
+ - Sections, 12-column rows, slots and multi-step forms from the definition's layout
81
+ - Steps that validate on Next and only show errors the user has seen
82
+ - Conditions from the affects package, with hidden steps skipped by Next and Back
83
+ - Fields placed in your own markup with `<JsonField>`, `only` and `except`
244
84
 
245
- ```vue
246
- <script setup lang="ts">
247
- const jsonForm = useJsonForm(definition, {
248
- components,
249
- layoutComponents: { section: Section, row: Row },
250
- })
251
- </script>
85
+ ## Documentation
252
86
 
253
- <template>
254
- <Form :of="jsonForm.form" @submit="save">
255
- <JsonFields :of="jsonForm">
256
- <template #submit><button type="submit">Place order</button></template>
257
- </JsonFields>
258
- </Form>
259
- </template>
260
- ```
261
-
262
- | Layout type | Props | Slot |
263
- | ----------- | ------------------------------------------------------------------------- | ---------------------------------------------- |
264
- | `section` | `section`, as written, with its `id`, `label`, `description` and `ui` | default: the section's items |
265
- | `row` | `row`, as written, with its `id` and `ui`; `spans`, the span of each cell | default, with `{ index }`: the cell at `index` |
266
-
267
- The props are typed as `SectionProps` and `RowProps`, and the slots as `SectionSlots` and `RowSlots`.
268
-
269
- - `layoutComponents` goes to `useJsonForm`, or to `<JsonFields>` as a prop, which wins. A definition without sections or rows needs none, and one without a `layout` renders as it always did.
270
- - A layout type the layout uses without a component, or a slot without content, throws a `TypeError` naming each one when `<JsonFields>` sets up, on every step.
271
- - Display elements written in the layout render with your field components, like the ones in `fields`.
272
- - `except` leaves fields out of the layout too, and a section or a row left without fields drops out. The other cells of a row keep their spans. `only` renders the fields it names without the layout.
273
- - The spans are for a wide screen: the layout says nothing about narrow ones, so your row component decides, e.g. with a media query.
87
+ The [guide](https://github.com/whatsaaaa/form-builder/blob/main/frameworks/vue/docs/guide.md) covers options, components for every role, layout, steps, conditions, placing fields yourself and typing the form.
274
88
 
275
- ### Steps
276
-
277
- `jsonForm.steps` is where the form is, and how to move. It is reactive, so templates follow it:
278
-
279
- ```vue
280
- <template>
281
- <Form :of="jsonForm.form" @submit="save">
282
- <ol>
283
- <li
284
- v-for="(step, i) in jsonForm.steps.list"
285
- :key="step.step.id ?? i"
286
- :aria-current="i === jsonForm.steps.index ? 'step' : undefined"
287
- >
288
- {{ step.step.label }}
289
- </li>
290
- </ol>
291
- <JsonFields :of="jsonForm" />
292
- <button v-if="!jsonForm.steps.isFirst" type="button" @click="jsonForm.steps.back()">
293
- Back
294
- </button>
295
- <button v-if="!jsonForm.steps.isLast" type="button" @click="jsonForm.steps.next()">Next</button>
296
- <button v-else type="submit">Sign up</button>
297
- </Form>
298
- </template>
299
- ```
300
-
301
- | Member | |
302
- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
303
- | `list` | the steps on show, each with its `step` as written (`id`, `label`, `description`, `ui`), `children` and `fields`: a step that [conditions](#conditions) hide is left out, unless the user is on it |
304
- | `index`, `current` | the step `<JsonFields>` renders |
305
- | `isFirst`, `isLast` | |
306
- | `next()` | validates, and moves on when the step's fields have no issues. Resolves to whether it moved |
307
- | `back()` | moves back, without validating |
308
- | `goTo(index)` | back freely; forward through each step on the way, stopping at the first with issues |
309
-
310
- The form is one Formisch form, so Formisch validates all of it at once. The steps make that work:
311
-
312
- - **Next** validates the whole form, and only the issues on the current step's fields keep the user there. It focuses the first one, in layout order.
313
- - **A step shows what the user left**: the errors it had when they left it, and none the first time they get to it, so the issues of steps nobody saw yet stay out of sight.
314
- - **A step that failed Next revalidates** as the user fixes it, on the `revalidate` event, like a form after a failed submit.
315
- - **A submit before the last step is a Next**, so Enter in a step with a single text input doesn't submit the form early. On the last step, Formisch submits as usual.
316
- - **A submit that fails because of an earlier step**, like a form rule that reads fields of two steps, goes back to that step.
317
- - `<JsonFields :step="2" />` renders a given step instead of the current one, e.g. on a review page. A form without steps has one, and nothing changes for it.
318
-
319
- ## Conditions
320
-
321
- With the [affects package](https://github.com/whatsaaaa/form-builder/tree/main/packages/affects), fields, sections and steps show and hide by the values of other fields. Pass `useJsonForm` a builder with the extension:
322
-
323
- ```ts
324
- import { affects } from '@whatsaaaa/form-builder-affects'
325
-
326
- const builder = createSchemaBuilder({ extensions: [affects()] })
327
- const jsonForm = useJsonForm(definition, { builder, components, layoutComponents })
328
- ```
329
-
330
- - **`<JsonFields>` and `<JsonField>` leave out what the conditions hide**: a hidden field, a hidden section, and a group, section or row whose fields are all hidden. A field hidden in a row drops out of it, and the others keep their spans.
331
- - **Steps skip the hidden ones.** `steps.list` holds the steps on show, so a step whose fields are all hidden is left out, and Next and Back go past it. The step the user is on stays, until they leave it.
332
- - **`useJsonForm` clears what `hideAndClear` clears**, with Formisch's `setInput`, as soon as it is cleared, wherever the field renders: in `<JsonFields>`, with `<JsonField>`, or by your own code. A field that starts hidden this way starts blank.
333
- - **The schema skips hidden fields**, so they never block the form, and submits what the affects package says.
334
- - **`jsonForm.visibility`** is the state for the form's input now, for your own renderers: `isHidden(node, indexes)`, `isStepHidden(step)`, `shownNodes(nodes, indexes)`. It is a new object only when what is hidden changes.
335
-
336
- Without conditions, nothing is hidden, and nothing changes.
337
-
338
- ## Placing fields yourself
339
-
340
- Without a `layout`, `<JsonFields>` renders every field in the order of the definition. To put some of them somewhere else, in code rather than in the definition, place them with `<JsonField>` and leave them out of `<JsonFields>` with `except`:
341
-
342
- ```vue
343
- <template>
344
- <Form :of="jsonForm.form" @submit="save">
345
- <main>
346
- <JsonFields :of="jsonForm" :except="['coupon', 'acceptTerms']" />
347
- </main>
348
- <aside>
349
- <JsonField :of="jsonForm" path="coupon" />
350
- <JsonField :of="jsonForm" path="acceptTerms" />
351
- <button type="submit">Place order</button>
352
- </aside>
353
- </Form>
354
- </template>
355
- ```
356
-
357
- - A path is the field's names joined with dots: `'email'`, or `'address.city'` for a field of a group. A placed field validates, focuses and submits like any other, because it is still part of the one Formisch form.
358
- - A group or an array comes with all its fields. A field of an array's items can't be placed on its own, because it has no index outside its item, so place the array. To leave such a field out of every item, use `'*'` for the index: `except: ['lines.*.note']`.
359
- - `only` renders just the fields it names, in the order of the definition: `<JsonFields :of="jsonForm" :only="['name', 'email']" />`. Display elements have no dot path, even with a name, so `only` leaves them out.
360
- - A path that names no field throws a `TypeError`, so a typo shows up at once. `only` and `except` are read when the component sets up.
361
-
362
- ## Typing the form
363
-
364
- The definition is data, so TypeScript doesn't know its fields, and `jsonForm.form` is typed loosely: Formisch's own functions accept only top-level paths on it. Give `useJsonForm` your input type, and your output type when it differs, and they check paths and values as in any Formisch form:
365
-
366
- ```ts
367
- import { getInput, useFieldArray } from '@formisch/vue'
368
-
369
- interface CheckoutInput {
370
- name: string
371
- address: { city: string }
372
- lines: { product: string; quantity: number }[]
373
- }
374
-
375
- const jsonForm = useJsonForm<CheckoutInput>(definition, { components })
376
-
377
- getInput(jsonForm.form, { path: ['address', 'city'] }) // string | undefined
378
- useFieldArray(jsonForm.form, { path: ['lines'] })
379
- ```
89
+ ## Related packages
380
90
 
381
- - Formisch's `<Field>`, `<FieldArray>`, `useField`, `getInput` and `setInput` work on `jsonForm.form` either way. With a type, nested paths type-check, their values are typed, and so is the output your submit handler gets.
382
- - The type is a promise you make: nothing checks it against the definition. Keep the two in step.
383
- - A field you render with Formisch's `<Field>` gets none of the adapter's help, like `setInput` turning `'5'` into `5`, or the defaults of a new array item. Leave it out of `<JsonFields>` with `except`.
91
+ - [`@whatsaaaa/form-builder-schema`](https://github.com/whatsaaaa/form-builder/tree/main/packages/schema): build Formisch-ready Valibot schemas from JSON definitions
92
+ - [`@whatsaaaa/form-builder-affects`](https://github.com/whatsaaaa/form-builder/tree/main/packages/affects): show, hide and clear fields by conditions
93
+ - [`@whatsaaaa/form-builder-react`](https://github.com/whatsaaaa/form-builder/tree/main/frameworks/react): the same adapter for React
384
94
 
385
- ## Not included
95
+ ## License
386
96
 
387
- - Labels and display text reach your components as written in the definition. The builder's `resolveMessage` only changes what goes into the schema, so translate `field.label` in your components.
388
- - `ui.widget` doesn't pick a component. A component can read `field.ui` itself, e.g. to show a multiselect as checkboxes.
389
- - Showing and hiding fields, or making one required because of another, belongs to a future affects package. The layout already hides a section, a row or a step whose fields are all hidden, through `shownNodes` in the schema package.
97
+ [MIT](https://github.com/whatsaaaa/form-builder/blob/main/LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whatsaaaa/form-builder-vue",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Render JSON form definitions with Formisch and your own Vue components.",
5
5
  "keywords": [
6
6
  "form",
@@ -33,12 +33,13 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@whatsaaaa/form-builder-schema": "^0.15.0"
36
+ "@whatsaaaa/form-builder-schema": "^0.15.1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@arethetypeswrong/cli": "^0.18.5",
40
40
  "@formisch/vue": "^1.1.1",
41
- "@whatsaaaa/form-builder-affects": "^0.1.0",
41
+ "@vitest/coverage-v8": "^5.0.5",
42
+ "@whatsaaaa/form-builder-affects": "^0.1.1",
42
43
  "happy-dom": "^20.0.0",
43
44
  "publint": "^0.3.25",
44
45
  "tsdown": "^0.23.2",
@@ -55,6 +56,7 @@
55
56
  "scripts": {
56
57
  "build": "tsdown",
57
58
  "test": "vitest run",
59
+ "test:coverage": "vitest run --coverage",
58
60
  "type-check": "tsc --noEmit",
59
61
  "lint:pkg": "publint && pnpm pack --pack-destination .attw && attw .attw/*.tgz --profile esm-only"
60
62
  }