@tanstack/solid-form 0.6.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.
@@ -0,0 +1,376 @@
1
+ import { render, waitFor } from '@solidjs/testing-library'
2
+ import userEvent from '@testing-library/user-event'
3
+ import '@testing-library/jest-dom'
4
+ import { createFormFactory } from '..'
5
+ import { sleep } from './utils'
6
+
7
+ const user = userEvent.setup()
8
+
9
+ describe('createField', () => {
10
+ it('should allow to set default value', () => {
11
+ type Person = {
12
+ firstName: string
13
+ lastName: string
14
+ }
15
+
16
+ const formFactory = createFormFactory<Person, unknown>()
17
+
18
+ function Comp() {
19
+ const form = formFactory.createForm()
20
+
21
+ return (
22
+ <form.Provider>
23
+ <form.Field
24
+ name="firstName"
25
+ defaultValue="FirstName"
26
+ children={(field) => {
27
+ return (
28
+ <input
29
+ data-testid="fieldinput"
30
+ value={field().state.value}
31
+ onBlur={field().handleBlur}
32
+ onInput={(e) => field().handleChange(e.currentTarget.value)}
33
+ />
34
+ )
35
+ }}
36
+ />
37
+ </form.Provider>
38
+ )
39
+ }
40
+
41
+ const { getByTestId } = render(() => <Comp />)
42
+ const input = getByTestId('fieldinput')
43
+ expect(input).toHaveValue('FirstName')
44
+ })
45
+
46
+ it('should use field default value first', async () => {
47
+ type Person = {
48
+ firstName: string
49
+ lastName: string
50
+ }
51
+
52
+ const formFactory = createFormFactory<Person, unknown>()
53
+
54
+ function Comp() {
55
+ const form = formFactory.createForm(() => ({
56
+ defaultValues: {
57
+ firstName: 'FirstName',
58
+ lastName: 'LastName',
59
+ },
60
+ }))
61
+
62
+ return (
63
+ <form.Provider>
64
+ <form.Field
65
+ name="firstName"
66
+ defaultValue="otherName"
67
+ children={(field) => {
68
+ return (
69
+ <input
70
+ data-testid="fieldinput"
71
+ value={field().state.value}
72
+ onBlur={field().handleBlur}
73
+ onChange={(e) => field().handleChange(e.target.value)}
74
+ />
75
+ )
76
+ }}
77
+ />
78
+ </form.Provider>
79
+ )
80
+ }
81
+
82
+ const { getByTestId } = render(<Comp />)
83
+ const input = getByTestId('fieldinput')
84
+ expect(input).toHaveValue('otherName')
85
+ })
86
+
87
+ it('should not validate on change if isTouched is false', async () => {
88
+ type Person = {
89
+ firstName: string
90
+ lastName: string
91
+ }
92
+ const error = 'Please enter a different value'
93
+
94
+ const formFactory = createFormFactory<Person, unknown>()
95
+
96
+ function Comp() {
97
+ const form = formFactory.createForm()
98
+
99
+ return (
100
+ <form.Provider>
101
+ <form.Field
102
+ name="firstName"
103
+ onChange={(value) => (value.includes('other') ? error : undefined)}
104
+ children={(field) => (
105
+ <div>
106
+ <input
107
+ data-testid="fieldinput"
108
+ name={field().name}
109
+ value={field().state.value}
110
+ onBlur={field().handleBlur}
111
+ onInput={(e) => field().setValue(e.currentTarget.value)}
112
+ />
113
+ <p>{field().getMeta().errors}</p>
114
+ </div>
115
+ )}
116
+ />
117
+ </form.Provider>
118
+ )
119
+ }
120
+
121
+ const { getByTestId, queryByText } = render(() => <Comp />)
122
+ const input = getByTestId('fieldinput')
123
+ await user.type(input, 'other')
124
+ expect(queryByText(error)).not.toBeInTheDocument()
125
+ })
126
+
127
+ it('should validate on change if isTouched is true', async () => {
128
+ type Person = {
129
+ firstName: string
130
+ lastName: string
131
+ }
132
+ const error = 'Please enter a different value'
133
+
134
+ const formFactory = createFormFactory<Person, unknown>()
135
+
136
+ function Comp() {
137
+ const form = formFactory.createForm()
138
+
139
+ return (
140
+ <form.Provider>
141
+ <form.Field
142
+ name="firstName"
143
+ defaultMeta={{ isTouched: true }}
144
+ onChange={(value) => (value.includes('other') ? error : undefined)}
145
+ children={(field) => {
146
+ return (
147
+ <div>
148
+ <input
149
+ data-testid="fieldinput"
150
+ name={field().name}
151
+ value={field().state.value}
152
+ onBlur={field().handleBlur}
153
+ onInput={(e) => field().setValue(e.currentTarget.value)}
154
+ />
155
+ <p>{field().getMeta().errorMap.onChange}</p>
156
+ </div>
157
+ )
158
+ }}
159
+ />
160
+ </form.Provider>
161
+ )
162
+ }
163
+
164
+ const { getByTestId, getByText, queryByText } = render(() => <Comp />)
165
+ const input = getByTestId('fieldinput')
166
+ expect(queryByText(error)).not.toBeInTheDocument()
167
+ await user.type(input, 'other')
168
+ expect(getByText(error)).toBeInTheDocument()
169
+ })
170
+
171
+ it('should validate on change and on blur', async () => {
172
+ type Person = {
173
+ firstName: string
174
+ lastName: string
175
+ }
176
+ const onChangeError = 'Please enter a different value (onChangeError)'
177
+ const onBlurError = 'Please enter a different value (onBlurError)'
178
+
179
+ const formFactory = createFormFactory<Person, unknown>()
180
+
181
+ function Comp() {
182
+ const form = formFactory.createForm()
183
+
184
+ return (
185
+ <form.Provider>
186
+ <form.Field
187
+ name="firstName"
188
+ defaultMeta={{ isTouched: true }}
189
+ onChange={(value) =>
190
+ value.includes('other') ? onChangeError : undefined
191
+ }
192
+ onBlur={(value) =>
193
+ value.includes('other') ? onBlurError : undefined
194
+ }
195
+ children={(field) => (
196
+ <div>
197
+ <input
198
+ data-testid="fieldinput"
199
+ name={field().name}
200
+ value={field().state.value}
201
+ onBlur={field().handleBlur}
202
+ onInput={(e) => field().handleChange(e.currentTarget.value)}
203
+ />
204
+ <p>{field().getMeta().errorMap.onChange}</p>
205
+ <p>{field().getMeta().errorMap.onBlur}</p>
206
+ </div>
207
+ )}
208
+ />
209
+ </form.Provider>
210
+ )
211
+ }
212
+
213
+ const { getByTestId, getByText, queryByText } = render(() => <Comp />)
214
+ const input = getByTestId('fieldinput')
215
+ expect(queryByText(onChangeError)).not.toBeInTheDocument()
216
+ expect(queryByText(onBlurError)).not.toBeInTheDocument()
217
+ await user.type(input, 'other')
218
+ expect(getByText(onChangeError)).toBeInTheDocument()
219
+ // @ts-expect-error unsure why the 'vitest/globals' in tsconfig doesnt work here
220
+ await user.click(document.body)
221
+ expect(queryByText(onBlurError)).toBeInTheDocument()
222
+ })
223
+
224
+ it('should validate async on change', async () => {
225
+ type Person = {
226
+ firstName: string
227
+ lastName: string
228
+ }
229
+ const error = 'Please enter a different value'
230
+
231
+ const formFactory = createFormFactory<Person, unknown>()
232
+
233
+ function Comp() {
234
+ const form = formFactory.createForm()
235
+
236
+ return (
237
+ <form.Provider>
238
+ <form.Field
239
+ name="firstName"
240
+ defaultMeta={{ isTouched: true }}
241
+ onChangeAsync={async () => {
242
+ await sleep(10)
243
+ return error
244
+ }}
245
+ children={(field) => (
246
+ <div>
247
+ <input
248
+ data-testid="fieldinput"
249
+ name={field().name}
250
+ value={field().state.value}
251
+ onBlur={field().handleBlur}
252
+ onInput={(e) => field().handleChange(e.currentTarget.value)}
253
+ />
254
+ <p>{field().getMeta().errorMap.onChange}</p>
255
+ </div>
256
+ )}
257
+ />
258
+ </form.Provider>
259
+ )
260
+ }
261
+
262
+ const { getByTestId, getByText, queryByText } = render(() => <Comp />)
263
+ const input = getByTestId('fieldinput')
264
+ expect(queryByText(error)).not.toBeInTheDocument()
265
+ await user.type(input, 'other')
266
+ await waitFor(() => getByText(error))
267
+ expect(getByText(error)).toBeInTheDocument()
268
+ })
269
+
270
+ it('should validate async on change and async on blur', async () => {
271
+ type Person = {
272
+ firstName: string
273
+ lastName: string
274
+ }
275
+ const onChangeError = 'Please enter a different value (onChangeError)'
276
+ const onBlurError = 'Please enter a different value (onBlurError)'
277
+
278
+ const formFactory = createFormFactory<Person, unknown>()
279
+
280
+ function Comp() {
281
+ const form = formFactory.createForm()
282
+
283
+ return (
284
+ <form.Provider>
285
+ <form.Field
286
+ name="firstName"
287
+ defaultMeta={{ isTouched: true }}
288
+ onChangeAsync={async () => {
289
+ await sleep(10)
290
+ return onChangeError
291
+ }}
292
+ onBlurAsync={async () => {
293
+ await sleep(10)
294
+ return onBlurError
295
+ }}
296
+ children={(field) => (
297
+ <div>
298
+ <input
299
+ data-testid="fieldinput"
300
+ name={field().name}
301
+ value={field().state.value}
302
+ onBlur={field().handleBlur}
303
+ onInput={(e) => field().handleChange(e.currentTarget.value)}
304
+ />
305
+ <p>{field().getMeta().errorMap?.onChange}</p>
306
+ <p>{field().getMeta().errorMap?.onBlur}</p>
307
+ </div>
308
+ )}
309
+ />
310
+ </form.Provider>
311
+ )
312
+ }
313
+
314
+ const { getByTestId, getByText, queryByText } = render(() => <Comp />)
315
+ const input = getByTestId('fieldinput')
316
+
317
+ expect(queryByText(onChangeError)).not.toBeInTheDocument()
318
+ expect(queryByText(onBlurError)).not.toBeInTheDocument()
319
+ await user.type(input, 'other')
320
+ await waitFor(() => getByText(onChangeError))
321
+ expect(getByText(onChangeError)).toBeInTheDocument()
322
+ // @ts-expect-error unsure why the 'vitest/globals' in tsconfig doesnt work here
323
+ await user.click(document.body)
324
+ await waitFor(() => getByText(onBlurError))
325
+ expect(getByText(onBlurError)).toBeInTheDocument()
326
+ })
327
+
328
+ it('should validate async on change with debounce', async () => {
329
+ type Person = {
330
+ firstName: string
331
+ lastName: string
332
+ }
333
+ const mockFn = vi.fn()
334
+ const error = 'Please enter a different value'
335
+ const formFactory = createFormFactory<Person, unknown>()
336
+
337
+ function Comp() {
338
+ const form = formFactory.createForm()
339
+
340
+ return (
341
+ <form.Provider>
342
+ <form.Field
343
+ name="firstName"
344
+ defaultMeta={{ isTouched: true }}
345
+ onChangeAsyncDebounceMs={100}
346
+ onChangeAsync={async () => {
347
+ mockFn()
348
+ await sleep(10)
349
+ return error
350
+ }}
351
+ children={(field) => (
352
+ <div>
353
+ <input
354
+ data-testid="fieldinput"
355
+ name={field().name}
356
+ value={field().state.value}
357
+ onBlur={field().handleBlur}
358
+ onInput={(e) => field().handleChange(e.currentTarget.value)}
359
+ />
360
+ <p>{field().getMeta().errors}</p>
361
+ </div>
362
+ )}
363
+ />
364
+ </form.Provider>
365
+ )
366
+ }
367
+
368
+ const { getByTestId, getByText } = render(() => <Comp />)
369
+ const input = getByTestId('fieldinput')
370
+ await user.type(input, 'other')
371
+ // mockFn will have been called 5 times without onChangeAsyncDebounceMs
372
+ expect(mockFn).toHaveBeenCalledTimes(0)
373
+ await waitFor(() => getByText(error))
374
+ expect(getByText(error)).toBeInTheDocument()
375
+ })
376
+ })
@@ -0,0 +1,116 @@
1
+ import { render, screen, waitFor } from '@solidjs/testing-library'
2
+ import '@testing-library/jest-dom'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { createFormFactory, createForm } from '..'
5
+
6
+ const user = userEvent.setup()
7
+
8
+ describe('createForm', () => {
9
+ it('preserves field state', async () => {
10
+ type Person = {
11
+ firstName: string
12
+ lastName: string
13
+ }
14
+
15
+ const formFactory = createFormFactory<Person, unknown>()
16
+
17
+ function Comp() {
18
+ const form = formFactory.createForm()
19
+ return (
20
+ <form.Provider>
21
+ <form.Field
22
+ name="firstName"
23
+ defaultValue={''}
24
+ children={(field) => (
25
+ <input
26
+ data-testid="fieldinput"
27
+ value={field().state.value}
28
+ onBlur={field().handleBlur}
29
+ onChange={(e) => field().handleChange(e.currentTarget.value)}
30
+ />
31
+ )}
32
+ />
33
+ </form.Provider>
34
+ )
35
+ }
36
+
37
+ render(() => <Comp />)
38
+ const input = screen.getByTestId('fieldinput')
39
+ expect(screen.queryByText('FirstName')).not.toBeInTheDocument()
40
+ await user.type(input, 'FirstName')
41
+ expect(input).toHaveValue('FirstName')
42
+ })
43
+
44
+ it('should allow default values to be set', async () => {
45
+ type Person = {
46
+ firstName: string
47
+ lastName: string
48
+ }
49
+
50
+ const formFactory = createFormFactory<Person, unknown>()
51
+
52
+ function Comp() {
53
+ const form = formFactory.createForm(() => ({
54
+ defaultValues: {
55
+ firstName: 'FirstName',
56
+ lastName: 'LastName',
57
+ },
58
+ }))
59
+
60
+ return (
61
+ <form.Provider>
62
+ <form.Field
63
+ name="firstName"
64
+ children={(field) => {
65
+ return <p>{field().state.value}</p>
66
+ }}
67
+ />
68
+ </form.Provider>
69
+ )
70
+ }
71
+
72
+ const { findByText, queryByText } = render(() => <Comp />)
73
+ expect(await findByText('FirstName')).toBeInTheDocument()
74
+ expect(queryByText('LastName')).not.toBeInTheDocument()
75
+ })
76
+
77
+ it('should handle submitting properly', async () => {
78
+ let submittedData = null as { firstName: string } | null
79
+ function Comp() {
80
+ const form = createForm(() => ({
81
+ defaultValues: {
82
+ firstName: 'FirstName',
83
+ },
84
+ onSubmit: (data) => {
85
+ submittedData = data
86
+ },
87
+ }))
88
+
89
+ return (
90
+ <form.Provider>
91
+ <form.Field
92
+ name="firstName"
93
+ children={(field) => {
94
+ return (
95
+ <input
96
+ value={field().state.value}
97
+ onBlur={field().handleBlur}
98
+ onChange={(e) => field().handleChange(e.target.value)}
99
+ placeholder={'First name'}
100
+ />
101
+ )
102
+ }}
103
+ />
104
+ <button onClick={form.handleSubmit}>Submit</button>
105
+ </form.Provider>
106
+ )
107
+ }
108
+
109
+ const { findByPlaceholderText, getByText } = render(() => <Comp />)
110
+ const input = await findByPlaceholderText('First name')
111
+ await user.clear(input)
112
+ await user.type(input, 'OtherName')
113
+ await user.click(getByText('Submit'))
114
+ expect(submittedData?.firstName).toEqual('OtherName')
115
+ })
116
+ })
@@ -0,0 +1,38 @@
1
+ import { render } from '@solidjs/testing-library'
2
+ import '@testing-library/jest-dom'
3
+ import { createFormFactory } from '..'
4
+
5
+ describe('createFormFactory', () => {
6
+ it('should allow default values to be set', async () => {
7
+ type Person = {
8
+ firstName: string
9
+ lastName: string
10
+ }
11
+
12
+ const formFactory = createFormFactory<Person, unknown>(() => ({
13
+ defaultValues: {
14
+ firstName: 'FirstName',
15
+ lastName: 'LastName',
16
+ },
17
+ }))
18
+
19
+ function Comp() {
20
+ const form = formFactory.createForm()
21
+
22
+ return (
23
+ <form.Provider>
24
+ <form.Field
25
+ name="firstName"
26
+ children={(field) => {
27
+ return <p>{field().state.value}</p>
28
+ }}
29
+ />
30
+ </form.Provider>
31
+ )
32
+ }
33
+
34
+ const { findByText, queryByText } = render(() => <Comp />)
35
+ expect(await findByText('FirstName')).toBeInTheDocument()
36
+ expect(queryByText('LastName')).not.toBeInTheDocument()
37
+ })
38
+ })
@@ -0,0 +1,5 @@
1
+ export function sleep(timeout: number): Promise<void> {
2
+ return new Promise((resolve, _reject) => {
3
+ setTimeout(resolve, timeout)
4
+ })
5
+ }
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { FieldOptions, DeepKeys, DeepValue } from '@tanstack/form-core'
2
+
3
+ export type CreateFieldOptions<
4
+ TParentData,
5
+ TName extends DeepKeys<TParentData>,
6
+ ValidatorType,
7
+ FormValidator,
8
+ TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
9
+ > = FieldOptions<TParentData, TName, ValidatorType, FormValidator, TData> & {
10
+ mode?: 'value' | 'array'
11
+ }