@tanstack/vue-form 0.10.1 → 0.10.3

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 (46) hide show
  1. package/build/legacy/createFormFactory.d.cts +1 -1
  2. package/build/legacy/createFormFactory.d.ts +1 -1
  3. package/build/legacy/formContext.cjs +3 -3
  4. package/build/legacy/formContext.cjs.map +1 -1
  5. package/build/legacy/formContext.js +1 -1
  6. package/build/legacy/formContext.js.map +1 -1
  7. package/build/legacy/index.d.cts +1 -1
  8. package/build/legacy/index.d.ts +1 -1
  9. package/build/legacy/useField.cjs +5 -5
  10. package/build/legacy/useField.cjs.map +1 -1
  11. package/build/legacy/useField.d.cts +1 -1
  12. package/build/legacy/useField.d.ts +1 -1
  13. package/build/legacy/useField.js +1 -1
  14. package/build/legacy/useField.js.map +1 -1
  15. package/build/legacy/useForm.cjs +5 -5
  16. package/build/legacy/useForm.cjs.map +1 -1
  17. package/build/legacy/useForm.d.cts +2 -2
  18. package/build/legacy/useForm.d.ts +2 -2
  19. package/build/legacy/useForm.js +1 -1
  20. package/build/legacy/useForm.js.map +1 -1
  21. package/build/modern/createFormFactory.d.cts +1 -1
  22. package/build/modern/createFormFactory.d.ts +1 -1
  23. package/build/modern/formContext.cjs +3 -3
  24. package/build/modern/formContext.cjs.map +1 -1
  25. package/build/modern/formContext.js +1 -1
  26. package/build/modern/formContext.js.map +1 -1
  27. package/build/modern/index.d.cts +1 -1
  28. package/build/modern/index.d.ts +1 -1
  29. package/build/modern/useField.cjs +5 -5
  30. package/build/modern/useField.cjs.map +1 -1
  31. package/build/modern/useField.d.cts +1 -1
  32. package/build/modern/useField.d.ts +1 -1
  33. package/build/modern/useField.js +1 -1
  34. package/build/modern/useField.js.map +1 -1
  35. package/build/modern/useForm.cjs +5 -5
  36. package/build/modern/useForm.cjs.map +1 -1
  37. package/build/modern/useForm.d.cts +2 -2
  38. package/build/modern/useForm.d.ts +2 -2
  39. package/build/modern/useForm.js +1 -1
  40. package/build/modern/useForm.js.map +1 -1
  41. package/package.json +5 -18
  42. package/src/formContext.ts +1 -1
  43. package/src/tests/useField.test.tsx +2 -8
  44. package/src/tests/useForm.test.tsx +388 -26
  45. package/src/useField.tsx +2 -2
  46. 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-demi';
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-demi';
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<{
@@ -6,7 +6,7 @@ import { provideFormContext } from "./formContext.js";
6
6
  import {
7
7
  defineComponent,
8
8
  onMounted
9
- } from "vue-demi";
9
+ } from "vue";
10
10
  function useForm(opts) {
11
11
  const formApi = (() => {
12
12
  const api = new FormApi(opts);
@@ -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-demi'\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 ) => 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,EAIE;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":[]}
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.1",
3
+ "version": "0.10.3",
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
- "vue-demi": "^0.14.6",
48
- "@tanstack/form-core": "0.10.1"
47
+ "@tanstack/form-core": "0.10.3"
49
48
  },
50
49
  "devDependencies": {
51
- "@vue/composition-api": "1.7.2",
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
- "@vue/composition-api": "^1.1.2",
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": "pnpm run test:3",
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"
@@ -1,5 +1,5 @@
1
1
  import type { FormApi } from '@tanstack/form-core'
2
- import { inject, provide } from 'vue-demi'
2
+ import { inject, provide } from 'vue'
3
3
 
4
4
  export type FormContext = {
5
5
  formApi: FormApi<any, unknown>
@@ -1,15 +1,9 @@
1
1
  /// <reference lib="dom" />
2
- import { h, defineComponent } from 'vue-demi'
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 '../index'
11
- import userEvent from '@testing-library/user-event'
12
- import * as React from 'react'
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
- <form.Provider>
102
+ <div>
106
103
  <form.Field name="firstName">
107
104
  {({
108
105
  field,
109
106
  }: {
110
- field: FieldApi<{ firstName: string }, 'firstName', never, never>
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
- onChange={(e) =>
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
- </form.Provider>
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
- <form.Provider>
158
+ <div>
161
159
  <h1>{formMounted.value ? 'Form mounted' : 'Not mounted'}</h1>
162
- </form.Provider>
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-demi'
5
- import type { SlotsType, SetupContext, Ref } from 'vue-demi'
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-demi'
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