@tanstack/vue-form 0.2.0
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/LICENSE +21 -0
- package/README.md +35 -0
- package/build/legacy/createFormFactory-161e85f9.d.ts +42 -0
- package/build/legacy/createFormFactory.cjs +42 -0
- package/build/legacy/createFormFactory.cjs.map +1 -0
- package/build/legacy/createFormFactory.d.cts +3 -0
- package/build/legacy/createFormFactory.d.ts +3 -0
- package/build/legacy/createFormFactory.js +17 -0
- package/build/legacy/createFormFactory.js.map +1 -0
- package/build/legacy/formContext.cjs +46 -0
- package/build/legacy/formContext.cjs.map +1 -0
- package/build/legacy/formContext.d.cts +14 -0
- package/build/legacy/formContext.d.ts +14 -0
- package/build/legacy/formContext.js +19 -0
- package/build/legacy/formContext.js.map +1 -0
- package/build/legacy/index.cjs +33 -0
- package/build/legacy/index.cjs.map +1 -0
- package/build/legacy/index.d.cts +23 -0
- package/build/legacy/index.d.ts +23 -0
- package/build/legacy/index.js +7 -0
- package/build/legacy/index.js.map +1 -0
- package/build/legacy/types.cjs +19 -0
- package/build/legacy/types.cjs.map +1 -0
- package/build/legacy/types.d.cts +3 -0
- package/build/legacy/types.d.ts +3 -0
- package/build/legacy/types.js +1 -0
- package/build/legacy/types.js.map +1 -0
- package/build/lib/createFormFactory.d.ts +9 -0
- package/build/lib/createFormFactory.d.ts.map +1 -0
- package/build/lib/createFormFactory.js +12 -0
- package/build/lib/formContext.d.ts +12 -0
- package/build/lib/formContext.d.ts.map +1 -0
- package/build/lib/formContext.js +12 -0
- package/build/lib/index.d.ts +6 -0
- package/build/lib/index.d.ts.map +1 -0
- package/build/lib/index.js +5 -0
- package/build/lib/tests/useField.test.d.ts +3 -0
- package/build/lib/tests/useField.test.d.ts.map +1 -0
- package/build/lib/tests/useField.test.jsx +109 -0
- package/build/lib/tests/useForm.test.d.ts +3 -0
- package/build/lib/tests/useForm.test.d.ts.map +1 -0
- package/build/lib/tests/useForm.test.jsx +71 -0
- package/build/lib/tests/utils.d.ts +2 -0
- package/build/lib/tests/utils.d.ts.map +1 -0
- package/build/lib/tests/utils.js +5 -0
- package/build/lib/types.d.ts +2 -0
- package/build/lib/types.d.ts.map +1 -0
- package/build/lib/types.js +1 -0
- package/build/lib/useField.d.ts +33 -0
- package/build/lib/useField.d.ts.map +1 -0
- package/build/lib/useField.jsx +39 -0
- package/build/lib/useForm.d.ts +17 -0
- package/build/lib/useForm.d.ts.map +1 -0
- package/build/lib/useForm.jsx +35 -0
- package/build/modern/createFormFactory-161e85f9.d.ts +42 -0
- package/build/modern/createFormFactory.cjs +42 -0
- package/build/modern/createFormFactory.cjs.map +1 -0
- package/build/modern/createFormFactory.d.cts +3 -0
- package/build/modern/createFormFactory.d.ts +3 -0
- package/build/modern/createFormFactory.js +17 -0
- package/build/modern/createFormFactory.js.map +1 -0
- package/build/modern/formContext.cjs +46 -0
- package/build/modern/formContext.cjs.map +1 -0
- package/build/modern/formContext.d.cts +14 -0
- package/build/modern/formContext.d.ts +14 -0
- package/build/modern/formContext.js +19 -0
- package/build/modern/formContext.js.map +1 -0
- package/build/modern/index.cjs +33 -0
- package/build/modern/index.cjs.map +1 -0
- package/build/modern/index.d.cts +23 -0
- package/build/modern/index.d.ts +23 -0
- package/build/modern/index.js +7 -0
- package/build/modern/index.js.map +1 -0
- package/build/modern/types.cjs +19 -0
- package/build/modern/types.cjs.map +1 -0
- package/build/modern/types.d.cts +3 -0
- package/build/modern/types.d.ts +3 -0
- package/build/modern/types.js +1 -0
- package/build/modern/types.js.map +1 -0
- package/package.json +78 -0
- package/src/createFormFactory.ts +23 -0
- package/src/formContext.ts +23 -0
- package/src/index.ts +5 -0
- package/src/tests/useField.test.tsx +240 -0
- package/src/tests/useForm.test.tsx +129 -0
- package/src/tests/utils.ts +5 -0
- package/src/types.ts +1 -0
- package/src/useField.tsx +151 -0
- package/src/useForm.tsx +63 -0
package/package.json
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
{
|
2
|
+
"name": "@tanstack/vue-form",
|
3
|
+
"version": "0.2.0",
|
4
|
+
"description": "Powerful, type-safe forms for Vue.",
|
5
|
+
"author": "tannerlinsley",
|
6
|
+
"license": "MIT",
|
7
|
+
"repository": "tanstack/form",
|
8
|
+
"homepage": "https://tanstack.com/form",
|
9
|
+
"funding": {
|
10
|
+
"type": "github",
|
11
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
12
|
+
},
|
13
|
+
"type": "module",
|
14
|
+
"types": "build/legacy/index.d.ts",
|
15
|
+
"main": "build/legacy/index.cjs",
|
16
|
+
"module": "build/legacy/index.js",
|
17
|
+
"exports": {
|
18
|
+
".": {
|
19
|
+
"import": {
|
20
|
+
"types": "./build/modern/index.d.ts",
|
21
|
+
"default": "./build/modern/index.js"
|
22
|
+
},
|
23
|
+
"require": {
|
24
|
+
"types": "./build/modern/index.d.cts",
|
25
|
+
"default": "./build/modern/index.cjs"
|
26
|
+
}
|
27
|
+
},
|
28
|
+
"./package.json": "./package.json"
|
29
|
+
},
|
30
|
+
"sideEffects": false,
|
31
|
+
"nx": {
|
32
|
+
"targets": {
|
33
|
+
"test:build": {
|
34
|
+
"dependsOn": [
|
35
|
+
"build"
|
36
|
+
]
|
37
|
+
}
|
38
|
+
}
|
39
|
+
},
|
40
|
+
"files": [
|
41
|
+
"build",
|
42
|
+
"src"
|
43
|
+
],
|
44
|
+
"dependencies": {
|
45
|
+
"@tanstack/store": "0.1.3",
|
46
|
+
"@tanstack/vue-store": "0.1.3",
|
47
|
+
"vue-demi": "^0.14.6",
|
48
|
+
"@tanstack/form-core": "0.2.0"
|
49
|
+
},
|
50
|
+
"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"
|
55
|
+
},
|
56
|
+
"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
|
+
}
|
64
|
+
},
|
65
|
+
"scripts": {
|
66
|
+
"clean": "rimraf ./build && rimraf ./coverage",
|
67
|
+
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
68
|
+
"test:types": "tsc",
|
69
|
+
"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",
|
74
|
+
"test:lib:dev": "pnpm run test:lib --watch",
|
75
|
+
"test:build": "publint --strict",
|
76
|
+
"build": "tsup"
|
77
|
+
}
|
78
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import type { FormApi, FormOptions } from '@tanstack/form-core'
|
2
|
+
|
3
|
+
import { type UseField, type FieldComponent, Field, useField } from './useField'
|
4
|
+
import { useForm } from './useForm'
|
5
|
+
|
6
|
+
export type FormFactory<TFormData> = {
|
7
|
+
useForm: (opts?: FormOptions<TFormData>) => FormApi<TFormData>
|
8
|
+
useField: UseField<TFormData>
|
9
|
+
Field: FieldComponent<TFormData, TFormData>
|
10
|
+
}
|
11
|
+
|
12
|
+
export function createFormFactory<TFormData>(
|
13
|
+
defaultOpts?: FormOptions<TFormData>,
|
14
|
+
): FormFactory<TFormData> {
|
15
|
+
return {
|
16
|
+
useForm: (opts) => {
|
17
|
+
const formOptions = Object.assign({}, defaultOpts, opts)
|
18
|
+
return useForm<TFormData>(formOptions)
|
19
|
+
},
|
20
|
+
useField: useField as any,
|
21
|
+
Field: Field as any,
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import type { FormApi } from '@tanstack/form-core'
|
2
|
+
import { inject, provide } from 'vue-demi'
|
3
|
+
|
4
|
+
export type FormContext = {
|
5
|
+
formApi: FormApi<any>
|
6
|
+
parentFieldName?: string
|
7
|
+
} | null
|
8
|
+
|
9
|
+
export const formContext = Symbol('FormContext')
|
10
|
+
|
11
|
+
export function provideFormContext(val: FormContext) {
|
12
|
+
provide(formContext, val)
|
13
|
+
}
|
14
|
+
|
15
|
+
export function useFormContext() {
|
16
|
+
const formApi = inject(formContext) as FormContext
|
17
|
+
|
18
|
+
if (!formApi) {
|
19
|
+
throw new Error(`You are trying to use the form API outside of a form!`)
|
20
|
+
}
|
21
|
+
|
22
|
+
return formApi
|
23
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
/// <reference lib="dom" />
|
2
|
+
import { h, defineComponent } from 'vue-demi'
|
3
|
+
import { render, waitFor } from '@testing-library/vue'
|
4
|
+
import '@testing-library/jest-dom'
|
5
|
+
import {
|
6
|
+
createFormFactory,
|
7
|
+
type FieldApi,
|
8
|
+
provideFormContext,
|
9
|
+
useForm,
|
10
|
+
} from '../index'
|
11
|
+
import userEvent from '@testing-library/user-event'
|
12
|
+
import * as React from 'react'
|
13
|
+
import { sleep } from './utils'
|
14
|
+
|
15
|
+
const user = userEvent.setup()
|
16
|
+
|
17
|
+
describe('useField', () => {
|
18
|
+
it('should allow to set default value', async () => {
|
19
|
+
type Person = {
|
20
|
+
firstName: string
|
21
|
+
lastName: string
|
22
|
+
}
|
23
|
+
|
24
|
+
const formFactory = createFormFactory<Person>()
|
25
|
+
|
26
|
+
const Comp = defineComponent(() => {
|
27
|
+
const form = formFactory.useForm()
|
28
|
+
|
29
|
+
provideFormContext({ formApi: form })
|
30
|
+
|
31
|
+
return () => (
|
32
|
+
<form.Field name="firstName" defaultValue="FirstName">
|
33
|
+
{(field: FieldApi<string, Person>) => (
|
34
|
+
<input
|
35
|
+
data-testid={'fieldinput'}
|
36
|
+
value={field.state.value}
|
37
|
+
onBlur={field.handleBlur}
|
38
|
+
onInput={(e) =>
|
39
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
40
|
+
}
|
41
|
+
/>
|
42
|
+
)}
|
43
|
+
</form.Field>
|
44
|
+
)
|
45
|
+
})
|
46
|
+
|
47
|
+
const { getByTestId } = render(Comp)
|
48
|
+
const input = getByTestId('fieldinput')
|
49
|
+
await waitFor(() => expect(input).toHaveValue('FirstName'))
|
50
|
+
})
|
51
|
+
|
52
|
+
it('should not validate on change if isTouched is false', async () => {
|
53
|
+
type Person = {
|
54
|
+
firstName: string
|
55
|
+
lastName: string
|
56
|
+
}
|
57
|
+
const error = 'Please enter a different value'
|
58
|
+
|
59
|
+
const formFactory = createFormFactory<Person>()
|
60
|
+
|
61
|
+
const Comp = defineComponent(() => {
|
62
|
+
const form = formFactory.useForm()
|
63
|
+
|
64
|
+
provideFormContext({ formApi: form })
|
65
|
+
|
66
|
+
return () => (
|
67
|
+
<form.Field
|
68
|
+
name="firstName"
|
69
|
+
onChange={(value) => (value === 'other' ? error : undefined)}
|
70
|
+
>
|
71
|
+
{(field: FieldApi<string, Person>) => (
|
72
|
+
<div>
|
73
|
+
<input
|
74
|
+
data-testid="fieldinput"
|
75
|
+
name={field.name}
|
76
|
+
value={field.state.value}
|
77
|
+
onBlur={field.handleBlur}
|
78
|
+
onInput={(e) =>
|
79
|
+
field.setValue((e.target as HTMLInputElement).value)
|
80
|
+
}
|
81
|
+
/>
|
82
|
+
<p>{field.getMeta().error}</p>
|
83
|
+
</div>
|
84
|
+
)}
|
85
|
+
</form.Field>
|
86
|
+
)
|
87
|
+
})
|
88
|
+
|
89
|
+
const { getByTestId, queryByText } = render(Comp)
|
90
|
+
const input = getByTestId('fieldinput')
|
91
|
+
await user.type(input, 'other')
|
92
|
+
expect(queryByText(error)).not.toBeInTheDocument()
|
93
|
+
})
|
94
|
+
|
95
|
+
it('should validate on change if isTouched is true', async () => {
|
96
|
+
type Person = {
|
97
|
+
firstName: string
|
98
|
+
lastName: string
|
99
|
+
}
|
100
|
+
const error = 'Please enter a different value'
|
101
|
+
|
102
|
+
const formFactory = createFormFactory<Person>()
|
103
|
+
|
104
|
+
const Comp = defineComponent(() => {
|
105
|
+
const form = formFactory.useForm()
|
106
|
+
|
107
|
+
provideFormContext({ formApi: form })
|
108
|
+
|
109
|
+
return () => (
|
110
|
+
<form.Field
|
111
|
+
name="firstName"
|
112
|
+
onChange={(value) => (value === 'other' ? error : undefined)}
|
113
|
+
>
|
114
|
+
{(field: FieldApi<string, Person>) => (
|
115
|
+
<div>
|
116
|
+
<input
|
117
|
+
data-testid="fieldinput"
|
118
|
+
name={field.name}
|
119
|
+
value={field.state.value}
|
120
|
+
onBlur={field.handleBlur}
|
121
|
+
onInput={(e) =>
|
122
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
123
|
+
}
|
124
|
+
/>
|
125
|
+
<p>{field.getMeta().error}</p>
|
126
|
+
</div>
|
127
|
+
)}
|
128
|
+
</form.Field>
|
129
|
+
)
|
130
|
+
})
|
131
|
+
|
132
|
+
const { getByTestId, getByText, queryByText } = render(Comp)
|
133
|
+
const input = getByTestId('fieldinput')
|
134
|
+
expect(queryByText(error)).not.toBeInTheDocument()
|
135
|
+
await user.type(input, 'other')
|
136
|
+
expect(getByText(error)).toBeInTheDocument()
|
137
|
+
})
|
138
|
+
|
139
|
+
it('should validate async on change', async () => {
|
140
|
+
type Person = {
|
141
|
+
firstName: string
|
142
|
+
lastName: string
|
143
|
+
}
|
144
|
+
const error = 'Please enter a different value'
|
145
|
+
|
146
|
+
const formFactory = createFormFactory<Person>()
|
147
|
+
|
148
|
+
const Comp = defineComponent(() => {
|
149
|
+
const form = formFactory.useForm()
|
150
|
+
|
151
|
+
provideFormContext({ formApi: form })
|
152
|
+
|
153
|
+
return () => (
|
154
|
+
<form.Field
|
155
|
+
name="firstName"
|
156
|
+
defaultMeta={{ isTouched: true }}
|
157
|
+
onChangeAsync={async () => {
|
158
|
+
await sleep(10)
|
159
|
+
return error
|
160
|
+
}}
|
161
|
+
>
|
162
|
+
{(field: FieldApi<string, Person>) => (
|
163
|
+
<div>
|
164
|
+
<input
|
165
|
+
data-testid="fieldinput"
|
166
|
+
name={field.name}
|
167
|
+
value={field.state.value}
|
168
|
+
onBlur={field.handleBlur}
|
169
|
+
onInput={(e) =>
|
170
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
171
|
+
}
|
172
|
+
/>
|
173
|
+
<p>{field.getMeta().error}</p>
|
174
|
+
</div>
|
175
|
+
)}
|
176
|
+
</form.Field>
|
177
|
+
)
|
178
|
+
})
|
179
|
+
|
180
|
+
const { getByTestId, getByText, queryByText } = render(Comp)
|
181
|
+
const input = getByTestId('fieldinput')
|
182
|
+
expect(queryByText(error)).not.toBeInTheDocument()
|
183
|
+
await user.type(input, 'other')
|
184
|
+
await waitFor(() => getByText(error))
|
185
|
+
expect(getByText(error)).toBeInTheDocument()
|
186
|
+
})
|
187
|
+
|
188
|
+
it('should validate async on change with debounce', async () => {
|
189
|
+
type Person = {
|
190
|
+
firstName: string
|
191
|
+
lastName: string
|
192
|
+
}
|
193
|
+
|
194
|
+
const mockFn = vi.fn()
|
195
|
+
const error = 'Please enter a different value'
|
196
|
+
const formFactory = createFormFactory<Person>()
|
197
|
+
|
198
|
+
const Comp = defineComponent(() => {
|
199
|
+
const form = formFactory.useForm()
|
200
|
+
|
201
|
+
provideFormContext({ formApi: form })
|
202
|
+
|
203
|
+
return () => (
|
204
|
+
<form.Field
|
205
|
+
name="firstName"
|
206
|
+
defaultMeta={{ isTouched: true }}
|
207
|
+
onChangeAsyncDebounceMs={100}
|
208
|
+
onChangeAsync={async () => {
|
209
|
+
mockFn()
|
210
|
+
await sleep(10)
|
211
|
+
return error
|
212
|
+
}}
|
213
|
+
>
|
214
|
+
{(field: FieldApi<string, Person>) => (
|
215
|
+
<div>
|
216
|
+
<input
|
217
|
+
data-testid="fieldinput"
|
218
|
+
name={field.name}
|
219
|
+
value={field.state.value}
|
220
|
+
onBlur={field.handleBlur}
|
221
|
+
onInput={(e) =>
|
222
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
223
|
+
}
|
224
|
+
/>
|
225
|
+
<p>{field.getMeta().error}</p>
|
226
|
+
</div>
|
227
|
+
)}
|
228
|
+
</form.Field>
|
229
|
+
)
|
230
|
+
})
|
231
|
+
|
232
|
+
const { getByTestId, getByText } = render(<Comp />)
|
233
|
+
const input = getByTestId('fieldinput')
|
234
|
+
await user.type(input, 'other')
|
235
|
+
// mockFn will have been called 5 times without onChangeAsyncDebounceMs
|
236
|
+
expect(mockFn).toHaveBeenCalledTimes(0)
|
237
|
+
await waitFor(() => getByText(error))
|
238
|
+
expect(getByText(error)).toBeInTheDocument()
|
239
|
+
})
|
240
|
+
})
|
@@ -0,0 +1,129 @@
|
|
1
|
+
/// <reference lib="dom" />
|
2
|
+
import { h, defineComponent, ref } from 'vue-demi'
|
3
|
+
import { render, waitFor } from '@testing-library/vue'
|
4
|
+
import '@testing-library/jest-dom'
|
5
|
+
import {
|
6
|
+
createFormFactory,
|
7
|
+
type FieldApi,
|
8
|
+
provideFormContext,
|
9
|
+
useForm,
|
10
|
+
} from '../index'
|
11
|
+
import userEvent from '@testing-library/user-event'
|
12
|
+
import * as React from 'react'
|
13
|
+
|
14
|
+
const user = userEvent.setup()
|
15
|
+
|
16
|
+
describe('useForm', () => {
|
17
|
+
it('preserved field state', async () => {
|
18
|
+
type Person = {
|
19
|
+
firstName: string
|
20
|
+
lastName: string
|
21
|
+
}
|
22
|
+
|
23
|
+
const formFactory = createFormFactory<Person>()
|
24
|
+
|
25
|
+
const Comp = defineComponent(() => {
|
26
|
+
const form = formFactory.useForm()
|
27
|
+
|
28
|
+
provideFormContext({ formApi: form })
|
29
|
+
|
30
|
+
return () => (
|
31
|
+
<form.Field name="firstName" defaultValue="">
|
32
|
+
{(field: FieldApi<string, Person>) => (
|
33
|
+
<input
|
34
|
+
data-testid={'fieldinput'}
|
35
|
+
value={field.state.value}
|
36
|
+
onBlur={field.handleBlur}
|
37
|
+
onInput={(e) =>
|
38
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
39
|
+
}
|
40
|
+
/>
|
41
|
+
)}
|
42
|
+
</form.Field>
|
43
|
+
)
|
44
|
+
})
|
45
|
+
|
46
|
+
const { getByTestId, queryByText } = render(Comp)
|
47
|
+
const input = getByTestId('fieldinput')
|
48
|
+
expect(queryByText('FirstName')).not.toBeInTheDocument()
|
49
|
+
await user.type(input, 'FirstName')
|
50
|
+
expect(input).toHaveValue('FirstName')
|
51
|
+
})
|
52
|
+
|
53
|
+
it('should allow default values to be set', async () => {
|
54
|
+
type Person = {
|
55
|
+
firstName: string
|
56
|
+
lastName: string
|
57
|
+
}
|
58
|
+
|
59
|
+
const formFactory = createFormFactory<Person>()
|
60
|
+
|
61
|
+
const Comp = defineComponent(() => {
|
62
|
+
const form = formFactory.useForm({
|
63
|
+
defaultValues: {
|
64
|
+
firstName: 'FirstName',
|
65
|
+
lastName: 'LastName',
|
66
|
+
},
|
67
|
+
})
|
68
|
+
form.provideFormContext()
|
69
|
+
|
70
|
+
return () => (
|
71
|
+
<form.Field name="firstName" defaultValue="">
|
72
|
+
{(field: FieldApi<string, Person>) => <p>{field.state.value}</p>}
|
73
|
+
</form.Field>
|
74
|
+
)
|
75
|
+
})
|
76
|
+
|
77
|
+
const { findByText, queryByText } = render(Comp)
|
78
|
+
expect(await findByText('FirstName')).toBeInTheDocument()
|
79
|
+
expect(queryByText('LastName')).not.toBeInTheDocument()
|
80
|
+
})
|
81
|
+
|
82
|
+
it('should handle submitting properly', async () => {
|
83
|
+
const Comp = defineComponent(() => {
|
84
|
+
const submittedData = ref<{ firstName: string }>()
|
85
|
+
|
86
|
+
const form = useForm({
|
87
|
+
defaultValues: {
|
88
|
+
firstName: 'FirstName',
|
89
|
+
},
|
90
|
+
onSubmit: (data) => {
|
91
|
+
submittedData.value = data
|
92
|
+
},
|
93
|
+
})
|
94
|
+
form.provideFormContext()
|
95
|
+
|
96
|
+
return () => (
|
97
|
+
<form.Provider>
|
98
|
+
<form.Field name="firstName">
|
99
|
+
{(field: FieldApi<string, { firstName: string }>) => {
|
100
|
+
return (
|
101
|
+
<input
|
102
|
+
value={field.state.value}
|
103
|
+
onBlur={field.handleBlur}
|
104
|
+
onChange={(e) =>
|
105
|
+
field.handleChange((e.target as HTMLInputElement).value)
|
106
|
+
}
|
107
|
+
placeholder={'First name'}
|
108
|
+
/>
|
109
|
+
)
|
110
|
+
}}
|
111
|
+
</form.Field>
|
112
|
+
<button onClick={form.handleSubmit}>Submit</button>
|
113
|
+
{submittedData.value && (
|
114
|
+
<p>Submitted data: {submittedData.value.firstName}</p>
|
115
|
+
)}
|
116
|
+
</form.Provider>
|
117
|
+
)
|
118
|
+
})
|
119
|
+
|
120
|
+
const { findByPlaceholderText, getByText } = render(Comp)
|
121
|
+
const input = await findByPlaceholderText('First name')
|
122
|
+
await user.clear(input)
|
123
|
+
await user.type(input, 'OtherName')
|
124
|
+
await user.click(getByText('Submit'))
|
125
|
+
await waitFor(() =>
|
126
|
+
expect(getByText('Submitted data: OtherName')).toBeInTheDocument(),
|
127
|
+
)
|
128
|
+
})
|
129
|
+
})
|
package/src/types.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export type NoInfer<T> = [T][T extends any ? 0 : never]
|
package/src/useField.tsx
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
import {
|
2
|
+
type DeepKeys,
|
3
|
+
type DeepValue,
|
4
|
+
FieldApi,
|
5
|
+
type FieldOptions,
|
6
|
+
type Narrow,
|
7
|
+
} from '@tanstack/form-core'
|
8
|
+
import { useStore } from '@tanstack/vue-store'
|
9
|
+
import {
|
10
|
+
type SetupContext,
|
11
|
+
defineComponent,
|
12
|
+
type Ref,
|
13
|
+
onMounted,
|
14
|
+
onUnmounted,
|
15
|
+
watch,
|
16
|
+
} from 'vue-demi'
|
17
|
+
import { provideFormContext, useFormContext } from './formContext'
|
18
|
+
|
19
|
+
declare module '@tanstack/form-core' {
|
20
|
+
// eslint-disable-next-line no-shadow
|
21
|
+
interface FieldApi<TData, TFormData> {
|
22
|
+
Field: FieldComponent<TData, TFormData>
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
export interface UseFieldOptions<TData, TFormData>
|
27
|
+
extends FieldOptions<TData, TFormData> {
|
28
|
+
mode?: 'value' | 'array'
|
29
|
+
}
|
30
|
+
|
31
|
+
export type UseField<TFormData> = <TField extends DeepKeys<TFormData>>(
|
32
|
+
opts?: { name: Narrow<TField> } & UseFieldOptions<
|
33
|
+
DeepValue<TFormData, TField>,
|
34
|
+
TFormData
|
35
|
+
>,
|
36
|
+
) => FieldApi<DeepValue<TFormData, TField>, TFormData>
|
37
|
+
|
38
|
+
export function useField<TData, TFormData>(
|
39
|
+
opts: UseFieldOptions<TData, TFormData>,
|
40
|
+
): {
|
41
|
+
api: FieldApi<TData, TFormData>
|
42
|
+
state: Readonly<Ref<FieldApi<TData, TFormData>['state']>>
|
43
|
+
} {
|
44
|
+
// Get the form API either manually or from context
|
45
|
+
const { formApi, parentFieldName } = useFormContext()
|
46
|
+
|
47
|
+
const fieldApi = (() => {
|
48
|
+
const name = (
|
49
|
+
typeof opts.index === 'number'
|
50
|
+
? [parentFieldName, opts.index, opts.name]
|
51
|
+
: [parentFieldName, opts.name]
|
52
|
+
)
|
53
|
+
.filter((d) => d !== undefined)
|
54
|
+
.join('.')
|
55
|
+
|
56
|
+
const api = new FieldApi({ ...opts, form: formApi, name: name as never })
|
57
|
+
|
58
|
+
api.Field = Field as never
|
59
|
+
|
60
|
+
return api
|
61
|
+
})()
|
62
|
+
|
63
|
+
const fieldState = useStore(fieldApi.store, (state) => state)
|
64
|
+
|
65
|
+
let cleanup!: () => void
|
66
|
+
onMounted(() => {
|
67
|
+
cleanup = fieldApi.mount()
|
68
|
+
})
|
69
|
+
|
70
|
+
onUnmounted(() => {
|
71
|
+
cleanup()
|
72
|
+
})
|
73
|
+
|
74
|
+
watch(
|
75
|
+
() => opts,
|
76
|
+
() => {
|
77
|
+
// Keep options up to date as they are rendered
|
78
|
+
fieldApi.update({ ...opts, form: formApi } as never)
|
79
|
+
},
|
80
|
+
)
|
81
|
+
|
82
|
+
return { api: fieldApi, state: fieldState } as never
|
83
|
+
}
|
84
|
+
|
85
|
+
// export type FieldValue<TFormData, TField> = TFormData extends any[]
|
86
|
+
// ? TField extends `[${infer TIndex extends number | 'i'}].${infer TRest}`
|
87
|
+
// ? DeepValue<TFormData[TIndex extends 'i' ? number : TIndex], TRest>
|
88
|
+
// : TField extends `[${infer TIndex extends number | 'i'}]`
|
89
|
+
// ? TFormData[TIndex extends 'i' ? number : TIndex]
|
90
|
+
// : never
|
91
|
+
// : TField extends `${infer TPrefix}[${infer TIndex extends
|
92
|
+
// | number
|
93
|
+
// | 'i'}].${infer TRest}`
|
94
|
+
// ? DeepValue<
|
95
|
+
// DeepValue<TFormData, TPrefix>[TIndex extends 'i' ? number : TIndex],
|
96
|
+
// TRest
|
97
|
+
// >
|
98
|
+
// : TField extends `${infer TPrefix}[${infer TIndex extends number | 'i'}]`
|
99
|
+
// ? DeepValue<TFormData, TPrefix>[TIndex extends 'i' ? number : TIndex]
|
100
|
+
// : DeepValue<TFormData, TField>
|
101
|
+
|
102
|
+
export type FieldValue<TFormData, TField> = TFormData extends any[]
|
103
|
+
? unknown extends TField
|
104
|
+
? TFormData[number]
|
105
|
+
: DeepValue<TFormData[number], TField>
|
106
|
+
: DeepValue<TFormData, TField>
|
107
|
+
|
108
|
+
// type Test1 = FieldValue<{ foo: { bar: string }[] }, 'foo'>
|
109
|
+
// // ^?
|
110
|
+
// type Test2 = FieldValue<{ foo: { bar: string }[] }, 'foo[i]'>
|
111
|
+
// // ^?
|
112
|
+
// type Test3 = FieldValue<{ foo: { bar: string }[] }, 'foo[2].bar'>
|
113
|
+
// // ^?
|
114
|
+
|
115
|
+
export type FieldComponent<TParentData, TFormData> = <TField>(
|
116
|
+
fieldOptions: {
|
117
|
+
children?: (
|
118
|
+
fieldApi: FieldApi<FieldValue<TParentData, TField>, TFormData>,
|
119
|
+
) => any
|
120
|
+
} & Omit<
|
121
|
+
UseFieldOptions<FieldValue<TParentData, TField>, TFormData>,
|
122
|
+
'name' | 'index'
|
123
|
+
> &
|
124
|
+
(TParentData extends any[]
|
125
|
+
? {
|
126
|
+
name?: TField extends undefined ? TField : DeepKeys<TParentData>
|
127
|
+
index: number
|
128
|
+
}
|
129
|
+
: {
|
130
|
+
name: TField extends undefined ? TField : DeepKeys<TParentData>
|
131
|
+
index?: never
|
132
|
+
}),
|
133
|
+
context: SetupContext,
|
134
|
+
) => any
|
135
|
+
|
136
|
+
export const Field = defineComponent(
|
137
|
+
<TData, TFormData>(
|
138
|
+
fieldOptions: UseFieldOptions<TData, TFormData>,
|
139
|
+
context: SetupContext,
|
140
|
+
) => {
|
141
|
+
const fieldApi = useField({ ...fieldOptions, ...context.attrs })
|
142
|
+
|
143
|
+
provideFormContext({
|
144
|
+
formApi: fieldApi.api.form,
|
145
|
+
parentFieldName: fieldApi.api.name,
|
146
|
+
} as never)
|
147
|
+
|
148
|
+
return () => context.slots.default!(fieldApi.api, fieldApi.state.value)
|
149
|
+
},
|
150
|
+
{ name: 'Field', inheritAttrs: false },
|
151
|
+
)
|