Skip to content

fix(vue-form): losing reactivity #1371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 14 additions & 18 deletions docs/framework/vue/guides/arrays.md
Original file line number Diff line number Diff line change
@@ -14,24 +14,24 @@ with [`Index` from `solid-js`](https://www.solidjs.com/tutorial/flow_index):
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
const form = useForm(() => ({
defaultValues: {
people: [] as Array<{ age: number; name: string }>,
},
onSubmit: ({ value }) => alert(JSON.stringify(value)),
})
}))
</script>
<template>
<form.Field name="people">
<template v-slot="{ field, state }">
<template v-slot="{ field, value }">
<div>
<form.Field
v-for="(_, i) of field.state.value"
v-for="(_, i) of value()"
:key="i"
:name="`people[${i}].name`"
>
<template v-slot="{ field: subField, state }">
<template v-slot="{ field: subField, meta }">
<!-- ... -->
</template>
</form.Field>
@@ -52,17 +52,13 @@ This will generate the mapped slot every time you run `pushValue` on `field`:
Finally, you can use a subfield like so:

```vue
<form.Field
v-for="(_, i) of field.state.value"
:key="i"
:name="`people[${i}].name`"
>
<template v-slot="{ field: subField, state }">
<form.Field v-for="(_, i) of value()" :key="i" :name="`people[${i}].name`">
<template v-slot="{ field: subField, value }">
<div>
<label>
<div>Name for person {{ i }}</div>
<input
:value="subField.state.value"
:value="value()"
@input="
(e) =>
subField.handleChange(
@@ -82,12 +78,12 @@ Finally, you can use a subfield like so:
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
const form = useForm(() => ({
defaultValues: {
people: [] as Array<{ age: number; name: string }>,
},
onSubmit: ({ value }) => alert(JSON.stringify(value)),
})
}))
</script>
<template>
@@ -102,19 +98,19 @@ const form = useForm({
>
<div>
<form.Field name="people">
<template v-slot="{ field, state }">
<template v-slot="{ value }">
<div>
<form.Field
v-for="(_, i) of field.state.value"
v-for="(_, i) of value()"
:key="i"
:name="`people[${i}].name`"
>
<template v-slot="{ field: subField, state }">
<template v-slot="{ field: subField, value }">
<div>
<label>
<div>Name for person {{ i }}</div>
<input
:value="subField.state.value"
:value="value()"
@input="
(e) =>
subField.handleChange(
17 changes: 6 additions & 11 deletions docs/framework/vue/guides/async-initial-values.md
Original file line number Diff line number Diff line change
@@ -31,21 +31,16 @@ const { data, isLoading } = useQuery({
},
})
const firstName = computed(() => data.value?.firstName || '')
const lastName = computed(() => data.value?.lastName || '')
const defaultValues = reactive({
firstName,
lastName,
})
const form = useForm({
defaultValues,
const form = useForm(() => ({
defaultValues: {
firstName: data.value?.firstName || '',
lastName: data.value?.lastName || '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})
}))
</script>
<template>
48 changes: 24 additions & 24 deletions docs/framework/vue/guides/basic-concepts.md
Original file line number Diff line number Diff line change
@@ -26,19 +26,19 @@ const formOpts = formOptions({
A Form Instance is an object that represents an individual form and provides methods and properties for working with the form. You create a form instance using the `useForm` function. The function accepts an object with an `onSubmit` function, which is called when the form is submitted.

```js
const form = useForm({
const form = useForm(() => ({
...formOpts,
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})
}))
```

You may also create a form instance without using `formOptions` by using the standalone `useForm` API:

```ts
const form = useForm({
const form = useForm(() => ({
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
@@ -48,7 +48,7 @@ const form = useForm({
lastName: '',
hobbies: [],
} as Person,
})
}))
```

## Field
@@ -61,10 +61,10 @@ Example:
<template>
<!-- ... -->
<form.Field name="fullName">
<template v-slot="{ field }">
<template v-slot="{ field, value }">
<input
:name="field.name"
:value="field.state.value"
:value="value()"
@blur="field.handleBlur"
@input="(e) => field.handleChange(e.target.value)"
/>
@@ -102,10 +102,10 @@ The Field API is an object provided by a scoped slot using the `v-slot` directiv
Example:

```vue
<template v-slot="{ field }">
<template v-slot="{ field, value }">
<input
:name="field.name"
:value="field.state.value"
:value="value()"
@blur="field.handleBlur"
@input="(e) => field.handleChange(e.target.value)"
/>
@@ -136,13 +136,13 @@ Example:
},
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<input
:value="field.state.value"
:value="value()"
@input="(e) => field.handleChange(e.target.value)"
@blur="field.handleBlur"
/>
<FieldInfo :field="field" />
<FieldInfo :meta="meta()" />
</template>
</form.Field>
<!-- ... -->
@@ -166,9 +166,9 @@ Supported libraries include:
import { useForm } from '@tanstack/vue-form'
import { z } from 'zod'
const form = useForm({
const form = useForm(() => ({
// ...
})
}))
const onChangeFirstName = z.string().refine(
async (value) => {
@@ -191,16 +191,16 @@ const onChangeFirstName = z.string().refine(
onChangeAsync: onChangeFirstName,
}"
>
<template v-slot="{ field, state }">
<template v-slot="{ field, value, meta }">
<label :htmlFor="field.name">First Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
@input="(e) => field.handleChange((e.target as HTMLInputElement).value)"
@blur="field.handleBlur"
/>
<FieldInfo :state="state" />
<FieldInfo :meta="meta()" />
</template>
</form.Field>
<!-- ... -->
@@ -249,9 +249,9 @@ Example:
},
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value }">
<input
:value="field.state.value"
:value="value()"
@input="(e) => field.handleChange(e.target.value)"
/>
</template>
@@ -290,13 +290,13 @@ Example:
<div v-else>
<div v-for="(_, i) in hobbiesField.state.value" :key="i">
<form.Field :name="`hobbies[${i}].name`">
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<div>
<label :for="field.name">Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
@blur="field.handleBlur"
@input="(e) => field.handleChange(e.target.value)"
/>
@@ -306,22 +306,22 @@ Example:
>
X
</button>
<FieldInfo :field="field" />
<FieldInfo :meta="meta()" />
</div>
</template>
</form.Field>
<form.Field :name="`hobbies[${i}].description`">
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<div>
<label :for="field.name">Description:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
@blur="field.handleBlur"
@input="(e) => field.handleChange(e.target.value)"
/>
<FieldInfo :field="field" />
<FieldInfo :meta="meta()" />
</div>
</template>
</form.Field>
14 changes: 7 additions & 7 deletions docs/framework/vue/guides/linked-fields.md
Original file line number Diff line number Diff line change
@@ -23,24 +23,24 @@ To do this, you can add a `onChangeListenTo` property to the `confirm_password`
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
const form = useForm(() => ({
defaultValues: {
password: '',
confirm_password: '',
},
// ...
})
}))
</script>
<template>
<div>
<form @submit.prevent.stop="form.handleSubmit">
<div>
<form.Field name="password">
<template v-slot="{ field }">
<template v-slot="{ field, value }">
<div>Password:</div>
<input
:value="field.state.value"
:value="value()"
@input="
(e) => field.handleChange((e.target as HTMLInputElement).value)
"
@@ -59,15 +59,15 @@ const form = useForm({
},
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<div>Confirm Password:</div>
<input
:value="field.state.value"
:value="value()"
@input="
(e) => field.handleChange((e.target as HTMLInputElement).value)
"
/>
<div v-for="(err, index) in field.state.meta.errors" :key="index">
<div v-for="(err, index) in meta().errors" :key="index">
{{ err }}
</div>
</template>
12 changes: 6 additions & 6 deletions docs/framework/vue/guides/listeners.md
Original file line number Diff line number Diff line change
@@ -24,13 +24,13 @@ Events that can be "listened" to are:
<script setup>
import { useForm } from '@tanstack/vue-form'
const form = useForm({
const form = useForm(() => ({
defaultValues: {
country: '',
province: '',
},
// ...
})
}))
</script>
<template>
@@ -44,18 +44,18 @@ const form = useForm({
},
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value }">
<input
:value="field.state.value"
:value="value()"
@input="(e) => field.handleChange(e.target.value)"
/>
</template>
</form.Field>
<form.Field name="province">
<template v-slot="{ field }">
<template v-slot="{ field, value }">
<input
:value="field.state.value"
:value="value()"
@input="(e) => field.handleChange(e.target.value)"
/>
</template>
66 changes: 27 additions & 39 deletions docs/framework/vue/guides/validation.md
Original file line number Diff line number Diff line change
@@ -25,19 +25,17 @@ Here is an example:
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
type="number"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="field.state.meta.errors">{{
field.state.meta.errors.join(', ')
}}</em>
<em role="alert" v-if="meta().errors">{{ meta().errors.join(', ') }}</em>
</template>
</form.Field>
<!-- ... -->
@@ -56,22 +54,20 @@ In the example above, the validation is done at each keystroke (`onChange`). If,
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<label :for="field.name">Age:</label>
<!-- We always need to implement onChange, so that TanStack Form receives the changes -->
<!-- Listen to the onBlur event on the field -->
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
type="number"
@blur="field.handleBlur"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="field.state.meta.errors">{{
field.state.meta.errors.join(', ')
}}</em>
<em role="alert" v-if="meta().errors">{{ meta().errors.join(', ') }}</em>
</template>
</form.Field>
<!-- ... -->
@@ -91,22 +87,20 @@ So you can control when the validation is done by implementing the desired callb
onBlur: ({ value }) => (value < 0 ? 'Invalid value' : undefined),
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<label :for="field.name">Age:</label>
<!-- We always need to implement onChange, so that TanStack Form receives the changes -->
<!-- Listen to the onBlur event on the field -->
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
type="number"
@blur="field.handleBlur"
@input="(e) => field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="field.state.meta.errors">{{
field.state.meta.errors.join(', ')
}}</em>
<em role="alert" v-if="meta().errors">{{ meta().errors.join(', ') }}</em>
</template>
</form.Field>
<!-- ... -->
@@ -129,11 +123,9 @@ Once you have your validation in place, you can map the errors from an array to
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, meta }">
<!-- ... -->
<em role="alert" v-if="field.state.meta.errors">{{
field.state.meta.errors.join(', ')
}}</em>
<em role="alert" v-if="meta().errors">{{ meta().errors.join(', ') }}</em>
</template>
</form.Field>
<!-- ... -->
@@ -152,10 +144,10 @@ Or use the `errorMap` property to access the specific error you're looking for:
value < 13 ? 'You must be 13 to make an account' : undefined,
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, meta }">
<!-- ... -->
<em role="alert" v-if="field.state.meta.errorMap['onChange']">{{
field.state.meta.errorMap['onChange']
<em role="alert" v-if="meta().errorMap['onChange']">{{
meta().errorMap['onChange']
}}</em>
</template>
</form.Field>
@@ -172,11 +164,11 @@ It's worth mentioning that our `errors` array and the `errorMap` matches the typ
onChange: ({ value }) => (value < 13 ? { isOldEnough: false } : undefined),
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, meta }">
<!-- ... -->
<!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
<!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
<em v-if="!field.state.meta.errorMap['onChange']?.isOldEnough">The user is not old enough</em>
<!-- meta().errors is type `Array<{isOldEnough: false} | undefined>` -->
<em v-if="!meta().errorMap['onChange']?.isOldEnough">The user is not old enough</em>
</template>
</form.Field>
```
@@ -191,7 +183,7 @@ Example:
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
const form = useForm({
const form = useForm(() => ({
defaultValues: {
age: 0,
},
@@ -207,7 +199,7 @@ const form = useForm({
return undefined
},
},
})
}))
// Subscribe to the form's error map so that updates to it will render
// alternately, you can use `form.Subscribe`
@@ -249,21 +241,19 @@ const onChangeAge = async ({ value }) => {
onChangeAsync: onChangeAge,
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
type="number"
@input="
(e) =>
field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="field.state.meta.errors">{{
field.state.meta.errors.join(', ')
}}</em>
<em role="alert" v-if="meta().errors">{{ meta().errors.join(', ') }}</em>
</template>
</form.Field>
<!-- ... -->
@@ -293,22 +283,20 @@ const onBlurAgeAsync = async ({ value }) => {
onBlurAsync: onBlurAgeAsync,
}"
>
<template v-slot="{ field }">
<template v-slot="{ field, value, meta }">
<label :for="field.name">Age:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
:value="value()"
type="number"
@blur="field.handleBlur"
@input="
(e) =>
field.handleChange((e.target as HTMLInputElement).valueAsNumber)
"
/>
<em role="alert" v-if="field.state.meta.errors">{{
field.state.meta.errors.join(', ')
}}</em>
<em role="alert" v-if="meta().errors">{{ meta.errors.join(', ') }}</em>
</template>
</form.Field>
<!-- ... -->
@@ -392,9 +380,9 @@ To use schemas from these libraries you can pass them to the `validators` props
import { z } from 'zod'
// ...
const form = useForm({
const form = useForm(() => ({
// ...
})
}))
</script>
<template>
6 changes: 3 additions & 3 deletions docs/framework/vue/quick-start.md
Original file line number Diff line number Diff line change
@@ -10,23 +10,23 @@ The bare minimum to get started with TanStack Form is to create a form and add a
<script setup>
import { useForm } from '@tanstack/vue-form'
const form = useForm({
const form = useForm(() => ({
defaultValues: {
fullName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})
}))
</script>
<template>
<div>
<form @submit.prevent.stop="form.handleSubmit">
<div>
<form.Field name="fullName">
<template v-slot="{ field }">
<template v-slot="{ field, value }">
<input
:name="field.name"
:value="field.state.value"
667 changes: 404 additions & 263 deletions packages/vue-form/src/useField.tsx → packages/vue-form/src/useField.ts

Large diffs are not rendered by default.

238 changes: 125 additions & 113 deletions packages/vue-form/src/useForm.tsx → packages/vue-form/src/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { FormApi } from '@tanstack/form-core'
import { useStore } from '@tanstack/vue-store'
import { defineComponent, h, onMounted } from 'vue'
import { FormApi, shallow } from '@tanstack/form-core'
import {
defineComponent,
h,
onMounted,
onScopeDispose,
shallowRef,
watchEffect,
} from 'vue'
import { Field, useField } from './useField'
import type { FieldComponent, UseField } from './useField'
import type {
FormAsyncValidateOrFn,
FormOptions,
FormState,
FormValidateOrFn,
} from '@tanstack/form-core'
import type { NoInfer } from '@tanstack/vue-store'
import type {
ComponentOptionsMixin,
CreateComponentPublicInstanceWithMixins,
DefineSetupFnComponent,
EmitsOptions,
EmitsToProps,
FunctionalComponent,
PublicProps,
Ref,
SlotsType,
VNode,
} from 'vue'
import type { FieldComponent, UseField } from './useField'
import type {
FormAsyncValidateOrFn,
FormOptions,
FormState,
FormValidateOrFn,
} from '@tanstack/form-core'
import type { NoInfer } from '@tanstack/vue-store'

type SubscribeComponent<
TParentData,
@@ -66,50 +73,44 @@ type SubscribeComponent<
) => TSelected
} & EmitsToProps<EmitsOptions> &
PublicProps,
) => CreateComponentPublicInstanceWithMixins<
{
selector?: (
state: NoInfer<
FormState<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnServer
>
>,
) => TSelected
},
{},
{},
{},
{},
ComponentOptionsMixin,
ComponentOptionsMixin,
EmitsOptions,
PublicProps,
{},
false,
{},
SlotsType<{
default: NoInfer<
FormState<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnServer
>
>
}>
) => InstanceType<
DefineSetupFnComponent<
{
selector?: (
state: NoInfer<
FormState<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnServer
>
>,
) => TSelected
},
[],
SlotsType<{
default?: (
slotProps: NoInfer<
FormState<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnServer
>
>,
) => VNode[]
}>
>
>

export interface VueFormApi<
@@ -204,7 +205,7 @@ export function useForm<
TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>,
TSubmitMeta,
>(
opts?: FormOptions<
opts?: () => FormOptions<
TParentData,
TFormOnMount,
TFormOnChange,
@@ -217,8 +218,19 @@ export function useForm<
TSubmitMeta
>,
) {
const formApi = (() => {
const api = new FormApi<
let api: FormApi<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnServer,
TSubmitMeta
> &
VueFormApi<
TParentData,
TFormOnMount,
TFormOnChange,
@@ -229,62 +241,62 @@ export function useForm<
TFormOnSubmitAsync,
TFormOnServer,
TSubmitMeta
>(opts)
>

const extendedApi: typeof api &
VueFormApi<
TParentData,
TFormOnMount,
TFormOnChange,
TFormOnChangeAsync,
TFormOnBlur,
TFormOnBlurAsync,
TFormOnSubmit,
TFormOnSubmitAsync,
TFormOnServer,
TSubmitMeta
> = api as never
extendedApi.Field = defineComponent(
(props, context) => {
return () =>
h(
Field as never,
{ ...props, ...context.attrs, form: api },
context.slots,
)
},
{
name: 'APIField',
inheritAttrs: false,
},
) as never
extendedApi.useField = (props) => {
const field = useField({ ...props, form: api })
return field
}
extendedApi.useStore = (selector) => {
return useStore(api.store as never, selector as never) as never
}
extendedApi.Subscribe = defineComponent(
(props, context) => {
const allProps = { ...props, ...context.attrs }
const selector = allProps.selector ?? ((state: never) => state)
const data = useStore(api.store as never, selector as never)
return () => context.slots.default!(data.value)
},
{
name: 'Subscribe',
inheritAttrs: false,
},
) as never
watchEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!api) api = new FormApi(opts?.()) as never
else api.update(opts?.())
})

const APIField: FunctionalComponent = (_, { attrs, slots }) =>
h(Field as never, { ...attrs, form: api }, slots)

api!.Field = APIField as never

api!.useField = (props) => {
return useField(() => ({ ...props(), form: api })) as never
}
api!.useStore = (selector = (v) => v as never) => {
const state = shallowRef(selector(api!.store.state))
const cleanup = api!.store.subscribe(() => {
const newValue = selector(api!.store.state)
if (shallow(newValue, state.value)) return
state.value = newValue
})
onScopeDispose(cleanup)

return extendedApi
})()
return state as never
}
api!.Subscribe = defineComponent(
(props, { slots }) => {
const state = shallowRef(props.selector(api!.store.state))
const cleanup = api!.store.subscribe(() => {
const newValue = props.selector(api!.store.state)
if (shallow(newValue, state.value)) return
state.value = newValue
})
onScopeDispose(cleanup)

onMounted(formApi.mount)
return () => slots.default!(state.value)
},
{
name: 'Subscribe',
inheritAttrs: false,
props: {
selector: {
type: Function,
default: (v) => v,
},
},
},
) as never

// formApi.useStore((state) => state.isSubmitting)
formApi.update(opts)
let cleanup = () => {}
onMounted(() => {
cleanup = api!.mount()
})
onScopeDispose(cleanup)

return formApi
return api!
}
79 changes: 51 additions & 28 deletions packages/vue-form/tests/useField.test.tsx
Original file line number Diff line number Diff line change
@@ -16,16 +16,16 @@ describe('useField', () => {
}

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {} as Person,
})
}))

return () => (
<form.Field name="firstName" defaultValue="FirstName">
{({ field }: { field: AnyFieldApi }) => (
{({ field, value }: { field: AnyFieldApi; value: any }) => (
<input
data-testid={'fieldinput'}
value={field.state.value}
value={value()}
onBlur={field.handleBlur}
onInput={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
@@ -49,7 +49,7 @@ describe('useField', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({ defaultValues: {} as Person })
const form = useForm(() => ({ defaultValues: {} as Person }))

return () => (
<form.Field
@@ -58,7 +58,7 @@ describe('useField', () => {
onChange: ({ value }) => (value === 'other' ? error : undefined),
}}
>
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -92,7 +92,7 @@ describe('useField', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({ defaultValues: {} as Person })
const form = useForm(() => ({ defaultValues: {} as Person }))

return () => (
<form.Field
@@ -101,12 +101,12 @@ describe('useField', () => {
onChange: ({ value }) => (value === 'other' ? error : undefined),
}}
>
{({ field }: { field: AnyFieldApi }) => (
{({ field, value }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"
name={field.name}
value={field.state.value}
value={value()}
onBlur={field.handleBlur}
onInput={(e) =>
field.handleChange((e.target as HTMLInputElement).value)
@@ -134,7 +134,7 @@ describe('useField', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({ defaultValues: {} as Person })
const form = useForm(() => ({ defaultValues: {} as Person }))

return () => (
<form.Field
@@ -147,7 +147,7 @@ describe('useField', () => {
},
}}
>
{({ field }: { field: AnyFieldApi }) => (
{({ field, meta }: { field: AnyFieldApi; value: any; meta: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -158,7 +158,7 @@ describe('useField', () => {
field.handleChange((e.target as HTMLInputElement).value)
}
/>
<p>{field.getMeta().errors}</p>
<p>{meta().errors}</p>
</div>
)}
</form.Field>
@@ -183,7 +183,7 @@ describe('useField', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({ defaultValues: {} as Person })
const form = useForm(() => ({ defaultValues: {} as Person }))

return () => (
<form.Field
@@ -198,7 +198,7 @@ describe('useField', () => {
},
}}
>
{({ field }: { field: AnyFieldApi }) => (
{({ field, meta }: { field: AnyFieldApi; value: any; meta: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -209,7 +209,7 @@ describe('useField', () => {
field.handleChange((e.target as HTMLInputElement).value)
}
/>
<p>{field.getMeta().errors}</p>
<p>{meta().errors}</p>
</div>
)}
</form.Field>
@@ -231,12 +231,12 @@ describe('useField', () => {
type CompVal = { people: Array<string> }

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {
people: [],
} as CompVal,
onSubmit: ({ value }) => fn(value),
})
}))

return () => (
<div>
@@ -248,17 +248,28 @@ describe('useField', () => {
}}
>
<form.Field name="people">
{({ field }: { field: AnyFieldApi }) => (
{({
field,
value: arrValue,
}: {
field: AnyFieldApi
value: any
}) => (
<div>
{field.state.value.map((_: never, i: number) => {
{arrValue().map((_: never, i: number) => {
return (
<form.Field key={i} name={`people[${i}]`}>
{({ field: subField }: { field: AnyFieldApi }) => (
{({
field: subField,
}: {
field: AnyFieldApi
value: any
}) => (
<div>
<label>
<div>Name for person {i}</div>
<input
value={subField.state.value}
value={field.state.value}
onChange={(e) =>
subField.handleChange(
(e.target as HTMLInputElement).value,
@@ -312,18 +323,18 @@ describe('useField', () => {
expect(fn).toHaveBeenCalledWith({ people: ['John'] })
})

it('should handle arrays with subvalues', async () => {
it('should handle arrays with subvalues 2', async () => {
const fn = vi.fn()

type CompVal = { people: Array<{ age: number; name: string }> }

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {
people: [],
} as CompVal,
onSubmit: ({ value }) => fn(value),
})
}))

return () => (
<div>
@@ -335,17 +346,29 @@ describe('useField', () => {
}}
>
<form.Field name="people">
{({ field }: { field: AnyFieldApi }) => (
{({
field,
value: arrValue,
}: {
field: AnyFieldApi
value: any
}) => (
<div>
{field.state.value.map((_: never, i: number) => {
{arrValue().map((_: never, i: number) => {
return (
<form.Field key={i} name={`people[${i}].name`}>
{({ field: subField }: { field: AnyFieldApi }) => (
{({
field: subField,
value,
}: {
field: AnyFieldApi
value: any
}) => (
<div>
<label>
<div>Name for person {i}</div>
<input
value={subField.state.value}
value={value()}
onChange={(e) =>
subField.handleChange(
(e.target as HTMLInputElement).value,
62 changes: 31 additions & 31 deletions packages/vue-form/tests/useForm.test.tsx
Original file line number Diff line number Diff line change
@@ -17,11 +17,11 @@ type Person = {
describe('useForm', () => {
it('preserved field state', async () => {
const Comp = defineComponent(() => {
const form = useForm({ defaultValues: {} as Person })
const form = useForm(() => ({ defaultValues: {} as Person }))

return () => (
<form.Field name="firstName" defaultValue="">
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<input
data-testid={'fieldinput'}
value={field.state.value}
@@ -44,16 +44,16 @@ describe('useForm', () => {

it('should allow default values to be set', async () => {
const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {
firstName: 'FirstName',
lastName: 'LastName',
} as Person,
})
}))

return () => (
<form.Field name="firstName">
{({ field }: { field: AnyFieldApi }) => <p>{field.state.value}</p>}
{({ value }: { field: AnyFieldApi; value: any }) => <p>{value()}</p>}
</form.Field>
)
})
@@ -67,19 +67,19 @@ describe('useForm', () => {
const Comp = defineComponent(() => {
const submittedData = ref<{ firstName: string }>()

const form = useForm({
const form = useForm(() => ({
defaultValues: {
firstName: 'FirstName',
},
onSubmit: ({ value }) => {
submittedData.value = value
},
})
}))

return () => (
<div>
<form.Field name="firstName">
{({ field }: { field: AnyFieldApi }) => {
{({ field }: { field: AnyFieldApi; value: any }) => {
return (
<input
value={field.state.value}
@@ -115,7 +115,7 @@ describe('useForm', () => {
const formMounted = ref(false)
const mountForm = ref(false)

const form = useForm({
const form = useForm(() => ({
defaultValues: {
firstName: 'FirstName',
},
@@ -125,7 +125,7 @@ describe('useForm', () => {
return undefined
},
},
})
}))

return () =>
mountForm.value ? (
@@ -146,19 +146,19 @@ describe('useForm', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {} as Person,
validators: {
onChange() {
return error
},
},
})
}))

return () => (
<div>
<form.Field name="firstName">
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<input
data-testid="fieldinput"
name={field.name}
@@ -188,20 +188,20 @@ describe('useForm', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {} as Person,
validators: {
onChange: ({ value }) =>
value.firstName === 'other' ? error : undefined,
},
})
}))

const errors = form.useStore((s) => s.errors)

return () => (
<div>
<form.Field name="firstName">
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -232,20 +232,20 @@ describe('useForm', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {} as Person,
validators: {
onChange: ({ value }) =>
value.firstName === 'other' ? error : undefined,
},
})
}))

const errors = form.useStore((s) => s.errorMap)

return () => (
<div>
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -276,7 +276,7 @@ describe('useForm', () => {
const onBlurError = 'Please enter a different value (onBlurError)'

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {
firstName: '',
},
@@ -290,14 +290,14 @@ describe('useForm', () => {
return undefined
},
},
})
}))

const errors = form.useStore((s) => s.errorMap)

return () => (
<div>
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -330,22 +330,22 @@ describe('useForm', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {} as Person,
validators: {
onChangeAsync: async () => {
await sleep(10)
return error
},
},
})
}))

const errors = form.useStore((s) => s.errorMap)

return () => (
<div>
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -377,7 +377,7 @@ describe('useForm', () => {
const onBlurError = 'Please enter a different value (onBlurError)'

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {} as Person,
validators: {
onChangeAsync: async () => {
@@ -389,13 +389,13 @@ describe('useForm', () => {
return onBlurError
},
},
})
}))
const errors = form.useStore((s) => s.errorMap)

return () => (
<div>
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"
@@ -433,7 +433,7 @@ describe('useForm', () => {
const error = 'Please enter a different value'

const Comp = defineComponent(() => {
const form = useForm({
const form = useForm(() => ({
defaultValues: {} as Person,
validators: {
onChangeAsyncDebounceMs: 100,
@@ -443,13 +443,13 @@ describe('useForm', () => {
return error
},
},
})
}))
const errors = form.useStore((s) => s.errors)

return () => (
<div>
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
{({ field }: { field: AnyFieldApi }) => (
{({ field }: { field: AnyFieldApi; value: any }) => (
<div>
<input
data-testid="fieldinput"