@tanstack/vue-form 0.10.1 → 0.10.2
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/build/legacy/createFormFactory.d.cts +1 -1
- package/build/legacy/createFormFactory.d.ts +1 -1
- package/build/legacy/formContext.cjs +3 -3
- package/build/legacy/formContext.cjs.map +1 -1
- package/build/legacy/formContext.js +1 -1
- package/build/legacy/formContext.js.map +1 -1
- package/build/legacy/index.d.cts +1 -1
- package/build/legacy/index.d.ts +1 -1
- package/build/legacy/useField.cjs +5 -5
- package/build/legacy/useField.cjs.map +1 -1
- package/build/legacy/useField.d.cts +1 -1
- package/build/legacy/useField.d.ts +1 -1
- package/build/legacy/useField.js +1 -1
- package/build/legacy/useField.js.map +1 -1
- package/build/legacy/useForm.cjs +5 -5
- package/build/legacy/useForm.cjs.map +1 -1
- package/build/legacy/useForm.d.cts +2 -2
- package/build/legacy/useForm.d.ts +2 -2
- package/build/legacy/useForm.js +1 -1
- package/build/legacy/useForm.js.map +1 -1
- package/build/modern/createFormFactory.d.cts +1 -1
- package/build/modern/createFormFactory.d.ts +1 -1
- package/build/modern/formContext.cjs +3 -3
- package/build/modern/formContext.cjs.map +1 -1
- package/build/modern/formContext.js +1 -1
- package/build/modern/formContext.js.map +1 -1
- package/build/modern/index.d.cts +1 -1
- package/build/modern/index.d.ts +1 -1
- package/build/modern/useField.cjs +5 -5
- package/build/modern/useField.cjs.map +1 -1
- package/build/modern/useField.d.cts +1 -1
- package/build/modern/useField.d.ts +1 -1
- package/build/modern/useField.js +1 -1
- package/build/modern/useField.js.map +1 -1
- package/build/modern/useForm.cjs +5 -5
- package/build/modern/useForm.cjs.map +1 -1
- package/build/modern/useForm.d.cts +2 -2
- package/build/modern/useForm.d.ts +2 -2
- package/build/modern/useForm.js +1 -1
- package/build/modern/useForm.js.map +1 -1
- package/package.json +5 -18
- package/src/formContext.ts +1 -1
- package/src/tests/useField.test.tsx +2 -8
- package/src/tests/useForm.test.tsx +388 -26
- package/src/useField.tsx +2 -2
- package/src/useForm.tsx +3 -2
@@ -1,7 +1,7 @@
|
|
1
1
|
import { FormState, FormOptions, FormApi } from '@tanstack/form-core';
|
2
2
|
import { NoInfer } from '@tanstack/vue-store';
|
3
3
|
import { FieldComponent, UseField } from './useField.cjs';
|
4
|
-
import { SetupContext, EmitsOptions, SlotsType } from 'vue
|
4
|
+
import { Ref, SetupContext, EmitsOptions, SlotsType } from 'vue';
|
5
5
|
import './types.cjs';
|
6
6
|
|
7
7
|
declare module '@tanstack/form-core' {
|
@@ -10,7 +10,7 @@ declare module '@tanstack/form-core' {
|
|
10
10
|
provideFormContext: () => void;
|
11
11
|
Field: FieldComponent<TFormData, ValidatorType>;
|
12
12
|
useField: UseField<TFormData, ValidatorType>;
|
13
|
-
useStore: <TSelected = NoInfer<FormState<TFormData>>>(selector?: (state: NoInfer<FormState<TFormData>>) => TSelected) => TSelected
|
13
|
+
useStore: <TSelected = NoInfer<FormState<TFormData>>>(selector?: (state: NoInfer<FormState<TFormData>>) => TSelected) => Readonly<Ref<TSelected>>;
|
14
14
|
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
|
15
15
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected;
|
16
16
|
}, context: SetupContext<EmitsOptions, SlotsType<{
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { FormState, FormOptions, FormApi } from '@tanstack/form-core';
|
2
2
|
import { NoInfer } from '@tanstack/vue-store';
|
3
3
|
import { FieldComponent, UseField } from './useField.js';
|
4
|
-
import { SetupContext, EmitsOptions, SlotsType } from 'vue
|
4
|
+
import { Ref, SetupContext, EmitsOptions, SlotsType } from 'vue';
|
5
5
|
import './types.js';
|
6
6
|
|
7
7
|
declare module '@tanstack/form-core' {
|
@@ -10,7 +10,7 @@ declare module '@tanstack/form-core' {
|
|
10
10
|
provideFormContext: () => void;
|
11
11
|
Field: FieldComponent<TFormData, ValidatorType>;
|
12
12
|
useField: UseField<TFormData, ValidatorType>;
|
13
|
-
useStore: <TSelected = NoInfer<FormState<TFormData>>>(selector?: (state: NoInfer<FormState<TFormData>>) => TSelected) => TSelected
|
13
|
+
useStore: <TSelected = NoInfer<FormState<TFormData>>>(selector?: (state: NoInfer<FormState<TFormData>>) => TSelected) => Readonly<Ref<TSelected>>;
|
14
14
|
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
|
15
15
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected;
|
16
16
|
}, context: SetupContext<EmitsOptions, SlotsType<{
|
package/build/modern/useForm.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../../src/useForm.tsx"],"sourcesContent":["import { FormApi, type FormState, type FormOptions } from '@tanstack/form-core'\nimport { type NoInfer, useStore } from '@tanstack/vue-store'\nimport { type UseField, type FieldComponent, Field, useField } from './useField'\nimport { provideFormContext } from './formContext'\nimport {\n type EmitsOptions,\n type SlotsType,\n type SetupContext,\n defineComponent,\n onMounted,\n} from 'vue
|
1
|
+
{"version":3,"sources":["../../src/useForm.tsx"],"sourcesContent":["import { FormApi, type FormState, type FormOptions } from '@tanstack/form-core'\nimport { type NoInfer, useStore } from '@tanstack/vue-store'\nimport { type UseField, type FieldComponent, Field, useField } from './useField'\nimport { provideFormContext } from './formContext'\nimport {\n type EmitsOptions,\n type SlotsType,\n type SetupContext,\n type Ref,\n defineComponent,\n onMounted,\n} from 'vue'\n\ndeclare module '@tanstack/form-core' {\n // eslint-disable-next-line no-shadow\n interface FormApi<TFormData, ValidatorType> {\n Provider: (props: Record<string, any> & {}) => any\n provideFormContext: () => void\n Field: FieldComponent<TFormData, ValidatorType>\n useField: UseField<TFormData, ValidatorType>\n useStore: <TSelected = NoInfer<FormState<TFormData>>>(\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,\n ) => Readonly<Ref<TSelected>>\n Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(\n props: {\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected\n },\n context: SetupContext<\n EmitsOptions,\n SlotsType<{ default: NoInfer<FormState<TFormData>> }>\n >,\n ) => any\n }\n}\n\nexport function useForm<TData, FormValidator>(\n opts?: FormOptions<TData, FormValidator>,\n): FormApi<TData, FormValidator> {\n const formApi = (() => {\n const api = new FormApi<TData, FormValidator>(opts)\n\n api.Provider = defineComponent(\n (_, context) => {\n onMounted(api.mount)\n provideFormContext({ formApi: formApi as never })\n return () => context.slots.default!()\n },\n { name: 'Provider' },\n )\n api.provideFormContext = () => {\n onMounted(api.mount)\n provideFormContext({ formApi: formApi as never })\n }\n api.Field = Field as never\n api.useField = useField as never\n api.useStore = (selector) => {\n return useStore(api.store as never, selector as never) as never\n }\n api.Subscribe = defineComponent(\n (props, context) => {\n const allProps = { ...props, ...context.attrs }\n const selector = allProps.selector ?? ((state) => state)\n const data = useStore(api.store as never, selector as never)\n return () => context.slots.default!(data.value)\n },\n {\n name: 'Subscribe',\n inheritAttrs: false,\n },\n )\n\n return api\n })()\n\n // formApi.useStore((state) => state.isSubmitting)\n formApi.update(opts)\n\n return formApi as never\n}\n"],"mappings":";AAAA,SAAS,eAAiD;AAC1D,SAAuB,gBAAgB;AACvC,SAA6C,OAAO,gBAAgB;AACpE,SAAS,0BAA0B;AACnC;AAAA,EAKE;AAAA,EACA;AAAA,OACK;AAwBA,SAAS,QACd,MAC+B;AAC/B,QAAM,WAAW,MAAM;AACrB,UAAM,MAAM,IAAI,QAA8B,IAAI;AAElD,QAAI,WAAW;AAAA,MACb,CAAC,GAAG,YAAY;AACd,kBAAU,IAAI,KAAK;AACnB,2BAAmB,EAAE,QAA0B,CAAC;AAChD,eAAO,MAAM,QAAQ,MAAM,QAAS;AAAA,MACtC;AAAA,MACA,EAAE,MAAM,WAAW;AAAA,IACrB;AACA,QAAI,qBAAqB,MAAM;AAC7B,gBAAU,IAAI,KAAK;AACnB,yBAAmB,EAAE,QAA0B,CAAC;AAAA,IAClD;AACA,QAAI,QAAQ;AACZ,QAAI,WAAW;AACf,QAAI,WAAW,CAAC,aAAa;AAC3B,aAAO,SAAS,IAAI,OAAgB,QAAiB;AAAA,IACvD;AACA,QAAI,YAAY;AAAA,MACd,CAAC,OAAO,YAAY;AAClB,cAAM,WAAW,EAAE,GAAG,OAAO,GAAG,QAAQ,MAAM;AAC9C,cAAM,WAAW,SAAS,aAAa,CAAC,UAAU;AAClD,cAAM,OAAO,SAAS,IAAI,OAAgB,QAAiB;AAC3D,eAAO,MAAM,QAAQ,MAAM,QAAS,KAAK,KAAK;AAAA,MAChD;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,cAAc;AAAA,MAChB;AAAA,IACF;AAEA,WAAO;AAAA,EACT,GAAG;AAGH,UAAQ,OAAO,IAAI;AAEnB,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@tanstack/vue-form",
|
3
|
-
"version": "0.10.
|
3
|
+
"version": "0.10.2",
|
4
4
|
"description": "Powerful, type-safe forms for Vue.",
|
5
5
|
"author": "tannerlinsley",
|
6
6
|
"license": "MIT",
|
@@ -44,33 +44,20 @@
|
|
44
44
|
"dependencies": {
|
45
45
|
"@tanstack/store": "0.1.3",
|
46
46
|
"@tanstack/vue-store": "0.1.3",
|
47
|
-
"
|
48
|
-
"@tanstack/form-core": "0.10.1"
|
47
|
+
"@tanstack/form-core": "0.10.2"
|
49
48
|
},
|
50
49
|
"devDependencies": {
|
51
|
-
"
|
52
|
-
"vue": "^3.3.4",
|
53
|
-
"vue2": "npm:vue@2.6",
|
54
|
-
"vue2.7": "npm:vue@2.7"
|
50
|
+
"vue": "^3.3.4"
|
55
51
|
},
|
56
52
|
"peerDependencies": {
|
57
|
-
"
|
58
|
-
"vue": "^2.5.0 || ^3.0.0"
|
59
|
-
},
|
60
|
-
"peerDependenciesMeta": {
|
61
|
-
"@vue/composition-api": {
|
62
|
-
"optional": true
|
63
|
-
}
|
53
|
+
"vue": "^3.3.0"
|
64
54
|
},
|
65
55
|
"scripts": {
|
66
56
|
"clean": "rimraf ./build && rimraf ./coverage",
|
67
57
|
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
68
58
|
"test:types": "tsc",
|
69
59
|
"fixme:test:lib": "pnpm run test:2 && pnpm run test:2.7 && pnpm run test:3",
|
70
|
-
"test:lib": "
|
71
|
-
"test:2": "vue-demi-switch 2 vue2 && vitest",
|
72
|
-
"test:2.7": "vue-demi-switch 2.7 vue2.7 && vitest",
|
73
|
-
"test:3": "vue-demi-switch 3 && vitest",
|
60
|
+
"test:lib": "vitest",
|
74
61
|
"test:lib:dev": "pnpm run test:lib --watch",
|
75
62
|
"test:build": "publint --strict",
|
76
63
|
"build": "tsup"
|
package/src/formContext.ts
CHANGED
@@ -1,15 +1,9 @@
|
|
1
1
|
/// <reference lib="dom" />
|
2
|
-
import { h, defineComponent } from 'vue
|
2
|
+
import { h, defineComponent } from 'vue'
|
3
3
|
import { render, waitFor } from '@testing-library/vue'
|
4
4
|
import '@testing-library/jest-dom'
|
5
|
-
import {
|
6
|
-
createFormFactory,
|
7
|
-
type FieldApi,
|
8
|
-
provideFormContext,
|
9
|
-
useForm,
|
10
|
-
} from '../index'
|
5
|
+
import { createFormFactory, type FieldApi, provideFormContext } from '../index'
|
11
6
|
import userEvent from '@testing-library/user-event'
|
12
|
-
import * as React from 'react'
|
13
7
|
import { sleep } from './utils'
|
14
8
|
|
15
9
|
const user = userEvent.setup()
|
@@ -1,25 +1,27 @@
|
|
1
1
|
/// <reference lib="dom" />
|
2
|
-
import { h, defineComponent, ref } from 'vue-demi'
|
3
|
-
import { render, waitFor } from '@testing-library/vue'
|
4
2
|
import '@testing-library/jest-dom'
|
3
|
+
import userEvent from '@testing-library/user-event'
|
4
|
+
import { render, waitFor } from '@testing-library/vue'
|
5
|
+
import { h, defineComponent, ref } from 'vue'
|
5
6
|
import {
|
7
|
+
ValidationError,
|
6
8
|
createFormFactory,
|
7
|
-
type FieldApi,
|
8
9
|
provideFormContext,
|
9
10
|
useForm,
|
10
|
-
} from '
|
11
|
-
import
|
12
|
-
|
11
|
+
} from '..'
|
12
|
+
import { sleep } from './utils'
|
13
|
+
|
14
|
+
import type { FieldApi, ValidationErrorMap } from '..'
|
13
15
|
|
14
16
|
const user = userEvent.setup()
|
15
17
|
|
18
|
+
type Person = {
|
19
|
+
firstName: string
|
20
|
+
lastName: string
|
21
|
+
}
|
22
|
+
|
16
23
|
describe('useForm', () => {
|
17
24
|
it('preserved field state', async () => {
|
18
|
-
type Person = {
|
19
|
-
firstName: string
|
20
|
-
lastName: string
|
21
|
-
}
|
22
|
-
|
23
25
|
const formFactory = createFormFactory<Person, unknown>()
|
24
26
|
|
25
27
|
const Comp = defineComponent(() => {
|
@@ -47,7 +49,7 @@ describe('useForm', () => {
|
|
47
49
|
)
|
48
50
|
})
|
49
51
|
|
50
|
-
const { getByTestId, queryByText } = render(Comp)
|
52
|
+
const { getByTestId, queryByText } = render(<Comp />)
|
51
53
|
const input = getByTestId('fieldinput')
|
52
54
|
expect(queryByText('FirstName')).not.toBeInTheDocument()
|
53
55
|
await user.type(input, 'FirstName')
|
@@ -55,11 +57,6 @@ describe('useForm', () => {
|
|
55
57
|
})
|
56
58
|
|
57
59
|
it('should allow default values to be set', async () => {
|
58
|
-
type Person = {
|
59
|
-
firstName: string
|
60
|
-
lastName: string
|
61
|
-
}
|
62
|
-
|
63
60
|
const formFactory = createFormFactory<Person, unknown>()
|
64
61
|
|
65
62
|
const Comp = defineComponent(() => {
|
@@ -82,7 +79,7 @@ describe('useForm', () => {
|
|
82
79
|
)
|
83
80
|
})
|
84
81
|
|
85
|
-
const { findByText, queryByText } = render(Comp)
|
82
|
+
const { findByText, queryByText } = render(<Comp />)
|
86
83
|
expect(await findByText('FirstName')).toBeInTheDocument()
|
87
84
|
expect(queryByText('LastName')).not.toBeInTheDocument()
|
88
85
|
})
|
@@ -102,18 +99,18 @@ describe('useForm', () => {
|
|
102
99
|
form.provideFormContext()
|
103
100
|
|
104
101
|
return () => (
|
105
|
-
<
|
102
|
+
<div>
|
106
103
|
<form.Field name="firstName">
|
107
104
|
{({
|
108
105
|
field,
|
109
106
|
}: {
|
110
|
-
field: FieldApi<
|
107
|
+
field: FieldApi<Person, 'firstName', never, never>
|
111
108
|
}) => {
|
112
109
|
return (
|
113
110
|
<input
|
114
111
|
value={field.state.value}
|
115
112
|
onBlur={field.handleBlur}
|
116
|
-
|
113
|
+
onInput={(e) =>
|
117
114
|
field.handleChange((e.target as HTMLInputElement).value)
|
118
115
|
}
|
119
116
|
placeholder={'First name'}
|
@@ -125,11 +122,11 @@ describe('useForm', () => {
|
|
125
122
|
{submittedData.value && (
|
126
123
|
<p>Submitted data: {submittedData.value.firstName}</p>
|
127
124
|
)}
|
128
|
-
</
|
125
|
+
</div>
|
129
126
|
)
|
130
127
|
})
|
131
128
|
|
132
|
-
const { findByPlaceholderText, getByText } = render(Comp)
|
129
|
+
const { findByPlaceholderText, getByText } = render(<Comp />)
|
133
130
|
const input = await findByPlaceholderText('First name')
|
134
131
|
await user.clear(input)
|
135
132
|
await user.type(input, 'OtherName')
|
@@ -153,20 +150,385 @@ describe('useForm', () => {
|
|
153
150
|
return undefined
|
154
151
|
},
|
155
152
|
})
|
153
|
+
|
156
154
|
form.provideFormContext()
|
157
155
|
|
158
156
|
return () =>
|
159
157
|
mountForm.value ? (
|
160
|
-
<
|
158
|
+
<div>
|
161
159
|
<h1>{formMounted.value ? 'Form mounted' : 'Not mounted'}</h1>
|
162
|
-
</
|
160
|
+
</div>
|
163
161
|
) : (
|
164
162
|
<button onClick={() => (mountForm.value = true)}>Mount form</button>
|
165
163
|
)
|
166
164
|
})
|
167
165
|
|
168
|
-
const { getByText, findByText } = render(Comp)
|
166
|
+
const { getByText, findByText } = render(<Comp />)
|
169
167
|
await user.click(getByText('Mount form'))
|
170
168
|
expect(await findByText('Form mounted')).toBeInTheDocument()
|
171
169
|
})
|
170
|
+
|
171
|
+
it('should validate async on change for the form', async () => {
|
172
|
+
const error = 'Please enter a different value'
|
173
|
+
|
174
|
+
const formFactory = createFormFactory<Person, unknown>()
|
175
|
+
|
176
|
+
const Comp = defineComponent(() => {
|
177
|
+
const form = formFactory.useForm({
|
178
|
+
onChange() {
|
179
|
+
return error
|
180
|
+
},
|
181
|
+
})
|
182
|
+
|
183
|
+
form.provideFormContext()
|
184
|
+
|
185
|
+
return () => (
|
186
|
+
<div>
|
187
|
+
<form.Field name="firstName">
|
188
|
+
{({
|
189
|
+
field,
|
190
|
+
}: {
|
191
|
+
field: FieldApi<Person, 'firstName', never, never>
|
192
|
+
}) => (
|
193
|
+
<input
|
194
|
+
data-testid="fieldinput"
|
195
|
+
name={field.name}
|
196
|
+
value={field.state.value}
|
197
|
+
onBlur={field.handleBlur}
|
198
|
+
onInput={(e) =>
|
199
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
200
|
+
}
|
201
|
+
/>
|
202
|
+
)}
|
203
|
+
</form.Field>
|
204
|
+
<form.Subscribe selector={(state) => state.errorMap}>
|
205
|
+
{(errorMap: ValidationErrorMap) => <p>{errorMap.onChange}</p>}
|
206
|
+
</form.Subscribe>
|
207
|
+
</div>
|
208
|
+
)
|
209
|
+
})
|
210
|
+
|
211
|
+
const { getByTestId, getByText, queryByText } = render(<Comp />)
|
212
|
+
const input = getByTestId('fieldinput')
|
213
|
+
expect(queryByText(error)).not.toBeInTheDocument()
|
214
|
+
await user.type(input, 'other')
|
215
|
+
await waitFor(() => getByText(error))
|
216
|
+
expect(getByText(error)).toBeInTheDocument()
|
217
|
+
})
|
218
|
+
it('should not validate on change if isTouched is false', async () => {
|
219
|
+
const error = 'Please enter a different value'
|
220
|
+
|
221
|
+
const formFactory = createFormFactory<Person, unknown>()
|
222
|
+
|
223
|
+
const Comp = defineComponent(() => {
|
224
|
+
const form = formFactory.useForm({
|
225
|
+
onChange: (value) => (value.firstName === 'other' ? error : undefined),
|
226
|
+
})
|
227
|
+
|
228
|
+
const errors = form.useStore((s) => s.errors)
|
229
|
+
|
230
|
+
form.provideFormContext()
|
231
|
+
|
232
|
+
return () => (
|
233
|
+
<div>
|
234
|
+
<form.Field name="firstName">
|
235
|
+
{({
|
236
|
+
field,
|
237
|
+
}: {
|
238
|
+
field: FieldApi<Person, 'firstName', never, never>
|
239
|
+
}) => (
|
240
|
+
<div>
|
241
|
+
<input
|
242
|
+
data-testid="fieldinput"
|
243
|
+
name={field.name}
|
244
|
+
value={field.state.value}
|
245
|
+
onBlur={field.handleBlur}
|
246
|
+
onInput={(e) =>
|
247
|
+
field.setValue((e.target as HTMLInputElement).value)
|
248
|
+
}
|
249
|
+
/>
|
250
|
+
</div>
|
251
|
+
)}
|
252
|
+
</form.Field>
|
253
|
+
<p>{errors}</p>
|
254
|
+
</div>
|
255
|
+
)
|
256
|
+
})
|
257
|
+
|
258
|
+
const { getByTestId, queryByText } = render(<Comp />)
|
259
|
+
const input = getByTestId('fieldinput')
|
260
|
+
await user.type(input, 'other')
|
261
|
+
expect(queryByText(error)).not.toBeInTheDocument()
|
262
|
+
})
|
263
|
+
|
264
|
+
it('should validate on change if isTouched is true', async () => {
|
265
|
+
const error = 'Please enter a different value'
|
266
|
+
|
267
|
+
const formFactory = createFormFactory<Person, unknown>()
|
268
|
+
|
269
|
+
const Comp = defineComponent(() => {
|
270
|
+
const form = formFactory.useForm({
|
271
|
+
onChange: (value) => (value.firstName === 'other' ? error : undefined),
|
272
|
+
})
|
273
|
+
|
274
|
+
const errors = form.useStore((s) => s.errorMap)
|
275
|
+
|
276
|
+
form.provideFormContext()
|
277
|
+
|
278
|
+
return () => (
|
279
|
+
<div>
|
280
|
+
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
|
281
|
+
{({
|
282
|
+
field,
|
283
|
+
}: {
|
284
|
+
field: FieldApi<Person, 'firstName', never, never>
|
285
|
+
}) => (
|
286
|
+
<div>
|
287
|
+
<input
|
288
|
+
data-testid="fieldinput"
|
289
|
+
name={field.name}
|
290
|
+
value={field.state.value}
|
291
|
+
onBlur={field.handleBlur}
|
292
|
+
onInput={(e) =>
|
293
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
294
|
+
}
|
295
|
+
/>
|
296
|
+
<p>{errors.value.onChange}</p>
|
297
|
+
</div>
|
298
|
+
)}
|
299
|
+
</form.Field>
|
300
|
+
</div>
|
301
|
+
)
|
302
|
+
})
|
303
|
+
|
304
|
+
const { getByTestId, getByText, queryByText } = render(<Comp />)
|
305
|
+
const input = getByTestId('fieldinput')
|
306
|
+
expect(queryByText(error)).not.toBeInTheDocument()
|
307
|
+
await user.type(input, 'other')
|
308
|
+
expect(getByText(error)).toBeInTheDocument()
|
309
|
+
})
|
310
|
+
|
311
|
+
it('should validate on change and on blur', async () => {
|
312
|
+
const onChangeError = 'Please enter a different value (onChangeError)'
|
313
|
+
const onBlurError = 'Please enter a different value (onBlurError)'
|
314
|
+
|
315
|
+
const Comp = defineComponent(() => {
|
316
|
+
const form = useForm({
|
317
|
+
defaultValues: {
|
318
|
+
firstName: '',
|
319
|
+
},
|
320
|
+
onChange: (vals) => {
|
321
|
+
if (vals.firstName === 'other') return onChangeError
|
322
|
+
return undefined
|
323
|
+
},
|
324
|
+
onBlur: (vals) => {
|
325
|
+
if (vals.firstName === 'other') return onBlurError
|
326
|
+
return undefined
|
327
|
+
},
|
328
|
+
})
|
329
|
+
|
330
|
+
const errors = form.useStore((s) => s.errorMap)
|
331
|
+
|
332
|
+
form.provideFormContext()
|
333
|
+
|
334
|
+
return () => (
|
335
|
+
<div>
|
336
|
+
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
|
337
|
+
{({
|
338
|
+
field,
|
339
|
+
}: {
|
340
|
+
field: FieldApi<Person, 'firstName', never, never>
|
341
|
+
}) => (
|
342
|
+
<div>
|
343
|
+
<input
|
344
|
+
data-testid="fieldinput"
|
345
|
+
name={field.name}
|
346
|
+
value={field.state.value}
|
347
|
+
onBlur={field.handleBlur}
|
348
|
+
onInput={(e) =>
|
349
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
350
|
+
}
|
351
|
+
/>
|
352
|
+
<p>{errors.value.onChange}</p>
|
353
|
+
<p>{errors.value.onBlur}</p>
|
354
|
+
</div>
|
355
|
+
)}
|
356
|
+
</form.Field>
|
357
|
+
</div>
|
358
|
+
)
|
359
|
+
})
|
360
|
+
const { getByTestId, getByText, queryByText } = render(<Comp />)
|
361
|
+
const input = getByTestId('fieldinput')
|
362
|
+
expect(queryByText(onChangeError)).not.toBeInTheDocument()
|
363
|
+
expect(queryByText(onBlurError)).not.toBeInTheDocument()
|
364
|
+
await user.type(input, 'other')
|
365
|
+
expect(getByText(onChangeError)).toBeInTheDocument()
|
366
|
+
await user.click(document.body)
|
367
|
+
expect(queryByText(onBlurError)).toBeInTheDocument()
|
368
|
+
})
|
369
|
+
|
370
|
+
it('should validate async on change', async () => {
|
371
|
+
const error = 'Please enter a different value'
|
372
|
+
|
373
|
+
const formFactory = createFormFactory<Person, unknown>()
|
374
|
+
|
375
|
+
const Comp = defineComponent(() => {
|
376
|
+
const form = formFactory.useForm({
|
377
|
+
onChangeAsync: async () => {
|
378
|
+
await sleep(10)
|
379
|
+
return error
|
380
|
+
},
|
381
|
+
})
|
382
|
+
|
383
|
+
const errors = form.useStore((s) => s.errorMap)
|
384
|
+
|
385
|
+
form.provideFormContext()
|
386
|
+
|
387
|
+
return () => (
|
388
|
+
<div>
|
389
|
+
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
|
390
|
+
{({
|
391
|
+
field,
|
392
|
+
}: {
|
393
|
+
field: FieldApi<Person, 'firstName', never, never>
|
394
|
+
}) => (
|
395
|
+
<div>
|
396
|
+
<input
|
397
|
+
data-testid="fieldinput"
|
398
|
+
name={field.name}
|
399
|
+
value={field.state.value}
|
400
|
+
onBlur={field.handleBlur}
|
401
|
+
onInput={(e) =>
|
402
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
403
|
+
}
|
404
|
+
/>
|
405
|
+
<p>{errors.value.onChange}</p>
|
406
|
+
</div>
|
407
|
+
)}
|
408
|
+
</form.Field>
|
409
|
+
</div>
|
410
|
+
)
|
411
|
+
})
|
412
|
+
|
413
|
+
const { getByTestId, getByText, queryByText } = render(<Comp />)
|
414
|
+
const input = getByTestId('fieldinput')
|
415
|
+
expect(queryByText(error)).not.toBeInTheDocument()
|
416
|
+
await user.type(input, 'other')
|
417
|
+
await waitFor(() => getByText(error))
|
418
|
+
expect(getByText(error)).toBeInTheDocument()
|
419
|
+
})
|
420
|
+
|
421
|
+
it('should validate async on change and async on blur', async () => {
|
422
|
+
const onChangeError = 'Please enter a different value (onChangeError)'
|
423
|
+
const onBlurError = 'Please enter a different value (onBlurError)'
|
424
|
+
|
425
|
+
const formFactory = createFormFactory<Person, unknown>()
|
426
|
+
|
427
|
+
const Comp = defineComponent(() => {
|
428
|
+
const form = formFactory.useForm({
|
429
|
+
onChangeAsync: async () => {
|
430
|
+
await sleep(10)
|
431
|
+
return onChangeError
|
432
|
+
},
|
433
|
+
onBlurAsync: async () => {
|
434
|
+
await sleep(10)
|
435
|
+
return onBlurError
|
436
|
+
},
|
437
|
+
})
|
438
|
+
const errors = form.useStore((s) => s.errorMap)
|
439
|
+
|
440
|
+
form.provideFormContext()
|
441
|
+
|
442
|
+
return () => (
|
443
|
+
<div>
|
444
|
+
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
|
445
|
+
{({
|
446
|
+
field,
|
447
|
+
}: {
|
448
|
+
field: FieldApi<Person, 'firstName', never, never>
|
449
|
+
}) => (
|
450
|
+
<div>
|
451
|
+
<input
|
452
|
+
data-testid="fieldinput"
|
453
|
+
name={field.name}
|
454
|
+
value={field.state.value}
|
455
|
+
onBlur={field.handleBlur}
|
456
|
+
onInput={(e) =>
|
457
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
458
|
+
}
|
459
|
+
/>
|
460
|
+
<p>{errors.value.onChange}</p>
|
461
|
+
<p>{errors.value.onBlur}</p>
|
462
|
+
</div>
|
463
|
+
)}
|
464
|
+
</form.Field>
|
465
|
+
</div>
|
466
|
+
)
|
467
|
+
})
|
468
|
+
|
469
|
+
const { getByTestId, getByText, queryByText } = render(<Comp />)
|
470
|
+
const input = getByTestId('fieldinput')
|
471
|
+
|
472
|
+
expect(queryByText(onChangeError)).not.toBeInTheDocument()
|
473
|
+
expect(queryByText(onBlurError)).not.toBeInTheDocument()
|
474
|
+
await user.type(input, 'other')
|
475
|
+
await waitFor(() => getByText(onChangeError))
|
476
|
+
expect(getByText(onChangeError)).toBeInTheDocument()
|
477
|
+
await user.click(document.body)
|
478
|
+
await waitFor(() => getByText(onBlurError))
|
479
|
+
expect(getByText(onBlurError)).toBeInTheDocument()
|
480
|
+
})
|
481
|
+
|
482
|
+
it('should validate async on change with debounce', async () => {
|
483
|
+
const mockFn = vi.fn()
|
484
|
+
const error = 'Please enter a different value'
|
485
|
+
const formFactory = createFormFactory<Person, unknown>()
|
486
|
+
|
487
|
+
const Comp = defineComponent(() => {
|
488
|
+
const form = formFactory.useForm({
|
489
|
+
onChangeAsyncDebounceMs: 100,
|
490
|
+
onChangeAsync: async () => {
|
491
|
+
mockFn()
|
492
|
+
await sleep(10)
|
493
|
+
return error
|
494
|
+
},
|
495
|
+
})
|
496
|
+
const errors = form.useStore((s) => s.errors)
|
497
|
+
|
498
|
+
form.provideFormContext()
|
499
|
+
|
500
|
+
return () => (
|
501
|
+
<div>
|
502
|
+
<form.Field name="firstName" defaultMeta={{ isTouched: true }}>
|
503
|
+
{({
|
504
|
+
field,
|
505
|
+
}: {
|
506
|
+
field: FieldApi<Person, 'firstName', never, never>
|
507
|
+
}) => (
|
508
|
+
<div>
|
509
|
+
<input
|
510
|
+
data-testid="fieldinput"
|
511
|
+
name={field.name}
|
512
|
+
value={field.state.value}
|
513
|
+
onBlur={field.handleBlur}
|
514
|
+
onInput={(e) =>
|
515
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
516
|
+
}
|
517
|
+
/>
|
518
|
+
<p>{errors.value.join(',')}</p>
|
519
|
+
</div>
|
520
|
+
)}
|
521
|
+
</form.Field>
|
522
|
+
</div>
|
523
|
+
)
|
524
|
+
})
|
525
|
+
|
526
|
+
const { getByTestId, getByText } = render(<Comp />)
|
527
|
+
const input = getByTestId('fieldinput')
|
528
|
+
await user.type(input, 'other')
|
529
|
+
// mockFn will have been called 5 times without onChangeAsyncDebounceMs
|
530
|
+
expect(mockFn).toHaveBeenCalledTimes(0)
|
531
|
+
await waitFor(() => getByText(error))
|
532
|
+
expect(getByText(error)).toBeInTheDocument()
|
533
|
+
})
|
172
534
|
})
|
package/src/useField.tsx
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import { FieldApi } from '@tanstack/form-core'
|
2
2
|
import type { DeepKeys, DeepValue, Narrow } from '@tanstack/form-core'
|
3
3
|
import { useStore } from '@tanstack/vue-store'
|
4
|
-
import { defineComponent, onMounted, onUnmounted, watch } from 'vue
|
5
|
-
import type { SlotsType, SetupContext, Ref } from 'vue
|
4
|
+
import { defineComponent, onMounted, onUnmounted, watch } from 'vue'
|
5
|
+
import type { SlotsType, SetupContext, Ref } from 'vue'
|
6
6
|
import { provideFormContext, useFormContext } from './formContext'
|
7
7
|
import type { UseFieldOptions } from './types'
|
8
8
|
|
package/src/useForm.tsx
CHANGED
@@ -6,9 +6,10 @@ import {
|
|
6
6
|
type EmitsOptions,
|
7
7
|
type SlotsType,
|
8
8
|
type SetupContext,
|
9
|
+
type Ref,
|
9
10
|
defineComponent,
|
10
11
|
onMounted,
|
11
|
-
} from 'vue
|
12
|
+
} from 'vue'
|
12
13
|
|
13
14
|
declare module '@tanstack/form-core' {
|
14
15
|
// eslint-disable-next-line no-shadow
|
@@ -19,7 +20,7 @@ declare module '@tanstack/form-core' {
|
|
19
20
|
useField: UseField<TFormData, ValidatorType>
|
20
21
|
useStore: <TSelected = NoInfer<FormState<TFormData>>>(
|
21
22
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
|
22
|
-
) => TSelected
|
23
|
+
) => Readonly<Ref<TSelected>>
|
23
24
|
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(
|
24
25
|
props: {
|
25
26
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected
|