@tanstack/react-form 1.26.0 → 1.27.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/dist/cjs/createFormHook.cjs +2 -3
- package/dist/cjs/createFormHook.cjs.map +1 -1
- package/dist/cjs/createFormHook.d.cts +7 -7
- package/dist/cjs/useField.cjs +74 -17
- package/dist/cjs/useField.cjs.map +1 -1
- package/dist/cjs/useField.d.cts +4 -4
- package/dist/cjs/useFieldGroup.cjs +1 -1
- package/dist/cjs/useFieldGroup.cjs.map +1 -1
- package/dist/cjs/useFieldGroup.d.cts +3 -3
- package/dist/cjs/useForm.cjs +26 -9
- package/dist/cjs/useForm.cjs.map +1 -1
- package/dist/cjs/useForm.d.cts +2 -2
- package/dist/esm/createFormHook.d.ts +7 -7
- package/dist/esm/createFormHook.js +2 -3
- package/dist/esm/createFormHook.js.map +1 -1
- package/dist/esm/useField.d.ts +4 -4
- package/dist/esm/useField.js +75 -18
- package/dist/esm/useField.js.map +1 -1
- package/dist/esm/useFieldGroup.d.ts +3 -3
- package/dist/esm/useFieldGroup.js +2 -2
- package/dist/esm/useFieldGroup.js.map +1 -1
- package/dist/esm/useForm.d.ts +2 -2
- package/dist/esm/useForm.js +29 -12
- package/dist/esm/useForm.js.map +1 -1
- package/package.json +3 -3
- package/src/createFormHook.tsx +18 -15
- package/src/useField.tsx +114 -27
- package/src/useFieldGroup.tsx +10 -5
- package/src/useForm.tsx +33 -15
package/dist/esm/useForm.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { FormApi, functionalUpdate } from "@tanstack/form-core";
|
|
2
|
+
import { jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { uuid, FormApi, functionalUpdate } from "@tanstack/form-core";
|
|
4
4
|
import { useStore } from "@tanstack/react-store";
|
|
5
|
-
import {
|
|
5
|
+
import { useState, useMemo } from "react";
|
|
6
6
|
import { Field } from "./useField.js";
|
|
7
7
|
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
|
|
8
8
|
function LocalSubscribe({
|
|
@@ -11,33 +11,50 @@ function LocalSubscribe({
|
|
|
11
11
|
children
|
|
12
12
|
}) {
|
|
13
13
|
const data = useStore(form.store, selector);
|
|
14
|
-
return functionalUpdate(children, data);
|
|
14
|
+
return /* @__PURE__ */ jsx(Fragment, { children: functionalUpdate(children, data) });
|
|
15
15
|
}
|
|
16
16
|
function useForm(opts) {
|
|
17
|
-
const
|
|
18
|
-
const [
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const fallbackFormId = useState(() => uuid())[0];
|
|
18
|
+
const [prevFormId, setPrevFormId] = useState(opts?.formId);
|
|
19
|
+
const [formApi, setFormApi] = useState(() => {
|
|
20
|
+
return new FormApi({ ...opts, formId: opts?.formId ?? fallbackFormId });
|
|
21
|
+
});
|
|
22
|
+
if (prevFormId !== opts?.formId) {
|
|
23
|
+
const formId = opts?.formId ?? fallbackFormId;
|
|
24
|
+
setFormApi(new FormApi({ ...opts, formId }));
|
|
25
|
+
setPrevFormId(formId);
|
|
26
|
+
}
|
|
27
|
+
const extendedFormApi = useMemo(() => {
|
|
28
|
+
const extendedApi = {
|
|
29
|
+
...formApi,
|
|
30
|
+
// We must add all `get`ters from `core`'s `FormApi` here, as otherwise the spread operator won't catch those
|
|
31
|
+
get formId() {
|
|
32
|
+
return formApi._formId;
|
|
33
|
+
},
|
|
34
|
+
get state() {
|
|
35
|
+
return formApi.store.state;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
21
38
|
extendedApi.Field = function APIField(props) {
|
|
22
|
-
return /* @__PURE__ */ jsx(Field, { ...props, form:
|
|
39
|
+
return /* @__PURE__ */ jsx(Field, { ...props, form: formApi });
|
|
23
40
|
};
|
|
24
41
|
extendedApi.Subscribe = function Subscribe(props) {
|
|
25
42
|
return /* @__PURE__ */ jsx(
|
|
26
43
|
LocalSubscribe,
|
|
27
44
|
{
|
|
28
|
-
form:
|
|
45
|
+
form: formApi,
|
|
29
46
|
selector: props.selector,
|
|
30
47
|
children: props.children
|
|
31
48
|
}
|
|
32
49
|
);
|
|
33
50
|
};
|
|
34
51
|
return extendedApi;
|
|
35
|
-
});
|
|
52
|
+
}, [formApi]);
|
|
36
53
|
useIsomorphicLayoutEffect(formApi.mount, []);
|
|
37
54
|
useIsomorphicLayoutEffect(() => {
|
|
38
55
|
formApi.update(opts);
|
|
39
56
|
});
|
|
40
|
-
return
|
|
57
|
+
return extendedFormApi;
|
|
41
58
|
}
|
|
42
59
|
export {
|
|
43
60
|
useForm
|
package/dist/esm/useForm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.js","sources":["../../src/useForm.tsx"],"sourcesContent":["'use client'\n\nimport { FormApi, functionalUpdate } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport {
|
|
1
|
+
{"version":3,"file":"useForm.js","sources":["../../src/useForm.tsx"],"sourcesContent":["'use client'\n\nimport { FormApi, functionalUpdate, uuid } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport { useMemo, useState } from 'react'\nimport { Field } from './useField'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type {\n AnyFormApi,\n AnyFormState,\n FormAsyncValidateOrFn,\n FormOptions,\n FormState,\n FormValidateOrFn,\n} from '@tanstack/form-core'\nimport type { FunctionComponent, PropsWithChildren, ReactNode } from 'react'\nimport type { FieldComponent } from './useField'\nimport type { NoInfer } from '@tanstack/react-store'\n\n/**\n * Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm`\n */\nexport interface ReactFormApi<\n in out TFormData,\n in out TOnMount extends undefined | FormValidateOrFn<TFormData>,\n in out TOnChange extends undefined | FormValidateOrFn<TFormData>,\n in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n in out TSubmitMeta,\n> {\n /**\n * A React component to render form fields. With this, you can render and manage individual form fields.\n */\n Field: FieldComponent<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >\n /**\n * A `Subscribe` function that allows you to listen and react to changes in the form's state. It's especially useful when you need to execute side effects or render specific components in response to state updates.\n */\n Subscribe: <\n TSelected = NoInfer<\n FormState<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer\n >\n >,\n >(props: {\n selector?: (\n state: NoInfer<\n FormState<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer\n >\n >,\n ) => TSelected\n children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode\n }) => ReturnType<FunctionComponent>\n}\n\n/**\n * An extended version of the `FormApi` class that includes React-specific functionalities from `ReactFormApi`\n */\nexport type ReactFormExtendedApi<\n TFormData,\n TOnMount extends undefined | FormValidateOrFn<TFormData>,\n TOnChange extends undefined | FormValidateOrFn<TFormData>,\n TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n TSubmitMeta,\n> = FormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n> &\n ReactFormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >\n\nfunction LocalSubscribe({\n form,\n selector,\n children,\n}: PropsWithChildren<{\n form: AnyFormApi\n selector: (state: AnyFormState) => AnyFormState\n}>): ReturnType<FunctionComponent> {\n const data = useStore(form.store, selector)\n\n return <>{functionalUpdate(children, data)}</>\n}\n\n/**\n * A custom React Hook that returns an extended instance of the `FormApi` class.\n *\n * This API encapsulates all the necessary functionalities related to the form. It allows you to manage form state, handle submissions, and interact with form fields\n */\nexport function useForm<\n TFormData,\n TOnMount extends undefined | FormValidateOrFn<TFormData>,\n TOnChange extends undefined | FormValidateOrFn<TFormData>,\n TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnBlur extends undefined | FormValidateOrFn<TFormData>,\n TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnSubmit extends undefined | FormValidateOrFn<TFormData>,\n TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnDynamic extends undefined | FormValidateOrFn<TFormData>,\n TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,\n TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,\n TSubmitMeta,\n>(\n opts?: FormOptions<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >,\n) {\n const fallbackFormId = useState(() => uuid())[0]\n const [prevFormId, setPrevFormId] = useState<string>(opts?.formId as never)\n\n const [formApi, setFormApi] = useState(() => {\n return new FormApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n >({ ...opts, formId: opts?.formId ?? fallbackFormId })\n })\n\n if (prevFormId !== opts?.formId) {\n const formId = opts?.formId ?? fallbackFormId\n setFormApi(new FormApi({ ...opts, formId }))\n setPrevFormId(formId)\n }\n\n const extendedFormApi = useMemo(() => {\n const extendedApi: ReactFormExtendedApi<\n TFormData,\n TOnMount,\n TOnChange,\n TOnChangeAsync,\n TOnBlur,\n TOnBlurAsync,\n TOnSubmit,\n TOnSubmitAsync,\n TOnDynamic,\n TOnDynamicAsync,\n TOnServer,\n TSubmitMeta\n > = {\n ...formApi,\n // We must add all `get`ters from `core`'s `FormApi` here, as otherwise the spread operator won't catch those\n get formId(): string {\n return formApi._formId\n },\n get state() {\n return formApi.store.state\n },\n } as never\n\n extendedApi.Field = function APIField(props) {\n return <Field {...props} form={formApi} />\n }\n\n extendedApi.Subscribe = function Subscribe(props: any) {\n return (\n <LocalSubscribe\n form={formApi}\n selector={props.selector}\n children={props.children}\n />\n )\n }\n\n return extendedApi\n }, [formApi])\n\n useIsomorphicLayoutEffect(formApi.mount, [])\n\n /**\n * formApi.update should not have any side effects. Think of it like a `useRef`\n * that we need to keep updated every render with the most up-to-date information.\n */\n useIsomorphicLayoutEffect(() => {\n formApi.update(opts)\n })\n\n return extendedFormApi\n}\n"],"names":[],"mappings":";;;;;;;AA2IA;AAAwB;AACtB;AACA;AAEF;AAIE;AAEA;AACF;AAOO;AA6BL;AACA;AAEA;AACE;AAaqD;AAGvD;AACE;AACA;AACA;AAAoB;AAGtB;AACE;AAaI;AACC;AAAA;AAGD;AAAe;AACjB;AAEE;AAAqB;AACvB;AAGF;AACE;AAAwC;AAG1C;AACE;AACE;AAAC;AAAA;AACO;AACU;AACA;AAAA;AAAA;AAKtB;AAAO;AAGT;AAMA;AACE;AAAmB;AAGrB;AACF;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-form",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Powerful, type-safe forms for React.",
|
|
5
5
|
"author": "tannerlinsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"src"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@tanstack/react-store": "^0.
|
|
41
|
-
"@tanstack/form-core": "1.
|
|
40
|
+
"@tanstack/react-store": "^0.8.0",
|
|
41
|
+
"@tanstack/form-core": "1.27.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "^19.0.7",
|
package/src/createFormHook.tsx
CHANGED
|
@@ -17,8 +17,8 @@ import type {
|
|
|
17
17
|
import type {
|
|
18
18
|
ComponentType,
|
|
19
19
|
Context,
|
|
20
|
+
FunctionComponent,
|
|
20
21
|
PropsWithChildren,
|
|
21
|
-
ReactNode,
|
|
22
22
|
} from 'react'
|
|
23
23
|
import type { FieldComponent } from './useField'
|
|
24
24
|
import type { ReactFormExtendedApi } from './useForm'
|
|
@@ -191,7 +191,10 @@ export type AppFieldExtendedReactFormApi<
|
|
|
191
191
|
TSubmitMeta,
|
|
192
192
|
NoInfer<TFieldComponents>
|
|
193
193
|
>
|
|
194
|
-
AppForm: ComponentType<
|
|
194
|
+
AppForm: ComponentType<
|
|
195
|
+
// PropsWithChildren<P> is not optional in React 17
|
|
196
|
+
PropsWithChildren<{}>
|
|
197
|
+
>
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
export interface WithFormProps<
|
|
@@ -226,8 +229,8 @@ export interface WithFormProps<
|
|
|
226
229
|
> {
|
|
227
230
|
// Optional, but adds props to the `render` function outside of `form`
|
|
228
231
|
props?: TRenderProps
|
|
229
|
-
render:
|
|
230
|
-
|
|
232
|
+
render: FunctionComponent<
|
|
233
|
+
PropsWithChildren<
|
|
231
234
|
NoInfer<TRenderProps> & {
|
|
232
235
|
form: AppFieldExtendedReactFormApi<
|
|
233
236
|
TFormData,
|
|
@@ -246,8 +249,8 @@ export interface WithFormProps<
|
|
|
246
249
|
TFormComponents
|
|
247
250
|
>
|
|
248
251
|
}
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
>
|
|
253
|
+
>
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
export interface WithFieldGroupProps<
|
|
@@ -259,8 +262,8 @@ export interface WithFieldGroupProps<
|
|
|
259
262
|
> extends BaseFormOptions<TFieldGroupData, TSubmitMeta> {
|
|
260
263
|
// Optional, but adds props to the `render` function outside of `form`
|
|
261
264
|
props?: TRenderProps
|
|
262
|
-
render:
|
|
263
|
-
|
|
265
|
+
render: FunctionComponent<
|
|
266
|
+
PropsWithChildren<
|
|
264
267
|
NoInfer<TRenderProps> & {
|
|
265
268
|
group: AppFieldExtendedReactFieldGroupApi<
|
|
266
269
|
unknown,
|
|
@@ -283,8 +286,8 @@ export interface WithFieldGroupProps<
|
|
|
283
286
|
TFormComponents
|
|
284
287
|
>
|
|
285
288
|
}
|
|
286
|
-
|
|
287
|
-
|
|
289
|
+
>
|
|
290
|
+
>
|
|
288
291
|
}
|
|
289
292
|
|
|
290
293
|
export function createFormHook<
|
|
@@ -342,13 +345,13 @@ export function createFormHook<
|
|
|
342
345
|
> {
|
|
343
346
|
const form = useForm(props)
|
|
344
347
|
|
|
345
|
-
|
|
346
|
-
|
|
348
|
+
// PropsWithChildren<P> is not optional in React 17
|
|
349
|
+
const AppForm = useMemo<ComponentType<PropsWithChildren<{}>>>(() => {
|
|
350
|
+
return ({ children }) => {
|
|
347
351
|
return (
|
|
348
352
|
<formContext.Provider value={form}>{children}</formContext.Provider>
|
|
349
353
|
)
|
|
350
|
-
}
|
|
351
|
-
return AppForm
|
|
354
|
+
}
|
|
352
355
|
}, [form])
|
|
353
356
|
|
|
354
357
|
const AppField = useMemo(() => {
|
|
@@ -521,7 +524,7 @@ export function createFormHook<
|
|
|
521
524
|
fields: TFields
|
|
522
525
|
}
|
|
523
526
|
>,
|
|
524
|
-
) =>
|
|
527
|
+
) => ReturnType<FunctionComponent> {
|
|
525
528
|
return function Render(innerProps) {
|
|
526
529
|
const fieldGroupProps = useMemo(() => {
|
|
527
530
|
return {
|
package/src/useField.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useMemo, useRef } from 'react'
|
|
3
|
+
import { useMemo, useRef, useState } from 'react'
|
|
4
4
|
import { useStore } from '@tanstack/react-store'
|
|
5
5
|
import { FieldApi, functionalUpdate } from '@tanstack/form-core'
|
|
6
6
|
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
|
|
7
7
|
import type {
|
|
8
|
+
AnyFieldApi,
|
|
9
|
+
AnyFieldMeta,
|
|
8
10
|
DeepKeys,
|
|
9
11
|
DeepValue,
|
|
10
12
|
FieldAsyncValidateOrFn,
|
|
@@ -13,7 +15,7 @@ import type {
|
|
|
13
15
|
FormAsyncValidateOrFn,
|
|
14
16
|
FormValidateOrFn,
|
|
15
17
|
} from '@tanstack/form-core'
|
|
16
|
-
import type { FunctionComponent, ReactNode } from 'react'
|
|
18
|
+
import type { FunctionComponent, ReactElement, ReactNode } from 'react'
|
|
17
19
|
import type { UseFieldOptions, UseFieldOptionsBound } from './types'
|
|
18
20
|
|
|
19
21
|
interface ReactFieldApi<
|
|
@@ -195,17 +197,103 @@ export function useField<
|
|
|
195
197
|
) {
|
|
196
198
|
// Keep a snapshot of options so that React Compiler doesn't
|
|
197
199
|
// wrongly optimize fieldApi.
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
+
const [prevOptions, setPrevOptions] = useState(() => ({
|
|
201
|
+
form: opts.form,
|
|
202
|
+
name: opts.name,
|
|
203
|
+
}))
|
|
200
204
|
|
|
201
|
-
const fieldApi =
|
|
202
|
-
|
|
203
|
-
...
|
|
204
|
-
form: opts.form,
|
|
205
|
-
name: opts.name,
|
|
205
|
+
const [fieldApi, setFieldApi] = useState(() => {
|
|
206
|
+
return new FieldApi({
|
|
207
|
+
...opts,
|
|
206
208
|
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// We only want to
|
|
212
|
+
// update on name changes since those are at risk of becoming stale. The field
|
|
213
|
+
// state must be up to date for the internal JSX render.
|
|
214
|
+
// The other options can freely be in `fieldApi.update`
|
|
215
|
+
if (prevOptions.form !== opts.form || prevOptions.name !== opts.name) {
|
|
216
|
+
setFieldApi(
|
|
217
|
+
new FieldApi({
|
|
218
|
+
...opts,
|
|
219
|
+
}),
|
|
220
|
+
)
|
|
221
|
+
setPrevOptions({ form: opts.form, name: opts.name })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const reactiveStateValue = useStore(fieldApi.store, (state) => state.value)
|
|
225
|
+
const reactiveMetaIsTouched = useStore(
|
|
226
|
+
fieldApi.store,
|
|
227
|
+
(state) => state.meta.isTouched,
|
|
228
|
+
)
|
|
229
|
+
const reactiveMetaIsBlurred = useStore(
|
|
230
|
+
fieldApi.store,
|
|
231
|
+
(state) => state.meta.isBlurred,
|
|
232
|
+
)
|
|
233
|
+
const reactiveMetaIsDirty = useStore(
|
|
234
|
+
fieldApi.store,
|
|
235
|
+
(state) => state.meta.isDirty,
|
|
236
|
+
)
|
|
237
|
+
const reactiveMetaErrorMap = useStore(
|
|
238
|
+
fieldApi.store,
|
|
239
|
+
(state) => state.meta.errorMap,
|
|
240
|
+
)
|
|
241
|
+
const reactiveMetaErrorSourceMap = useStore(
|
|
242
|
+
fieldApi.store,
|
|
243
|
+
(state) => state.meta.errorSourceMap,
|
|
244
|
+
)
|
|
245
|
+
const reactiveMetaIsValidating = useStore(
|
|
246
|
+
fieldApi.store,
|
|
247
|
+
(state) => state.meta.isValidating,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
// This makes me sad, but if I understand correctly, this is what we have to do for reactivity to work properly with React compiler.
|
|
251
|
+
const extendedFieldApi = useMemo(() => {
|
|
252
|
+
const reactiveFieldApi = {
|
|
253
|
+
...fieldApi,
|
|
254
|
+
get state() {
|
|
255
|
+
return {
|
|
256
|
+
value: reactiveStateValue,
|
|
257
|
+
get meta() {
|
|
258
|
+
return {
|
|
259
|
+
...fieldApi.state.meta,
|
|
260
|
+
isTouched: reactiveMetaIsTouched,
|
|
261
|
+
isBlurred: reactiveMetaIsBlurred,
|
|
262
|
+
isDirty: reactiveMetaIsDirty,
|
|
263
|
+
errorMap: reactiveMetaErrorMap,
|
|
264
|
+
errorSourceMap: reactiveMetaErrorSourceMap,
|
|
265
|
+
isValidating: reactiveMetaIsValidating,
|
|
266
|
+
} satisfies AnyFieldMeta
|
|
267
|
+
},
|
|
268
|
+
} satisfies AnyFieldApi['state']
|
|
269
|
+
},
|
|
270
|
+
}
|
|
207
271
|
|
|
208
|
-
const extendedApi:
|
|
272
|
+
const extendedApi: FieldApi<
|
|
273
|
+
TParentData,
|
|
274
|
+
TName,
|
|
275
|
+
TData,
|
|
276
|
+
TOnMount,
|
|
277
|
+
TOnChange,
|
|
278
|
+
TOnChangeAsync,
|
|
279
|
+
TOnBlur,
|
|
280
|
+
TOnBlurAsync,
|
|
281
|
+
TOnSubmit,
|
|
282
|
+
TOnSubmitAsync,
|
|
283
|
+
TOnDynamic,
|
|
284
|
+
TOnDynamicAsync,
|
|
285
|
+
TFormOnMount,
|
|
286
|
+
TFormOnChange,
|
|
287
|
+
TFormOnChangeAsync,
|
|
288
|
+
TFormOnBlur,
|
|
289
|
+
TFormOnBlurAsync,
|
|
290
|
+
TFormOnSubmit,
|
|
291
|
+
TFormOnSubmitAsync,
|
|
292
|
+
TFormOnDynamic,
|
|
293
|
+
TFormOnDynamicAsync,
|
|
294
|
+
TFormOnServer,
|
|
295
|
+
TPatentSubmitMeta
|
|
296
|
+
> &
|
|
209
297
|
ReactFieldApi<
|
|
210
298
|
TParentData,
|
|
211
299
|
TFormOnMount,
|
|
@@ -219,16 +307,21 @@ export function useField<
|
|
|
219
307
|
TFormOnDynamicAsync,
|
|
220
308
|
TFormOnServer,
|
|
221
309
|
TPatentSubmitMeta
|
|
222
|
-
> =
|
|
310
|
+
> = reactiveFieldApi as never
|
|
223
311
|
|
|
224
312
|
extendedApi.Field = Field as never
|
|
225
313
|
|
|
226
314
|
return extendedApi
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
315
|
+
}, [
|
|
316
|
+
fieldApi,
|
|
317
|
+
reactiveStateValue,
|
|
318
|
+
reactiveMetaIsTouched,
|
|
319
|
+
reactiveMetaIsBlurred,
|
|
320
|
+
reactiveMetaIsDirty,
|
|
321
|
+
reactiveMetaErrorMap,
|
|
322
|
+
reactiveMetaErrorSourceMap,
|
|
323
|
+
reactiveMetaIsValidating,
|
|
324
|
+
])
|
|
232
325
|
|
|
233
326
|
useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])
|
|
234
327
|
|
|
@@ -252,7 +345,7 @@ export function useField<
|
|
|
252
345
|
: undefined,
|
|
253
346
|
)
|
|
254
347
|
|
|
255
|
-
return
|
|
348
|
+
return extendedFieldApi
|
|
256
349
|
}
|
|
257
350
|
|
|
258
351
|
/**
|
|
@@ -496,7 +589,7 @@ export type FieldComponent<
|
|
|
496
589
|
TFormOnServer,
|
|
497
590
|
TPatentSubmitMeta,
|
|
498
591
|
ExtendedApi
|
|
499
|
-
>) =>
|
|
592
|
+
>) => ReturnType<FunctionComponent>
|
|
500
593
|
|
|
501
594
|
/**
|
|
502
595
|
* A type alias representing a field component for a form lens data type.
|
|
@@ -584,7 +677,7 @@ export type LensFieldComponent<
|
|
|
584
677
|
*/
|
|
585
678
|
onBlurListenTo?: DeepKeys<TLensData>[]
|
|
586
679
|
}
|
|
587
|
-
}) =>
|
|
680
|
+
}) => ReturnType<FunctionComponent>
|
|
588
681
|
|
|
589
682
|
/**
|
|
590
683
|
* A function component that takes field options and a render function as children and returns a React component.
|
|
@@ -650,18 +743,12 @@ export const Field = (<
|
|
|
650
743
|
TFormOnDynamicAsync,
|
|
651
744
|
TFormOnServer,
|
|
652
745
|
TPatentSubmitMeta
|
|
653
|
-
>):
|
|
746
|
+
>): ReturnType<FunctionComponent> => {
|
|
654
747
|
const fieldApi = useField(fieldOptions as any)
|
|
655
748
|
|
|
656
749
|
const jsxToDisplay = useMemo(
|
|
657
750
|
() => functionalUpdate(children, fieldApi as any),
|
|
658
|
-
|
|
659
|
-
* The reason this exists is to fix an issue with the React Compiler.
|
|
660
|
-
* Namely, functionalUpdate is memoized where it checks for `fieldApi`, which is a static type.
|
|
661
|
-
* This means that when `state.value` changes, it does not trigger a re-render. The useMemo explicitly fixes this problem
|
|
662
|
-
*/
|
|
663
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
664
|
-
[children, fieldApi, fieldApi.state.value, fieldApi.state.meta],
|
|
751
|
+
[children, fieldApi],
|
|
665
752
|
)
|
|
666
753
|
return (<>{jsxToDisplay}</>) as never
|
|
667
754
|
}) satisfies FunctionComponent<
|
package/src/useFieldGroup.tsx
CHANGED
|
@@ -13,7 +13,12 @@ import type {
|
|
|
13
13
|
FormValidateOrFn,
|
|
14
14
|
} from '@tanstack/form-core'
|
|
15
15
|
import type { AppFieldExtendedReactFormApi } from './createFormHook'
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
ComponentType,
|
|
18
|
+
FunctionComponent,
|
|
19
|
+
PropsWithChildren,
|
|
20
|
+
ReactNode,
|
|
21
|
+
} from 'react'
|
|
17
22
|
import type { LensFieldComponent } from './useField'
|
|
18
23
|
|
|
19
24
|
function LocalSubscribe({
|
|
@@ -23,10 +28,10 @@ function LocalSubscribe({
|
|
|
23
28
|
}: PropsWithChildren<{
|
|
24
29
|
lens: AnyFieldGroupApi
|
|
25
30
|
selector: (state: FieldGroupState<any>) => FieldGroupState<any>
|
|
26
|
-
}>) {
|
|
31
|
+
}>): ReturnType<FunctionComponent> {
|
|
27
32
|
const data = useStore(lens.store, selector)
|
|
28
33
|
|
|
29
|
-
return functionalUpdate(children, data)
|
|
34
|
+
return <>{functionalUpdate(children, data)}</>
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
|
@@ -73,7 +78,7 @@ export type AppFieldExtendedReactFieldGroupApi<
|
|
|
73
78
|
TSubmitMeta,
|
|
74
79
|
NoInfer<TFieldComponents>
|
|
75
80
|
>
|
|
76
|
-
AppForm: ComponentType<PropsWithChildren
|
|
81
|
+
AppForm: ComponentType<PropsWithChildren<{}>>
|
|
77
82
|
/**
|
|
78
83
|
* A React component to render form fields. With this, you can render and manage individual form fields.
|
|
79
84
|
*/
|
|
@@ -85,7 +90,7 @@ export type AppFieldExtendedReactFieldGroupApi<
|
|
|
85
90
|
Subscribe: <TSelected = NoInfer<FieldGroupState<TFieldGroupData>>>(props: {
|
|
86
91
|
selector?: (state: NoInfer<FieldGroupState<TFieldGroupData>>) => TSelected
|
|
87
92
|
children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode
|
|
88
|
-
}) =>
|
|
93
|
+
}) => ReturnType<FunctionComponent>
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
export function useFieldGroup<
|
package/src/useForm.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { FormApi, functionalUpdate } from '@tanstack/form-core'
|
|
3
|
+
import { FormApi, functionalUpdate, uuid } from '@tanstack/form-core'
|
|
4
4
|
import { useStore } from '@tanstack/react-store'
|
|
5
|
-
import {
|
|
5
|
+
import { useMemo, useState } from 'react'
|
|
6
6
|
import { Field } from './useField'
|
|
7
7
|
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
|
|
8
8
|
import type {
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
FormState,
|
|
14
14
|
FormValidateOrFn,
|
|
15
15
|
} from '@tanstack/form-core'
|
|
16
|
-
import type { PropsWithChildren, ReactNode } from 'react'
|
|
16
|
+
import type { FunctionComponent, PropsWithChildren, ReactNode } from 'react'
|
|
17
17
|
import type { FieldComponent } from './useField'
|
|
18
18
|
import type { NoInfer } from '@tanstack/react-store'
|
|
19
19
|
|
|
@@ -89,7 +89,7 @@ export interface ReactFormApi<
|
|
|
89
89
|
>,
|
|
90
90
|
) => TSelected
|
|
91
91
|
children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode
|
|
92
|
-
}) =>
|
|
92
|
+
}) => ReturnType<FunctionComponent>
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
@@ -144,10 +144,10 @@ function LocalSubscribe({
|
|
|
144
144
|
}: PropsWithChildren<{
|
|
145
145
|
form: AnyFormApi
|
|
146
146
|
selector: (state: AnyFormState) => AnyFormState
|
|
147
|
-
}>) {
|
|
147
|
+
}>): ReturnType<FunctionComponent> {
|
|
148
148
|
const data = useStore(form.store, selector)
|
|
149
149
|
|
|
150
|
-
return functionalUpdate(children, data)
|
|
150
|
+
return <>{functionalUpdate(children, data)}</>
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/**
|
|
@@ -184,10 +184,11 @@ export function useForm<
|
|
|
184
184
|
TSubmitMeta
|
|
185
185
|
>,
|
|
186
186
|
) {
|
|
187
|
-
const
|
|
187
|
+
const fallbackFormId = useState(() => uuid())[0]
|
|
188
|
+
const [prevFormId, setPrevFormId] = useState<string>(opts?.formId as never)
|
|
188
189
|
|
|
189
|
-
const [formApi] = useState(() => {
|
|
190
|
-
|
|
190
|
+
const [formApi, setFormApi] = useState(() => {
|
|
191
|
+
return new FormApi<
|
|
191
192
|
TFormData,
|
|
192
193
|
TOnMount,
|
|
193
194
|
TOnChange,
|
|
@@ -200,8 +201,16 @@ export function useForm<
|
|
|
200
201
|
TOnDynamicAsync,
|
|
201
202
|
TOnServer,
|
|
202
203
|
TSubmitMeta
|
|
203
|
-
>({ ...opts, formId: formId })
|
|
204
|
+
>({ ...opts, formId: opts?.formId ?? fallbackFormId })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
if (prevFormId !== opts?.formId) {
|
|
208
|
+
const formId = opts?.formId ?? fallbackFormId
|
|
209
|
+
setFormApi(new FormApi({ ...opts, formId }))
|
|
210
|
+
setPrevFormId(formId)
|
|
211
|
+
}
|
|
204
212
|
|
|
213
|
+
const extendedFormApi = useMemo(() => {
|
|
205
214
|
const extendedApi: ReactFormExtendedApi<
|
|
206
215
|
TFormData,
|
|
207
216
|
TOnMount,
|
|
@@ -215,16 +224,25 @@ export function useForm<
|
|
|
215
224
|
TOnDynamicAsync,
|
|
216
225
|
TOnServer,
|
|
217
226
|
TSubmitMeta
|
|
218
|
-
> =
|
|
227
|
+
> = {
|
|
228
|
+
...formApi,
|
|
229
|
+
// We must add all `get`ters from `core`'s `FormApi` here, as otherwise the spread operator won't catch those
|
|
230
|
+
get formId(): string {
|
|
231
|
+
return formApi._formId
|
|
232
|
+
},
|
|
233
|
+
get state() {
|
|
234
|
+
return formApi.store.state
|
|
235
|
+
},
|
|
236
|
+
} as never
|
|
219
237
|
|
|
220
238
|
extendedApi.Field = function APIField(props) {
|
|
221
|
-
return <Field {...props} form={
|
|
239
|
+
return <Field {...props} form={formApi} />
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
extendedApi.Subscribe = function Subscribe(props: any) {
|
|
225
243
|
return (
|
|
226
244
|
<LocalSubscribe
|
|
227
|
-
form={
|
|
245
|
+
form={formApi}
|
|
228
246
|
selector={props.selector}
|
|
229
247
|
children={props.children}
|
|
230
248
|
/>
|
|
@@ -232,7 +250,7 @@ export function useForm<
|
|
|
232
250
|
}
|
|
233
251
|
|
|
234
252
|
return extendedApi
|
|
235
|
-
})
|
|
253
|
+
}, [formApi])
|
|
236
254
|
|
|
237
255
|
useIsomorphicLayoutEffect(formApi.mount, [])
|
|
238
256
|
|
|
@@ -244,5 +262,5 @@ export function useForm<
|
|
|
244
262
|
formApi.update(opts)
|
|
245
263
|
})
|
|
246
264
|
|
|
247
|
-
return
|
|
265
|
+
return extendedFormApi
|
|
248
266
|
}
|