@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.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/build/dev.cjs +109 -0
- package/build/dev.js +93 -0
- package/build/index.cjs +109 -0
- package/build/index.d.cts +56 -0
- package/build/index.d.ts +56 -0
- package/build/index.js +93 -0
- package/package.json +72 -0
- package/src/createField.tsx +196 -0
- package/src/createForm.tsx +53 -0
- package/src/createFormFactory.ts +31 -0
- package/src/formContext.ts +20 -0
- package/src/index.ts +27 -0
- package/src/tests/createField.test-d.tsx +68 -0
- package/src/tests/createField.test.tsx +376 -0
- package/src/tests/createForm.test.tsx +116 -0
- package/src/tests/createFormFactory.test.tsx +38 -0
- package/src/tests/utils.ts +5 -0
- package/src/types.ts +11 -0
@@ -0,0 +1,196 @@
|
|
1
|
+
import { FieldApi } from '@tanstack/form-core'
|
2
|
+
import {
|
3
|
+
createComponent,
|
4
|
+
createComputed,
|
5
|
+
createMemo,
|
6
|
+
createSignal,
|
7
|
+
onCleanup,
|
8
|
+
onMount,
|
9
|
+
} from 'solid-js'
|
10
|
+
import { formContext, useFormContext } from './formContext'
|
11
|
+
|
12
|
+
import type { DeepKeys, DeepValue, Narrow } from '@tanstack/form-core'
|
13
|
+
import type { JSXElement } from 'solid-js'
|
14
|
+
import type { CreateFieldOptions } from './types'
|
15
|
+
|
16
|
+
declare module '@tanstack/form-core' {
|
17
|
+
// eslint-disable-next-line no-shadow
|
18
|
+
interface FieldApi<
|
19
|
+
TParentData,
|
20
|
+
TName extends DeepKeys<TParentData>,
|
21
|
+
ValidatorType,
|
22
|
+
FormValidator,
|
23
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
24
|
+
> {
|
25
|
+
Field: FieldComponent<TData, FormValidator>
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
export type CreateField<TParentData> = <
|
30
|
+
TName extends DeepKeys<TParentData>,
|
31
|
+
ValidatorType,
|
32
|
+
FormValidator,
|
33
|
+
>(
|
34
|
+
opts: () => { name: Narrow<TName> } & CreateFieldOptions<
|
35
|
+
TParentData,
|
36
|
+
TName,
|
37
|
+
ValidatorType,
|
38
|
+
FormValidator
|
39
|
+
>,
|
40
|
+
) => () => FieldApi<
|
41
|
+
TParentData,
|
42
|
+
TName,
|
43
|
+
ValidatorType,
|
44
|
+
FormValidator,
|
45
|
+
DeepValue<TParentData, TName>
|
46
|
+
>
|
47
|
+
|
48
|
+
// ugly way to trick solid into triggering updates for changes on the fieldApi
|
49
|
+
function makeFieldReactive<FieldApiT extends FieldApi<any, any, any, any>>(
|
50
|
+
fieldApi: FieldApiT,
|
51
|
+
): () => FieldApiT {
|
52
|
+
const [flag, setFlag] = createSignal(false)
|
53
|
+
const fieldApiMemo = createMemo(() => [flag(), fieldApi] as const)
|
54
|
+
const unsubscribeStore = fieldApi.store.subscribe(() => setFlag((f) => !f))
|
55
|
+
onCleanup(unsubscribeStore)
|
56
|
+
return () => fieldApiMemo()[1]
|
57
|
+
}
|
58
|
+
|
59
|
+
export function createField<
|
60
|
+
TParentData,
|
61
|
+
TName extends DeepKeys<TParentData>,
|
62
|
+
ValidatorType,
|
63
|
+
FormValidator,
|
64
|
+
>(
|
65
|
+
opts: () => CreateFieldOptions<
|
66
|
+
TParentData,
|
67
|
+
TName,
|
68
|
+
ValidatorType,
|
69
|
+
FormValidator
|
70
|
+
>,
|
71
|
+
): () => FieldApi<
|
72
|
+
TParentData,
|
73
|
+
TName,
|
74
|
+
ValidatorType,
|
75
|
+
FormValidator
|
76
|
+
// Omit<typeof opts, 'onMount'> & {
|
77
|
+
// form: FormApi<TParentData>
|
78
|
+
// }
|
79
|
+
> {
|
80
|
+
// Get the form API either manually or from context
|
81
|
+
const { formApi, parentFieldName } = useFormContext()
|
82
|
+
|
83
|
+
const options = opts()
|
84
|
+
const name = (
|
85
|
+
typeof options.index === 'number'
|
86
|
+
? [parentFieldName, options.index, options.name]
|
87
|
+
: [parentFieldName, options.name]
|
88
|
+
)
|
89
|
+
.filter((d) => d !== undefined)
|
90
|
+
.join('.')
|
91
|
+
|
92
|
+
const fieldApi = new FieldApi({
|
93
|
+
...options,
|
94
|
+
form: formApi,
|
95
|
+
name: name as typeof options.name,
|
96
|
+
})
|
97
|
+
fieldApi.Field = Field as never
|
98
|
+
|
99
|
+
/**
|
100
|
+
* fieldApi.update should not have any side effects. Think of it like a `useRef`
|
101
|
+
* that we need to keep updated every render with the most up-to-date information.
|
102
|
+
*
|
103
|
+
* createComputed to make sure this effect runs before render effects
|
104
|
+
*/
|
105
|
+
createComputed(() => fieldApi.update({ ...opts(), form: formApi }))
|
106
|
+
|
107
|
+
// Instantiates field meta and removes it when unrendered
|
108
|
+
onMount(() => onCleanup(fieldApi.mount()))
|
109
|
+
|
110
|
+
return makeFieldReactive(fieldApi)
|
111
|
+
}
|
112
|
+
|
113
|
+
type FieldComponentProps<
|
114
|
+
TParentData,
|
115
|
+
TName extends DeepKeys<TParentData>,
|
116
|
+
ValidatorType,
|
117
|
+
FormValidator,
|
118
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
119
|
+
> = {
|
120
|
+
children: (
|
121
|
+
fieldApi: () => FieldApi<
|
122
|
+
TParentData,
|
123
|
+
TName,
|
124
|
+
ValidatorType,
|
125
|
+
FormValidator,
|
126
|
+
TData
|
127
|
+
>,
|
128
|
+
) => JSXElement
|
129
|
+
} & (TParentData extends any[]
|
130
|
+
? {
|
131
|
+
name?: TName
|
132
|
+
index: number
|
133
|
+
}
|
134
|
+
: {
|
135
|
+
name: TName
|
136
|
+
index?: never
|
137
|
+
}) &
|
138
|
+
Omit<
|
139
|
+
CreateFieldOptions<TParentData, TName, ValidatorType, FormValidator>,
|
140
|
+
'name' | 'index'
|
141
|
+
>
|
142
|
+
|
143
|
+
export type FieldComponent<TParentData, FormValidator> = <
|
144
|
+
TName extends DeepKeys<TParentData>,
|
145
|
+
ValidatorType,
|
146
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
147
|
+
>({
|
148
|
+
children,
|
149
|
+
...fieldOptions
|
150
|
+
}: FieldComponentProps<
|
151
|
+
TParentData,
|
152
|
+
TName,
|
153
|
+
ValidatorType,
|
154
|
+
FormValidator,
|
155
|
+
TData
|
156
|
+
>) => JSXElement
|
157
|
+
|
158
|
+
export function Field<
|
159
|
+
TParentData,
|
160
|
+
TName extends DeepKeys<TParentData>,
|
161
|
+
ValidatorType,
|
162
|
+
FormValidator,
|
163
|
+
>(
|
164
|
+
props: {
|
165
|
+
children: (
|
166
|
+
fieldApi: () => FieldApi<
|
167
|
+
TParentData,
|
168
|
+
TName,
|
169
|
+
ValidatorType,
|
170
|
+
FormValidator
|
171
|
+
>,
|
172
|
+
) => JSXElement
|
173
|
+
} & CreateFieldOptions<TParentData, TName, ValidatorType, FormValidator>,
|
174
|
+
) {
|
175
|
+
const fieldApi = createField<
|
176
|
+
TParentData,
|
177
|
+
TName,
|
178
|
+
ValidatorType,
|
179
|
+
FormValidator
|
180
|
+
>(() => {
|
181
|
+
const { children, ...fieldOptions } = props
|
182
|
+
return fieldOptions
|
183
|
+
})
|
184
|
+
|
185
|
+
return (
|
186
|
+
<formContext.Provider
|
187
|
+
value={{
|
188
|
+
formApi: fieldApi().form,
|
189
|
+
parentFieldName: String(fieldApi().name),
|
190
|
+
}}
|
191
|
+
>
|
192
|
+
{/* createComponent to make sure the signals in the children component are not tracked */}
|
193
|
+
{createComponent(() => props.children(fieldApi), {})}
|
194
|
+
</formContext.Provider>
|
195
|
+
)
|
196
|
+
}
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import type { FormOptions, FormState } from '@tanstack/form-core'
|
2
|
+
import { FormApi, functionalUpdate } from '@tanstack/form-core'
|
3
|
+
import { createComputed, type JSXElement } from 'solid-js'
|
4
|
+
import { useStore } from '@tanstack/solid-store'
|
5
|
+
import {
|
6
|
+
Field,
|
7
|
+
createField,
|
8
|
+
type CreateField,
|
9
|
+
type FieldComponent,
|
10
|
+
} from './createField'
|
11
|
+
import { formContext } from './formContext'
|
12
|
+
|
13
|
+
type NoInfer<T> = [T][T extends any ? 0 : never]
|
14
|
+
|
15
|
+
declare module '@tanstack/form-core' {
|
16
|
+
// eslint-disable-next-line no-shadow
|
17
|
+
interface FormApi<TFormData, ValidatorType> {
|
18
|
+
Provider: (props: { children: any }) => any
|
19
|
+
Field: FieldComponent<TFormData, ValidatorType>
|
20
|
+
createField: CreateField<TFormData>
|
21
|
+
useStore: <TSelected = NoInfer<FormState<TFormData>>>(
|
22
|
+
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
|
23
|
+
) => () => TSelected
|
24
|
+
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
|
25
|
+
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected
|
26
|
+
children: ((state: () => NoInfer<TSelected>) => JSXElement) | JSXElement
|
27
|
+
}) => any
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
export function createForm<TData, FormValidator>(
|
32
|
+
opts?: () => FormOptions<TData, FormValidator>,
|
33
|
+
): FormApi<TData, FormValidator> {
|
34
|
+
const options = opts?.()
|
35
|
+
const formApi = new FormApi<TData, FormValidator>(options)
|
36
|
+
|
37
|
+
formApi.Provider = function Provider(props) {
|
38
|
+
return <formContext.Provider {...props} value={{ formApi: formApi }} />
|
39
|
+
}
|
40
|
+
formApi.Field = Field as any
|
41
|
+
formApi.createField = createField as CreateField<TData>
|
42
|
+
formApi.useStore = (selector) => useStore(formApi.store, selector)
|
43
|
+
formApi.Subscribe = (props) =>
|
44
|
+
functionalUpdate(props.children, useStore(formApi.store, props.selector))
|
45
|
+
|
46
|
+
/**
|
47
|
+
* formApi.update should not have any side effects. Think of it like a `useRef`
|
48
|
+
* that we need to keep updated every render with the most up-to-date information.
|
49
|
+
*/
|
50
|
+
createComputed(() => formApi.update(opts?.()))
|
51
|
+
|
52
|
+
return formApi
|
53
|
+
}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import type { FormApi, FormOptions } from '@tanstack/form-core'
|
2
|
+
|
3
|
+
import {
|
4
|
+
type CreateField,
|
5
|
+
type FieldComponent,
|
6
|
+
Field,
|
7
|
+
createField,
|
8
|
+
} from './createField'
|
9
|
+
import { createForm } from './createForm'
|
10
|
+
import { mergeProps } from 'solid-js'
|
11
|
+
|
12
|
+
export type FormFactory<TFormData, FormValidator> = {
|
13
|
+
createForm: (
|
14
|
+
opts?: () => FormOptions<TFormData, FormValidator>,
|
15
|
+
) => FormApi<TFormData, FormValidator>
|
16
|
+
createField: CreateField<TFormData>
|
17
|
+
Field: FieldComponent<TFormData, FormValidator>
|
18
|
+
}
|
19
|
+
|
20
|
+
export function createFormFactory<TFormData, FormValidator>(
|
21
|
+
defaultOpts?: () => FormOptions<TFormData, FormValidator>,
|
22
|
+
): FormFactory<TFormData, FormValidator> {
|
23
|
+
return {
|
24
|
+
createForm: (opts) =>
|
25
|
+
createForm<TFormData, FormValidator>(() =>
|
26
|
+
mergeProps(defaultOpts?.() ?? {}, opts?.() ?? {}),
|
27
|
+
),
|
28
|
+
createField,
|
29
|
+
Field: Field as never,
|
30
|
+
}
|
31
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { createContext, useContext } from 'solid-js'
|
2
|
+
import type { FormApi } from '@tanstack/form-core'
|
3
|
+
|
4
|
+
type FormContextType =
|
5
|
+
| undefined
|
6
|
+
| {
|
7
|
+
formApi: FormApi<any, any>
|
8
|
+
parentFieldName?: string
|
9
|
+
}
|
10
|
+
|
11
|
+
export const formContext = createContext<FormContextType>(undefined)
|
12
|
+
|
13
|
+
export function useFormContext() {
|
14
|
+
const formApi: FormContextType = useContext(formContext)
|
15
|
+
|
16
|
+
if (!formApi)
|
17
|
+
throw new Error(`You are trying to use the form API outside of a form!`)
|
18
|
+
|
19
|
+
return formApi
|
20
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
export type {
|
2
|
+
DeepKeys,
|
3
|
+
DeepValue,
|
4
|
+
FieldApiOptions,
|
5
|
+
FieldInfo,
|
6
|
+
FieldMeta,
|
7
|
+
FieldOptions,
|
8
|
+
FieldState,
|
9
|
+
FormOptions,
|
10
|
+
FormState,
|
11
|
+
RequiredByKey,
|
12
|
+
Updater,
|
13
|
+
UpdaterFn,
|
14
|
+
ValidationCause,
|
15
|
+
ValidationError,
|
16
|
+
ValidationMeta,
|
17
|
+
} from '@tanstack/form-core'
|
18
|
+
|
19
|
+
export { FormApi, FieldApi, functionalUpdate } from '@tanstack/form-core'
|
20
|
+
|
21
|
+
export { createForm } from './createForm'
|
22
|
+
|
23
|
+
export type { CreateField, FieldComponent } from './createField'
|
24
|
+
export { createField, Field } from './createField'
|
25
|
+
|
26
|
+
export type { FormFactory } from './createFormFactory'
|
27
|
+
export { createFormFactory } from './createFormFactory'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import { assertType } from 'vitest'
|
2
|
+
import { createForm } from '../createForm'
|
3
|
+
|
4
|
+
it('should type state.value properly', () => {
|
5
|
+
function Comp() {
|
6
|
+
const form = createForm(
|
7
|
+
() =>
|
8
|
+
({
|
9
|
+
defaultValues: {
|
10
|
+
firstName: 'test',
|
11
|
+
age: 84,
|
12
|
+
},
|
13
|
+
}) as const,
|
14
|
+
)
|
15
|
+
|
16
|
+
return (
|
17
|
+
<form.Provider>
|
18
|
+
<form.Field
|
19
|
+
name="firstName"
|
20
|
+
children={(field) => {
|
21
|
+
assertType<'test'>(field().state.value)
|
22
|
+
}}
|
23
|
+
/>
|
24
|
+
<form.Field
|
25
|
+
name="age"
|
26
|
+
children={(field) => {
|
27
|
+
assertType<84>(field().state.value)
|
28
|
+
}}
|
29
|
+
/>
|
30
|
+
</form.Provider>
|
31
|
+
)
|
32
|
+
}
|
33
|
+
})
|
34
|
+
|
35
|
+
it('should type onChange properly', () => {
|
36
|
+
function Comp() {
|
37
|
+
const form = createForm(
|
38
|
+
() =>
|
39
|
+
({
|
40
|
+
defaultValues: {
|
41
|
+
firstName: 'test',
|
42
|
+
age: 84,
|
43
|
+
},
|
44
|
+
}) as const,
|
45
|
+
)
|
46
|
+
|
47
|
+
return (
|
48
|
+
<form.Provider>
|
49
|
+
<form.Field
|
50
|
+
name="firstName"
|
51
|
+
onChange={(val) => {
|
52
|
+
assertType<'test'>(val)
|
53
|
+
return null
|
54
|
+
}}
|
55
|
+
children={(field) => null}
|
56
|
+
/>
|
57
|
+
<form.Field
|
58
|
+
name="age"
|
59
|
+
onChange={(val) => {
|
60
|
+
assertType<84>(val)
|
61
|
+
return null
|
62
|
+
}}
|
63
|
+
children={(field) => null}
|
64
|
+
/>
|
65
|
+
</form.Provider>
|
66
|
+
)
|
67
|
+
}
|
68
|
+
})
|