@teamnovu/kit-vue-forms 0.2.16 → 0.2.17
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/dist/index.js +1 -1
- package/docs/FormInput-example.md +29 -18
- package/docs/example.md +11 -17
- package/docs/index.md +4 -3
- package/docs/reference.md +73 -14
- package/package.json +1 -1
- package/src/composables/useFieldRegistry.ts +2 -2
- package/tests/useForm.test.ts +5 -1
package/dist/index.js
CHANGED
|
@@ -290,7 +290,7 @@ function Pe(r, t, e) {
|
|
|
290
290
|
s.set(o, i);
|
|
291
291
|
}, v = (i) => {
|
|
292
292
|
var o;
|
|
293
|
-
n
|
|
293
|
+
h((n == null ? void 0 : n.keepValuesOnUnmount) ?? !0) || (o = s.get(i)) == null || o.reset(), s.delete(i);
|
|
294
294
|
}, y = (i) => {
|
|
295
295
|
var o;
|
|
296
296
|
a.has(i) ? (o = a.get(i)) == null || o.inc() : a.set(i, new Re(() => v(i)));
|
|
@@ -16,43 +16,54 @@ In the example below we use a text field `TextField.vue`.
|
|
|
16
16
|
|
|
17
17
|
Example:
|
|
18
18
|
```vue
|
|
19
|
-
<!--
|
|
19
|
+
<!-- FormTextField.vue -->
|
|
20
20
|
<template>
|
|
21
21
|
<FormFieldWrapper
|
|
22
22
|
:component="TextField"
|
|
23
|
-
:component-props="$props"
|
|
24
|
-
:path="path"
|
|
23
|
+
:component-props="omit($props, 'form', 'path')"
|
|
25
24
|
:form="form"
|
|
26
|
-
|
|
25
|
+
:path="path"
|
|
26
|
+
>
|
|
27
|
+
<slot />
|
|
28
|
+
<!-- https://vue-land.github.io/faq/forwarding-slots#passing-all-slots -->
|
|
29
|
+
<template
|
|
30
|
+
v-for="(_, slotName) in $slots"
|
|
31
|
+
#[slotName]="slotProps"
|
|
32
|
+
>
|
|
33
|
+
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
|
34
|
+
</template>
|
|
35
|
+
</FormFieldWrapper>
|
|
27
36
|
</template>
|
|
28
37
|
|
|
29
|
-
<script setup lang="ts" generic="TData extends object, TPath extends Paths<TData
|
|
30
|
-
import
|
|
31
|
-
import
|
|
32
|
-
import {
|
|
33
|
-
import type {
|
|
34
|
-
ExcludedFieldProps,
|
|
35
|
-
FormComponentProps,
|
|
36
|
-
} from '#types/form/FormComponentProps';
|
|
38
|
+
<script setup lang="ts" generic="TData extends object, TPath extends Paths<TData>, TDataOut = TData">
|
|
39
|
+
import TextField, { type TextFieldProps } from '#components/utils/form/plainInput/TextField.vue'
|
|
40
|
+
import { FormFieldWrapper, type ExcludedFieldProps, type FormComponentProps, type Paths } from '@teamnovu/kit-vue-forms'
|
|
41
|
+
import { omit } from 'lodash-es'
|
|
37
42
|
|
|
38
|
-
export type Props<
|
|
39
|
-
|
|
43
|
+
export type Props<
|
|
44
|
+
TData extends object,
|
|
45
|
+
TPath extends Paths<TData>,
|
|
46
|
+
TDataOut = TData,
|
|
47
|
+
> = FormComponentProps<TData, TPath, TextFieldProps['modelValue'], TDataOut> & Omit<TextFieldProps, ExcludedFieldProps>
|
|
40
48
|
|
|
41
|
-
defineProps<Props<TData, TPath>>()
|
|
49
|
+
defineProps<Props<TData, TPath, TDataOut>>()
|
|
42
50
|
</script>
|
|
43
51
|
```
|
|
44
|
-
Note the usage of the generic types `TData` and `
|
|
52
|
+
Note the usage of the generic types `TData`, `TPath`, and `TDataOut` to make the component fully type-safe. With this, the `path` prop
|
|
45
53
|
will only allow valid paths of the form data. Moreover, it will throw a type error if the property at `path` of the `form`
|
|
46
54
|
has the wrong type. This is ensured by using the `FormComponentProps` type above.
|
|
47
55
|
|
|
56
|
+
The `omit($props, 'form', 'path')` ensures that only the underlying component's props are passed through.
|
|
57
|
+
The slot forwarding pattern allows slots defined on the form input to be passed to the underlying plain input component.
|
|
58
|
+
|
|
48
59
|
The usage of such a form input component is as follows (assuming the input should handle the "firstName" property of the form data):
|
|
49
60
|
```vue
|
|
50
|
-
<
|
|
61
|
+
<FormTextField
|
|
51
62
|
:form="form"
|
|
52
63
|
path="firstName"
|
|
53
64
|
/>
|
|
54
65
|
```
|
|
55
|
-
Here, `form` is the
|
|
66
|
+
Here, `form` is the form object that was created with [`useForm`](./reference.md#composable-useform). All additional props are passed
|
|
56
67
|
to the underlying plain input component, e.g. `label`, `placeholder`, etc.
|
|
57
68
|
|
|
58
69
|
It is recommended to use this pattern of a styled "plain input" that works with v-model and a "form input" to work with form
|
package/docs/example.md
CHANGED
|
@@ -20,7 +20,7 @@ We can create a form like this:
|
|
|
20
20
|
|
|
21
21
|
```vue
|
|
22
22
|
<template>
|
|
23
|
-
<form @submit
|
|
23
|
+
<form @submit="form.submitHandler(sendToBackend)">
|
|
24
24
|
<!-- Simple form inputs -->
|
|
25
25
|
<!-- the "label" prop is passed through the FormTextField to the underlying TextField -->
|
|
26
26
|
<FormTextField
|
|
@@ -56,13 +56,15 @@ We can create a form like this:
|
|
|
56
56
|
<FormAddressField :form="subform" />
|
|
57
57
|
</FormPart>
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
<!-- Using getFieldArray for dynamic lists with add/remove functionality -->
|
|
60
|
+
<div v-for="field in hobbies.items.value" :key="field.id" class="ml-4">
|
|
61
61
|
<FormTextField
|
|
62
62
|
:form="form"
|
|
63
|
-
:path="
|
|
63
|
+
:path="field.path"
|
|
64
64
|
/>
|
|
65
|
+
<button type="button" @click="hobbies.remove(field.id)">Remove</button>
|
|
65
66
|
</div>
|
|
67
|
+
<button type="button" @click="hobbies.push('')">Add Hobby</button>
|
|
66
68
|
|
|
67
69
|
<button type="button" @click="toggleComment">
|
|
68
70
|
Toggle Comment
|
|
@@ -151,20 +153,12 @@ const form = useForm<{ // this type might be inferred automatically from the ini
|
|
|
151
153
|
}),
|
|
152
154
|
});
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const submit = async () => {
|
|
159
|
-
// validate the form
|
|
160
|
-
// if the zod schema is not satisfied, isValid will be false and the errors will be set on the corresponding fields
|
|
161
|
-
// if your TextInput component handles errors correctly, it should be directly visible
|
|
162
|
-
const { isValid } = await form.validateForm();
|
|
156
|
+
// getFieldArray for managing the hobbies list
|
|
157
|
+
const hobbies = form.getFieldArray('person.hobbies');
|
|
163
158
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
159
|
+
const sendToBackend = async (data) => {
|
|
160
|
+
// send validated data object to backend
|
|
161
|
+
// note: data is the validated/transformed output type (TOut)
|
|
168
162
|
};
|
|
169
163
|
|
|
170
164
|
// To programmatically change form values, use form.getField to get a ref and a setter function
|
package/docs/index.md
CHANGED
|
@@ -16,9 +16,10 @@ but pass it down as a prop to guarantee type safety.
|
|
|
16
16
|
## Usage
|
|
17
17
|
|
|
18
18
|
1. Wrap your plain input components into `FormFieldWrapper`, see [FormInput Example](./FormInput-example.md).
|
|
19
|
-
2. Inside your parent component where you want to use a form, use the composable `useForm` to create a form object.
|
|
20
|
-
This object contains the form data, errors, and methods to define
|
|
21
|
-
|
|
19
|
+
2. Inside your parent component where you want to use a form, use the composable `useForm` to create a form object.
|
|
20
|
+
This object contains the form data, errors, and methods to define fields, validate the form, reset it,
|
|
21
|
+
create subforms for nested objects, and manage dynamic lists with `getFieldArray`.
|
|
22
|
+
Use `submitHandler` for form submission with automatic validation, see [here](./reference#composable-useform).
|
|
22
23
|
3. Pass this form object to any form input component together with a path to the property of the form data that this input
|
|
23
24
|
should manage. You can also make subforms, pass the form down to child components, etc. See the examples for inspiration.
|
|
24
25
|
4. See Reference documentation for all types and methods: [here](./reference.md).
|
package/docs/reference.md
CHANGED
|
@@ -2,35 +2,53 @@
|
|
|
2
2
|
## Composable `useForm`
|
|
3
3
|
This is the main composable which creates the form. It has the following signature:
|
|
4
4
|
```typescript
|
|
5
|
-
function useForm<T extends object>(options: {
|
|
5
|
+
function useForm<T extends object, TOut = T>(options: {
|
|
6
6
|
// the initial data of the form
|
|
7
7
|
// reactive changes to this object will propagate to the form data
|
|
8
8
|
initialData: MaybeRefOrGetter<T>
|
|
9
9
|
// an ErrorBag object or ref of an ErrorBag object with external errors
|
|
10
10
|
// this is used e.g. for server side validation errors
|
|
11
|
-
// these errors will be merged with the internal errors of the form based on
|
|
11
|
+
// these errors will be merged with the internal errors of the form based on validateFn below and/or the zod schema
|
|
12
12
|
errors?: MaybeRef<ErrorBag | undefined>
|
|
13
13
|
// a zod schema of the form data
|
|
14
|
-
// this is validated based on the
|
|
15
|
-
schema
|
|
14
|
+
// this is validated based on the validation flags or by manually triggering validateForm on the form object
|
|
15
|
+
// TOut is inferred from the schema's output type if provided
|
|
16
|
+
schema?: MaybeRef<z.ZodType<TOut>>
|
|
16
17
|
// a custom validation function which is called with the current form data
|
|
17
18
|
// this is additional to the schema for custom validations
|
|
18
|
-
validateFn?: MaybeRef<ValidationFunction<T>>
|
|
19
|
+
validateFn?: MaybeRef<ValidationFunction<T, TOut>>
|
|
19
20
|
// if the form data of a property should be reset or kept if all fields corresponding to this property are unmounted
|
|
21
|
+
// defaults to true
|
|
20
22
|
keepValuesOnUnmount?: MaybeRef<boolean>
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
// validation flags before first submit (defaults: validateOnSubmit: true, all others: false)
|
|
24
|
+
validationBeforeSubmit?: ValidationFlags
|
|
25
|
+
// validation flags after first submit (defaults: validateOnSubmit: true, validateOnDataChange: true, validateOnFieldRegister: true)
|
|
26
|
+
validationAfterSubmit?: ValidationFlags
|
|
27
|
+
}): Form<T, TOut>
|
|
24
28
|
```
|
|
25
|
-
This composable returns a `Form<T>` object.
|
|
29
|
+
This composable returns a `Form<T, TOut>` object.
|
|
26
30
|
|
|
27
|
-
## Type `
|
|
31
|
+
## Type `ValidationFlags`
|
|
32
|
+
Controls when validation is triggered:
|
|
33
|
+
```typescript
|
|
34
|
+
interface ValidationFlags {
|
|
35
|
+
validateOnBlur?: MaybeRefOrGetter<boolean>
|
|
36
|
+
validateOnFormOpen?: MaybeRefOrGetter<boolean>
|
|
37
|
+
validateOnSubmit?: MaybeRefOrGetter<boolean>
|
|
38
|
+
validateOnDataChange?: MaybeRefOrGetter<boolean>
|
|
39
|
+
validateOnFieldRegister?: MaybeRefOrGetter<boolean>
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Type `Form<T, TOut>`
|
|
28
44
|
Here, `T` is the type of the form data, which is inferred from the `initialData` property of the options object passed to `useForm`.
|
|
45
|
+
`TOut` is the output type after validation/transformation. When a zod schema is provided, `TOut` is a merge of `T` with the schema's output type,
|
|
46
|
+
where the schema type takes precedence for properties it defines, and `T` provides types for properties unknown to the schema.
|
|
29
47
|
You can also explicitly provide a type argument to `useForm<T>` if needed (useful if the initial data is an empty object `{}`).
|
|
30
48
|
|
|
31
49
|
This object has the following properties and methods:
|
|
32
50
|
```typescript
|
|
33
|
-
interface Form<T extends object> {
|
|
51
|
+
interface Form<T extends object, TOut = T> {
|
|
34
52
|
// the current working data of the form
|
|
35
53
|
// this might differ from initialData if the user has changed some values
|
|
36
54
|
data: Ref<T>
|
|
@@ -66,21 +84,35 @@ interface Form<T extends object> {
|
|
|
66
84
|
// defines a custom validator for the form
|
|
67
85
|
// with this, a subcomponent might add a validator function and/or schema to the form
|
|
68
86
|
// without needing access to the initial useForm call
|
|
69
|
-
defineValidator: <TData extends T
|
|
87
|
+
defineValidator: <TData extends T, TDataOut extends TOut>(
|
|
88
|
+
options: ValidatorOptions<TData, TDataOut> | Ref<Validator<TData, TDataOut>>
|
|
89
|
+
) => Ref<Validator<TData, TDataOut> | undefined>
|
|
70
90
|
|
|
71
91
|
// resets the form data and errors, as well as the dirty, touched etc. state of all fields
|
|
72
92
|
reset: () => void
|
|
73
93
|
// manually triggers validation of the form data based on schema and/or validateFn
|
|
74
|
-
|
|
94
|
+
// returns the validation result with errors and parsed data (if valid)
|
|
95
|
+
validateForm: () => Promise<ValidationResult<TOut>>
|
|
96
|
+
|
|
97
|
+
// creates a submit handler that validates the form before calling onSubmit
|
|
98
|
+
// onSubmit receives the validated/transformed data (TOut)
|
|
99
|
+
submitHandler: (onSubmit: (data: TOut) => Awaitable<void>) => (event: SubmitEvent) => Promise<void>
|
|
75
100
|
|
|
76
101
|
// creates a subform for a nested object or array property of the form data
|
|
77
|
-
// the subform is again a Form
|
|
102
|
+
// the subform is again a Form object, where T is the type of the nested property
|
|
78
103
|
// it will contain all errors that have paths starting with the path of the subform
|
|
79
104
|
// changes to the subform data will propagate to the main form data and vice versa
|
|
80
105
|
getSubForm: <P extends EntityPaths<T>>(
|
|
81
106
|
path: P,
|
|
82
107
|
options?: SubformOptions<PickEntity<T, P>>,
|
|
83
108
|
) => Form<PickEntity<T, P>>
|
|
109
|
+
|
|
110
|
+
// creates a field array for managing dynamic lists
|
|
111
|
+
// see the Field Arrays section below for details
|
|
112
|
+
getFieldArray: <K extends Paths<T>>(
|
|
113
|
+
path: PickProps<T, K> extends unknown[] ? K : never,
|
|
114
|
+
options?: FieldArrayOptions<PickProps<T, K> extends (infer U)[] ? U : never>,
|
|
115
|
+
) => FieldArray<PickProps<T, K> extends (infer U)[] ? U : never, K>
|
|
84
116
|
}
|
|
85
117
|
```
|
|
86
118
|
|
|
@@ -98,6 +130,33 @@ interface ErrorBag {
|
|
|
98
130
|
}
|
|
99
131
|
```
|
|
100
132
|
|
|
133
|
+
## Field Arrays
|
|
134
|
+
For managing dynamic lists (add, remove, reorder items), use `getFieldArray`. It provides stable IDs for each item,
|
|
135
|
+
which is important for efficient Vue rendering with `v-for`.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const hobbies = form.getFieldArray('person.hobbies')
|
|
139
|
+
|
|
140
|
+
// add item
|
|
141
|
+
hobbies.push('new hobby')
|
|
142
|
+
|
|
143
|
+
// remove by id
|
|
144
|
+
hobbies.remove(hobbies.items.value[0].id)
|
|
145
|
+
```
|
|
146
|
+
```vue
|
|
147
|
+
<div v-for="field in hobbies.items.value" :key="field.id">
|
|
148
|
+
<FormTextField :form="form" :path="field.path" />
|
|
149
|
+
<button @click="hobbies.remove(field.id)">Remove</button>
|
|
150
|
+
</div>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For objects where identity should be based on a property (e.g. `id`) rather than reference equality, provide a `hashFn`:
|
|
154
|
+
```typescript
|
|
155
|
+
const products = form.getFieldArray('products', {
|
|
156
|
+
hashFn: (item) => item.id
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
101
160
|
## Component `FormPart`
|
|
102
161
|
A component to define a subform part for a nested object or array property of the form data. This corresponds to the
|
|
103
162
|
`getSubForm` method of the `Form<T>` object, but in component form to be easily used in the template. An example usage is as follows:
|
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@ export function useFieldRegistry<T extends FormDataDefault, TOut = T>(
|
|
|
82
82
|
) {
|
|
83
83
|
const fieldReferenceCounter = new Map<Paths<T>, Rc>()
|
|
84
84
|
const fields = shallowReactive(new Map()) as FieldRegistryCache<T>
|
|
85
|
-
const registryOptions = {
|
|
85
|
+
const registryOptions: FieldRegistryOptions = {
|
|
86
86
|
...optionDefaults,
|
|
87
87
|
...fieldRegistryOptions,
|
|
88
88
|
}
|
|
@@ -95,7 +95,7 @@ export function useFieldRegistry<T extends FormDataDefault, TOut = T>(
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const deregisterField = (path: Paths<T>) => {
|
|
98
|
-
if (!registryOptions?.keepValuesOnUnmount) {
|
|
98
|
+
if (!unref(registryOptions?.keepValuesOnUnmount ?? true)) {
|
|
99
99
|
fields.get(path)?.reset()
|
|
100
100
|
}
|
|
101
101
|
fields.delete(path)
|
package/tests/useForm.test.ts
CHANGED
|
@@ -397,7 +397,9 @@ describe('useForm', () => {
|
|
|
397
397
|
},
|
|
398
398
|
})
|
|
399
399
|
|
|
400
|
-
effectScope()
|
|
400
|
+
const scope = effectScope()
|
|
401
|
+
|
|
402
|
+
scope.run(() => {
|
|
401
403
|
const nameField = form.defineField({ path: 'name' })
|
|
402
404
|
|
|
403
405
|
expect(form.fields.value.length).toBe(1)
|
|
@@ -406,6 +408,8 @@ describe('useForm', () => {
|
|
|
406
408
|
nameField.setData('Modified')
|
|
407
409
|
})
|
|
408
410
|
|
|
411
|
+
scope.stop()
|
|
412
|
+
|
|
409
413
|
expect(form.data.value.name).toBe('Modified')
|
|
410
414
|
})
|
|
411
415
|
|