@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,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
+ })