@tanstack/react-form 0.13.6 → 0.13.7
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/useField.cjs +1 -11
- package/dist/cjs/useField.cjs.map +1 -1
- package/dist/cjs/useForm.cjs.map +1 -1
- package/dist/cjs/useForm.d.cts +11 -0
- package/dist/esm/useField.js +2 -12
- package/dist/esm/useField.js.map +1 -1
- package/dist/esm/useForm.d.ts +11 -0
- package/dist/esm/useForm.js.map +1 -1
- package/package.json +3 -3
- package/src/tests/useField.test.tsx +161 -10
- package/src/tests/useForm.test.tsx +24 -0
- package/src/useField.tsx +3 -15
- package/src/useForm.tsx +11 -1
- package/dist/cjs/useIsomorphicEffectOnce.cjs +0 -30
- package/dist/cjs/useIsomorphicEffectOnce.cjs.map +0 -1
- package/dist/cjs/useIsomorphicEffectOnce.d.cts +0 -5
- package/dist/esm/useIsomorphicEffectOnce.d.ts +0 -5
- package/dist/esm/useIsomorphicEffectOnce.js +0 -30
- package/dist/esm/useIsomorphicEffectOnce.js.map +0 -1
- package/src/useIsomorphicEffectOnce.ts +0 -39
package/dist/cjs/useField.cjs
CHANGED
|
@@ -6,7 +6,6 @@ const reactStore = require("@tanstack/react-store");
|
|
|
6
6
|
const formCore = require("@tanstack/form-core");
|
|
7
7
|
const formContext = require("./formContext.cjs");
|
|
8
8
|
const useIsomorphicLayoutEffect = require("./useIsomorphicLayoutEffect.cjs");
|
|
9
|
-
const useIsomorphicEffectOnce = require("./useIsomorphicEffectOnce.cjs");
|
|
10
9
|
function useField(opts) {
|
|
11
10
|
const { formApi, parentFieldName } = formContext.useFormContext();
|
|
12
11
|
const [fieldApi] = rehackt.useState(() => {
|
|
@@ -20,6 +19,7 @@ function useField(opts) {
|
|
|
20
19
|
api.Field = Field;
|
|
21
20
|
return api;
|
|
22
21
|
});
|
|
22
|
+
useIsomorphicLayoutEffect.useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]);
|
|
23
23
|
useIsomorphicLayoutEffect.useIsomorphicLayoutEffect(() => {
|
|
24
24
|
fieldApi.update({ ...opts, form: formApi });
|
|
25
25
|
});
|
|
@@ -29,16 +29,6 @@ function useField(opts) {
|
|
|
29
29
|
return [state.meta, Object.keys(state.value).length];
|
|
30
30
|
} : void 0
|
|
31
31
|
);
|
|
32
|
-
const unmountFn = rehackt.useRef(null);
|
|
33
|
-
useIsomorphicEffectOnce.useIsomorphicEffectOnce(() => {
|
|
34
|
-
return () => {
|
|
35
|
-
var _a;
|
|
36
|
-
(_a = unmountFn.current) == null ? void 0 : _a.call(unmountFn);
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
if (!unmountFn.current) {
|
|
40
|
-
unmountFn.current = fieldApi.mount();
|
|
41
|
-
}
|
|
42
32
|
return fieldApi;
|
|
43
33
|
}
|
|
44
34
|
function Field({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useField.cjs","sources":["../../src/useField.tsx"],"sourcesContent":["import React, {
|
|
1
|
+
{"version":3,"file":"useField.cjs","sources":["../../src/useField.tsx"],"sourcesContent":["import React, { useState } from 'rehackt'\nimport { useStore } from '@tanstack/react-store'\nimport { FieldApi, functionalUpdate } from '@tanstack/form-core'\nimport { formContext, useFormContext } from './formContext'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { UseFieldOptions } from './types'\nimport type {\n DeepKeys,\n DeepValue,\n Narrow,\n Validator,\n} from '@tanstack/form-core'\n\ndeclare module '@tanstack/form-core' {\n // eslint-disable-next-line no-shadow\n interface FieldApi<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,\n > {\n Field: FieldComponent<TParentData, TFormValidator>\n }\n}\n\nexport type UseField<TParentData> = <\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n>(\n opts?: { name: Narrow<TName> } & UseFieldOptions<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator\n >,\n) => FieldApi<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator,\n DeepValue<TParentData, TName>\n>\n\nexport function useField<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n>(\n opts: UseFieldOptions<TParentData, TName, TFieldValidator, TFormValidator>,\n): FieldApi<TParentData, TName, TFieldValidator, TFormValidator> {\n // Get the form API either manually or from context\n const { formApi, parentFieldName } = useFormContext()\n\n const [fieldApi] = useState(() => {\n const name = (\n typeof opts.index === 'number'\n ? [parentFieldName, opts.index, opts.name]\n : [parentFieldName, opts.name]\n )\n .filter((d) => d !== undefined)\n .join('.')\n\n const api = new FieldApi({\n ...opts,\n form: formApi as never,\n // TODO: Fix typings to include `index` and `parentFieldName`, if present\n name: name as typeof opts.name as never,\n })\n\n api.Field = Field as never\n\n return api\n })\n\n useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])\n\n /**\n * fieldApi.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 fieldApi.update({ ...opts, form: formApi } as never)\n })\n\n useStore(\n fieldApi.store,\n opts.mode === 'array'\n ? (state) => {\n return [state.meta, Object.keys(state.value).length]\n }\n : undefined,\n )\n\n return fieldApi as never\n}\n\ntype FieldComponentProps<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,\n> = {\n children: (\n fieldApi: FieldApi<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator,\n TData\n >,\n ) => any\n} & (TParentData extends any[]\n ? {\n name?: TName\n index: number\n }\n : {\n name: TName\n index?: never\n }) &\n Omit<\n UseFieldOptions<TParentData, TName, TFieldValidator, TFormValidator>,\n 'name' | 'index'\n >\n\nexport type FieldComponent<\n TParentData,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n> = <\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,\n>({\n children,\n ...fieldOptions\n}: FieldComponentProps<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator,\n TData\n>) => any\n\nexport function Field<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n>({\n children,\n ...fieldOptions\n}: {\n children: (\n fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator>,\n ) => any\n} & UseFieldOptions<TParentData, TName, TFieldValidator, TFormValidator>) {\n const fieldApi = useField(fieldOptions as any)\n\n return (\n <formContext.Provider\n value={{\n formApi: fieldApi.form as never,\n parentFieldName: fieldApi.name,\n }}\n children={functionalUpdate(children, fieldApi as any)}\n />\n )\n}\n"],"names":["useFormContext","useState","FieldApi","useIsomorphicLayoutEffect","useStore","jsx","formContext","functionalUpdate"],"mappings":";;;;;;;;AAqDO,SAAS,SAUd,MAC+D;AAE/D,QAAM,EAAE,SAAS,gBAAgB,IAAIA,YAAe,eAAA;AAEpD,QAAM,CAAC,QAAQ,IAAIC,QAAAA,SAAS,MAAM;AAC1B,UAAA,QACJ,OAAO,KAAK,UAAU,WAClB,CAAC,iBAAiB,KAAK,OAAO,KAAK,IAAI,IACvC,CAAC,iBAAiB,KAAK,IAAI,GAE9B,OAAO,CAAC,MAAM,MAAM,MAAS,EAC7B,KAAK,GAAG;AAEL,UAAA,MAAM,IAAIC,kBAAS;AAAA,MACvB,GAAG;AAAA,MACH,MAAM;AAAA;AAAA,MAEN;AAAA,IAAA,CACD;AAED,QAAI,QAAQ;AAEL,WAAA;AAAA,EAAA,CACR;AAEDC,4BAAAA,0BAA0B,SAAS,OAAO,CAAC,QAAQ,CAAC;AAMpDA,4BAAAA,0BAA0B,MAAM;AAC9B,aAAS,OAAO,EAAE,GAAG,MAAM,MAAM,SAAkB;AAAA,EAAA,CACpD;AAEDC,aAAA;AAAA,IACE,SAAS;AAAA,IACT,KAAK,SAAS,UACV,CAAC,UAAU;AACF,aAAA,CAAC,MAAM,MAAM,OAAO,KAAK,MAAM,KAAK,EAAE,MAAM;AAAA,IAErD,IAAA;AAAA,EAAA;AAGC,SAAA;AACT;AA0DO,SAAS,MASd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAI0E;AAClE,QAAA,WAAW,SAAS,YAAmB;AAG3C,SAAAC,2BAAA;AAAA,IAACC,YAAAA,YAAY;AAAA,IAAZ;AAAA,MACC,OAAO;AAAA,QACL,SAAS,SAAS;AAAA,QAClB,iBAAiB,SAAS;AAAA,MAC5B;AAAA,MACA,UAAUC,SAAAA,iBAAiB,UAAU,QAAe;AAAA,IAAA;AAAA,EAAA;AAG1D;;;"}
|
package/dist/cjs/useForm.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.cjs","sources":["../../src/useForm.tsx"],"sourcesContent":["import { FormApi, functionalUpdate } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport React, {\n type PropsWithChildren,\n type ReactNode,\n useState,\n} from 'rehackt'\nimport { Field, useField } from './useField'\nimport { formContext } from './formContext'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { NoInfer } from '@tanstack/react-store'\nimport type { FormOptions, FormState, Validator } from '@tanstack/form-core'\nimport type { FieldComponent, UseField } from './useField'\n\ndeclare module '@tanstack/form-core' {\n // eslint-disable-next-line no-shadow\n interface FormApi<TFormData, TFormValidator> {\n Provider: (props: PropsWithChildren) => JSX.Element\n Field: FieldComponent<TFormData, TFormValidator>\n useField: UseField<TFormData>\n useStore: <TSelected = NoInfer<FormState<TFormData>>>(\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,\n ) => TSelected\n Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected\n children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode\n }) => JSX.Element\n }\n}\n\nexport function useForm<\n TFormData,\n TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,\n>(\n opts?: FormOptions<TFormData, TFormValidator>,\n): FormApi<TFormData, TFormValidator> {\n const [formApi] = useState(() => {\n
|
|
1
|
+
{"version":3,"file":"useForm.cjs","sources":["../../src/useForm.tsx"],"sourcesContent":["import { FormApi, functionalUpdate } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport React, {\n type PropsWithChildren,\n type ReactNode,\n useState,\n} from 'rehackt'\nimport { Field, useField } from './useField'\nimport { formContext } from './formContext'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { NoInfer } from '@tanstack/react-store'\nimport type { FormOptions, FormState, Validator } from '@tanstack/form-core'\nimport type { FieldComponent, UseField } from './useField'\n\ndeclare module '@tanstack/form-core' {\n // eslint-disable-next-line no-shadow\n interface FormApi<TFormData, TFormValidator> {\n Provider: (props: PropsWithChildren) => JSX.Element\n Field: FieldComponent<TFormData, TFormValidator>\n useField: UseField<TFormData>\n useStore: <TSelected = NoInfer<FormState<TFormData>>>(\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,\n ) => TSelected\n Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {\n /**\n TypeScript versions <=5.0.4 have a bug that prevents\n the type of the `TSelected` generic from being inferred\n from the return type of this method.\n\n In these versions, `TSelected` will fall back to the default\n type (or `unknown` if that's not defined).\n\n @see {@link https://github.com/TanStack/form/pull/606/files#r1506715714 | This discussion on GitHub for the details}\n @see {@link https://github.com/microsoft/TypeScript/issues/52786 | The bug report in `microsoft/TypeScript`}\n */\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected\n children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode\n }) => JSX.Element\n }\n}\n\nexport function useForm<\n TFormData,\n TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,\n>(\n opts?: FormOptions<TFormData, TFormValidator>,\n): FormApi<TFormData, TFormValidator> {\n const [formApi] = useState(() => {\n const api = new FormApi<TFormData, TFormValidator>(opts)\n\n api.Provider = function Provider(props) {\n useIsomorphicLayoutEffect(api.mount, [])\n return (\n <formContext.Provider {...props} value={{ formApi: api as never }} />\n )\n }\n api.Field = Field as any\n api.useField = useField as any\n api.useStore = (\n // @ts-ignore\n selector,\n ) => {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useStore(api.store as any, selector as any) as any\n }\n api.Subscribe = (\n // @ts-ignore\n props,\n ) => {\n return functionalUpdate(\n props.children,\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useStore(api.store as any, props.selector as any),\n ) as any\n }\n\n return api\n })\n\n formApi.useStore((state) => state.isSubmitting)\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 formApi as any\n}\n"],"names":["useState","FormApi","useIsomorphicLayoutEffect","jsx","formContext","Field","useField","useStore","functionalUpdate"],"mappings":";;;;;;;;;AAyCO,SAAS,QAId,MACoC;AACpC,QAAM,CAAC,OAAO,IAAIA,QAAAA,SAAS,MAAM;AACzB,UAAA,MAAM,IAAIC,iBAAmC,IAAI;AAEnD,QAAA,WAAW,SAAS,SAAS,OAAO;AACZC,gCAAAA,0BAAA,IAAI,OAAO,CAAA,CAAE;AAErC,aAAAC,+BAACC,YAAAA,YAAY,UAAZ,EAAsB,GAAG,OAAO,OAAO,EAAE,SAAS,IAAgB,EAAA,CAAA;AAAA,IAAA;AAGvE,QAAI,QAAQC;AACZ,QAAI,WAAWC;AACX,QAAA,WAAW,CAEb,aACG;AAEI,aAAAC,oBAAS,IAAI,OAAc,QAAe;AAAA,IAAA;AAE/C,QAAA,YAAY,CAEd,UACG;AACI,aAAAC,SAAA;AAAA,QACL,MAAM;AAAA;AAAA,QAEND,WAAAA,SAAS,IAAI,OAAc,MAAM,QAAe;AAAA,MAAA;AAAA,IAClD;AAGK,WAAA;AAAA,EAAA,CACR;AAED,UAAQ,SAAS,CAAC,UAAU,MAAM,YAAY;AAM9CL,4BAAAA,0BAA0B,MAAM;AAC9B,YAAQ,OAAO,IAAI;AAAA,EAAA,CACpB;AAEM,SAAA;AACT;;"}
|
package/dist/cjs/useForm.d.cts
CHANGED
|
@@ -11,6 +11,17 @@ declare module '@tanstack/form-core' {
|
|
|
11
11
|
useField: UseField<TFormData>;
|
|
12
12
|
useStore: <TSelected = NoInfer<FormState<TFormData>>>(selector?: (state: NoInfer<FormState<TFormData>>) => TSelected) => TSelected;
|
|
13
13
|
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
|
|
14
|
+
/**
|
|
15
|
+
TypeScript versions <=5.0.4 have a bug that prevents
|
|
16
|
+
the type of the `TSelected` generic from being inferred
|
|
17
|
+
from the return type of this method.
|
|
18
|
+
|
|
19
|
+
In these versions, `TSelected` will fall back to the default
|
|
20
|
+
type (or `unknown` if that's not defined).
|
|
21
|
+
|
|
22
|
+
@see {@link https://github.com/TanStack/form/pull/606/files#r1506715714 | This discussion on GitHub for the details}
|
|
23
|
+
@see {@link https://github.com/microsoft/TypeScript/issues/52786 | The bug report in `microsoft/TypeScript`}
|
|
24
|
+
*/
|
|
14
25
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected;
|
|
15
26
|
children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode;
|
|
16
27
|
}) => JSX.Element;
|
package/dist/esm/useField.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState
|
|
2
|
+
import { useState } from "rehackt";
|
|
3
3
|
import { useStore } from "@tanstack/react-store";
|
|
4
4
|
import { FieldApi, functionalUpdate } from "@tanstack/form-core";
|
|
5
5
|
import { useFormContext, formContext } from "./formContext.js";
|
|
6
6
|
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
|
|
7
|
-
import { useIsomorphicEffectOnce } from "./useIsomorphicEffectOnce.js";
|
|
8
7
|
function useField(opts) {
|
|
9
8
|
const { formApi, parentFieldName } = useFormContext();
|
|
10
9
|
const [fieldApi] = useState(() => {
|
|
@@ -18,6 +17,7 @@ function useField(opts) {
|
|
|
18
17
|
api.Field = Field;
|
|
19
18
|
return api;
|
|
20
19
|
});
|
|
20
|
+
useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]);
|
|
21
21
|
useIsomorphicLayoutEffect(() => {
|
|
22
22
|
fieldApi.update({ ...opts, form: formApi });
|
|
23
23
|
});
|
|
@@ -27,16 +27,6 @@ function useField(opts) {
|
|
|
27
27
|
return [state.meta, Object.keys(state.value).length];
|
|
28
28
|
} : void 0
|
|
29
29
|
);
|
|
30
|
-
const unmountFn = useRef(null);
|
|
31
|
-
useIsomorphicEffectOnce(() => {
|
|
32
|
-
return () => {
|
|
33
|
-
var _a;
|
|
34
|
-
(_a = unmountFn.current) == null ? void 0 : _a.call(unmountFn);
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
if (!unmountFn.current) {
|
|
38
|
-
unmountFn.current = fieldApi.mount();
|
|
39
|
-
}
|
|
40
30
|
return fieldApi;
|
|
41
31
|
}
|
|
42
32
|
function Field({
|
package/dist/esm/useField.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useField.js","sources":["../../src/useField.tsx"],"sourcesContent":["import React, {
|
|
1
|
+
{"version":3,"file":"useField.js","sources":["../../src/useField.tsx"],"sourcesContent":["import React, { useState } from 'rehackt'\nimport { useStore } from '@tanstack/react-store'\nimport { FieldApi, functionalUpdate } from '@tanstack/form-core'\nimport { formContext, useFormContext } from './formContext'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { UseFieldOptions } from './types'\nimport type {\n DeepKeys,\n DeepValue,\n Narrow,\n Validator,\n} from '@tanstack/form-core'\n\ndeclare module '@tanstack/form-core' {\n // eslint-disable-next-line no-shadow\n interface FieldApi<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,\n > {\n Field: FieldComponent<TParentData, TFormValidator>\n }\n}\n\nexport type UseField<TParentData> = <\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n>(\n opts?: { name: Narrow<TName> } & UseFieldOptions<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator\n >,\n) => FieldApi<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator,\n DeepValue<TParentData, TName>\n>\n\nexport function useField<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n>(\n opts: UseFieldOptions<TParentData, TName, TFieldValidator, TFormValidator>,\n): FieldApi<TParentData, TName, TFieldValidator, TFormValidator> {\n // Get the form API either manually or from context\n const { formApi, parentFieldName } = useFormContext()\n\n const [fieldApi] = useState(() => {\n const name = (\n typeof opts.index === 'number'\n ? [parentFieldName, opts.index, opts.name]\n : [parentFieldName, opts.name]\n )\n .filter((d) => d !== undefined)\n .join('.')\n\n const api = new FieldApi({\n ...opts,\n form: formApi as never,\n // TODO: Fix typings to include `index` and `parentFieldName`, if present\n name: name as typeof opts.name as never,\n })\n\n api.Field = Field as never\n\n return api\n })\n\n useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])\n\n /**\n * fieldApi.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 fieldApi.update({ ...opts, form: formApi } as never)\n })\n\n useStore(\n fieldApi.store,\n opts.mode === 'array'\n ? (state) => {\n return [state.meta, Object.keys(state.value).length]\n }\n : undefined,\n )\n\n return fieldApi as never\n}\n\ntype FieldComponentProps<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,\n> = {\n children: (\n fieldApi: FieldApi<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator,\n TData\n >,\n ) => any\n} & (TParentData extends any[]\n ? {\n name?: TName\n index: number\n }\n : {\n name: TName\n index?: never\n }) &\n Omit<\n UseFieldOptions<TParentData, TName, TFieldValidator, TFormValidator>,\n 'name' | 'index'\n >\n\nexport type FieldComponent<\n TParentData,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n> = <\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,\n>({\n children,\n ...fieldOptions\n}: FieldComponentProps<\n TParentData,\n TName,\n TFieldValidator,\n TFormValidator,\n TData\n>) => any\n\nexport function Field<\n TParentData,\n TName extends DeepKeys<TParentData>,\n TFieldValidator extends\n | Validator<DeepValue<TParentData, TName>, unknown>\n | undefined = undefined,\n TFormValidator extends\n | Validator<TParentData, unknown>\n | undefined = undefined,\n>({\n children,\n ...fieldOptions\n}: {\n children: (\n fieldApi: FieldApi<TParentData, TName, TFieldValidator, TFormValidator>,\n ) => any\n} & UseFieldOptions<TParentData, TName, TFieldValidator, TFormValidator>) {\n const fieldApi = useField(fieldOptions as any)\n\n return (\n <formContext.Provider\n value={{\n formApi: fieldApi.form as never,\n parentFieldName: fieldApi.name,\n }}\n children={functionalUpdate(children, fieldApi as any)}\n />\n )\n}\n"],"names":[],"mappings":";;;;;;AAqDO,SAAS,SAUd,MAC+D;AAE/D,QAAM,EAAE,SAAS,gBAAgB,IAAI,eAAe;AAEpD,QAAM,CAAC,QAAQ,IAAI,SAAS,MAAM;AAC1B,UAAA,QACJ,OAAO,KAAK,UAAU,WAClB,CAAC,iBAAiB,KAAK,OAAO,KAAK,IAAI,IACvC,CAAC,iBAAiB,KAAK,IAAI,GAE9B,OAAO,CAAC,MAAM,MAAM,MAAS,EAC7B,KAAK,GAAG;AAEL,UAAA,MAAM,IAAI,SAAS;AAAA,MACvB,GAAG;AAAA,MACH,MAAM;AAAA;AAAA,MAEN;AAAA,IAAA,CACD;AAED,QAAI,QAAQ;AAEL,WAAA;AAAA,EAAA,CACR;AAED,4BAA0B,SAAS,OAAO,CAAC,QAAQ,CAAC;AAMpD,4BAA0B,MAAM;AAC9B,aAAS,OAAO,EAAE,GAAG,MAAM,MAAM,SAAkB;AAAA,EAAA,CACpD;AAED;AAAA,IACE,SAAS;AAAA,IACT,KAAK,SAAS,UACV,CAAC,UAAU;AACF,aAAA,CAAC,MAAM,MAAM,OAAO,KAAK,MAAM,KAAK,EAAE,MAAM;AAAA,IAErD,IAAA;AAAA,EAAA;AAGC,SAAA;AACT;AA0DO,SAAS,MASd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAI0E;AAClE,QAAA,WAAW,SAAS,YAAmB;AAG3C,SAAA;AAAA,IAAC,YAAY;AAAA,IAAZ;AAAA,MACC,OAAO;AAAA,QACL,SAAS,SAAS;AAAA,QAClB,iBAAiB,SAAS;AAAA,MAC5B;AAAA,MACA,UAAU,iBAAiB,UAAU,QAAe;AAAA,IAAA;AAAA,EAAA;AAG1D;"}
|
package/dist/esm/useForm.d.ts
CHANGED
|
@@ -11,6 +11,17 @@ declare module '@tanstack/form-core' {
|
|
|
11
11
|
useField: UseField<TFormData>;
|
|
12
12
|
useStore: <TSelected = NoInfer<FormState<TFormData>>>(selector?: (state: NoInfer<FormState<TFormData>>) => TSelected) => TSelected;
|
|
13
13
|
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
|
|
14
|
+
/**
|
|
15
|
+
TypeScript versions <=5.0.4 have a bug that prevents
|
|
16
|
+
the type of the `TSelected` generic from being inferred
|
|
17
|
+
from the return type of this method.
|
|
18
|
+
|
|
19
|
+
In these versions, `TSelected` will fall back to the default
|
|
20
|
+
type (or `unknown` if that's not defined).
|
|
21
|
+
|
|
22
|
+
@see {@link https://github.com/TanStack/form/pull/606/files#r1506715714 | This discussion on GitHub for the details}
|
|
23
|
+
@see {@link https://github.com/microsoft/TypeScript/issues/52786 | The bug report in `microsoft/TypeScript`}
|
|
24
|
+
*/
|
|
14
25
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected;
|
|
15
26
|
children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode;
|
|
16
27
|
}) => JSX.Element;
|
package/dist/esm/useForm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.js","sources":["../../src/useForm.tsx"],"sourcesContent":["import { FormApi, functionalUpdate } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport React, {\n type PropsWithChildren,\n type ReactNode,\n useState,\n} from 'rehackt'\nimport { Field, useField } from './useField'\nimport { formContext } from './formContext'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { NoInfer } from '@tanstack/react-store'\nimport type { FormOptions, FormState, Validator } from '@tanstack/form-core'\nimport type { FieldComponent, UseField } from './useField'\n\ndeclare module '@tanstack/form-core' {\n // eslint-disable-next-line no-shadow\n interface FormApi<TFormData, TFormValidator> {\n Provider: (props: PropsWithChildren) => JSX.Element\n Field: FieldComponent<TFormData, TFormValidator>\n useField: UseField<TFormData>\n useStore: <TSelected = NoInfer<FormState<TFormData>>>(\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,\n ) => TSelected\n Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected\n children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode\n }) => JSX.Element\n }\n}\n\nexport function useForm<\n TFormData,\n TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,\n>(\n opts?: FormOptions<TFormData, TFormValidator>,\n): FormApi<TFormData, TFormValidator> {\n const [formApi] = useState(() => {\n
|
|
1
|
+
{"version":3,"file":"useForm.js","sources":["../../src/useForm.tsx"],"sourcesContent":["import { FormApi, functionalUpdate } from '@tanstack/form-core'\nimport { useStore } from '@tanstack/react-store'\nimport React, {\n type PropsWithChildren,\n type ReactNode,\n useState,\n} from 'rehackt'\nimport { Field, useField } from './useField'\nimport { formContext } from './formContext'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { NoInfer } from '@tanstack/react-store'\nimport type { FormOptions, FormState, Validator } from '@tanstack/form-core'\nimport type { FieldComponent, UseField } from './useField'\n\ndeclare module '@tanstack/form-core' {\n // eslint-disable-next-line no-shadow\n interface FormApi<TFormData, TFormValidator> {\n Provider: (props: PropsWithChildren) => JSX.Element\n Field: FieldComponent<TFormData, TFormValidator>\n useField: UseField<TFormData>\n useStore: <TSelected = NoInfer<FormState<TFormData>>>(\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,\n ) => TSelected\n Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {\n /**\n TypeScript versions <=5.0.4 have a bug that prevents\n the type of the `TSelected` generic from being inferred\n from the return type of this method.\n\n In these versions, `TSelected` will fall back to the default\n type (or `unknown` if that's not defined).\n\n @see {@link https://github.com/TanStack/form/pull/606/files#r1506715714 | This discussion on GitHub for the details}\n @see {@link https://github.com/microsoft/TypeScript/issues/52786 | The bug report in `microsoft/TypeScript`}\n */\n selector?: (state: NoInfer<FormState<TFormData>>) => TSelected\n children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode\n }) => JSX.Element\n }\n}\n\nexport function useForm<\n TFormData,\n TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,\n>(\n opts?: FormOptions<TFormData, TFormValidator>,\n): FormApi<TFormData, TFormValidator> {\n const [formApi] = useState(() => {\n const api = new FormApi<TFormData, TFormValidator>(opts)\n\n api.Provider = function Provider(props) {\n useIsomorphicLayoutEffect(api.mount, [])\n return (\n <formContext.Provider {...props} value={{ formApi: api as never }} />\n )\n }\n api.Field = Field as any\n api.useField = useField as any\n api.useStore = (\n // @ts-ignore\n selector,\n ) => {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return useStore(api.store as any, selector as any) as any\n }\n api.Subscribe = (\n // @ts-ignore\n props,\n ) => {\n return functionalUpdate(\n props.children,\n // eslint-disable-next-line react-hooks/rules-of-hooks\n useStore(api.store as any, props.selector as any),\n ) as any\n }\n\n return api\n })\n\n formApi.useStore((state) => state.isSubmitting)\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 formApi as any\n}\n"],"names":[],"mappings":";;;;;;;AAyCO,SAAS,QAId,MACoC;AACpC,QAAM,CAAC,OAAO,IAAI,SAAS,MAAM;AACzB,UAAA,MAAM,IAAI,QAAmC,IAAI;AAEnD,QAAA,WAAW,SAAS,SAAS,OAAO;AACZ,gCAAA,IAAI,OAAO,CAAA,CAAE;AAErC,aAAA,oBAAC,YAAY,UAAZ,EAAsB,GAAG,OAAO,OAAO,EAAE,SAAS,IAAgB,EAAA,CAAA;AAAA,IAAA;AAGvE,QAAI,QAAQ;AACZ,QAAI,WAAW;AACX,QAAA,WAAW,CAEb,aACG;AAEI,aAAA,SAAS,IAAI,OAAc,QAAe;AAAA,IAAA;AAE/C,QAAA,YAAY,CAEd,UACG;AACI,aAAA;AAAA,QACL,MAAM;AAAA;AAAA,QAEN,SAAS,IAAI,OAAc,MAAM,QAAe;AAAA,MAAA;AAAA,IAClD;AAGK,WAAA;AAAA,EAAA,CACR;AAED,UAAQ,SAAS,CAAC,UAAU,MAAM,YAAY;AAM9C,4BAA0B,MAAM;AAC9B,YAAQ,OAAO,IAAI;AAAA,EAAA,CACpB;AAEM,SAAA;AACT;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-form",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.7",
|
|
4
4
|
"description": "Powerful, type-safe forms for React.",
|
|
5
5
|
"author": "tannerlinsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@tanstack/react-store": "^0.3.1",
|
|
44
44
|
"decode-formdata": "^0.4.0",
|
|
45
45
|
"rehackt": "^0.0.3",
|
|
46
|
-
"@tanstack/form-core": "0.13.
|
|
46
|
+
"@tanstack/form-core": "0.13.7"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"react": "^17.0.0 || ^18.0.0"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"clean": "rimraf ./dist && rimraf ./coverage",
|
|
53
53
|
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
|
54
54
|
"test:types:versions49": "node ../../node_modules/typescript49/lib/tsc.js --project tsconfig.legacy.json",
|
|
55
|
-
"test:types:versions50": "node ../../node_modules/typescript50/lib/tsc.js",
|
|
55
|
+
"test:types:versions50": "node ../../node_modules/typescript50/lib/tsc.js --project tsconfig.50.json",
|
|
56
56
|
"test:types:versions51": "node ../../node_modules/typescript51/lib/tsc.js",
|
|
57
57
|
"test:types:versions52": "tsc",
|
|
58
58
|
"test:types": "pnpm run \"/^test:types:versions.*/\"",
|
|
@@ -3,9 +3,9 @@ import * as React from 'react'
|
|
|
3
3
|
import { render, waitFor } from '@testing-library/react'
|
|
4
4
|
import userEvent from '@testing-library/user-event'
|
|
5
5
|
import '@testing-library/jest-dom'
|
|
6
|
-
import { createFormFactory } from '../index'
|
|
6
|
+
import { createFormFactory, useForm } from '../index'
|
|
7
7
|
import { sleep } from './utils'
|
|
8
|
-
import type { FormApi } from '../index'
|
|
8
|
+
import type { FieldApi, FormApi } from '../index'
|
|
9
9
|
|
|
10
10
|
const user = userEvent.setup()
|
|
11
11
|
|
|
@@ -101,7 +101,12 @@ describe('useField', () => {
|
|
|
101
101
|
const formFactory = createFormFactory<Person>()
|
|
102
102
|
|
|
103
103
|
function Comp() {
|
|
104
|
-
const form = formFactory.useForm(
|
|
104
|
+
const form = formFactory.useForm({
|
|
105
|
+
defaultValues: {
|
|
106
|
+
firstName: '',
|
|
107
|
+
lastName: '',
|
|
108
|
+
},
|
|
109
|
+
})
|
|
105
110
|
|
|
106
111
|
return (
|
|
107
112
|
<form.Provider>
|
|
@@ -143,7 +148,12 @@ describe('useField', () => {
|
|
|
143
148
|
const formFactory = createFormFactory<Person>()
|
|
144
149
|
|
|
145
150
|
function Comp() {
|
|
146
|
-
const form = formFactory.useForm(
|
|
151
|
+
const form = formFactory.useForm({
|
|
152
|
+
defaultValues: {
|
|
153
|
+
firstName: '',
|
|
154
|
+
lastName: '',
|
|
155
|
+
},
|
|
156
|
+
})
|
|
147
157
|
|
|
148
158
|
return (
|
|
149
159
|
<form.Provider>
|
|
@@ -188,7 +198,12 @@ describe('useField', () => {
|
|
|
188
198
|
const formFactory = createFormFactory<Person>()
|
|
189
199
|
|
|
190
200
|
function Comp() {
|
|
191
|
-
const form = formFactory.useForm(
|
|
201
|
+
const form = formFactory.useForm({
|
|
202
|
+
defaultValues: {
|
|
203
|
+
firstName: '',
|
|
204
|
+
lastName: '',
|
|
205
|
+
},
|
|
206
|
+
})
|
|
192
207
|
|
|
193
208
|
return (
|
|
194
209
|
<form.Provider>
|
|
@@ -239,7 +254,12 @@ describe('useField', () => {
|
|
|
239
254
|
const formFactory = createFormFactory<Person>()
|
|
240
255
|
|
|
241
256
|
function Comp() {
|
|
242
|
-
const form = formFactory.useForm(
|
|
257
|
+
const form = formFactory.useForm({
|
|
258
|
+
defaultValues: {
|
|
259
|
+
firstName: '',
|
|
260
|
+
lastName: '',
|
|
261
|
+
},
|
|
262
|
+
})
|
|
243
263
|
|
|
244
264
|
return (
|
|
245
265
|
<form.Provider>
|
|
@@ -288,7 +308,12 @@ describe('useField', () => {
|
|
|
288
308
|
const formFactory = createFormFactory<Person>()
|
|
289
309
|
|
|
290
310
|
function Comp() {
|
|
291
|
-
const form = formFactory.useForm(
|
|
311
|
+
const form = formFactory.useForm({
|
|
312
|
+
defaultValues: {
|
|
313
|
+
firstName: '',
|
|
314
|
+
lastName: '',
|
|
315
|
+
},
|
|
316
|
+
})
|
|
292
317
|
|
|
293
318
|
return (
|
|
294
319
|
<form.Provider>
|
|
@@ -346,7 +371,12 @@ describe('useField', () => {
|
|
|
346
371
|
const formFactory = createFormFactory<Person>()
|
|
347
372
|
|
|
348
373
|
function Comp() {
|
|
349
|
-
const form = formFactory.useForm(
|
|
374
|
+
const form = formFactory.useForm({
|
|
375
|
+
defaultValues: {
|
|
376
|
+
firstName: '',
|
|
377
|
+
lastName: '',
|
|
378
|
+
},
|
|
379
|
+
})
|
|
350
380
|
|
|
351
381
|
return (
|
|
352
382
|
<form.Provider>
|
|
@@ -395,7 +425,12 @@ describe('useField', () => {
|
|
|
395
425
|
const formFactory = createFormFactory<Person>()
|
|
396
426
|
let form: FormApi<Person> | null = null
|
|
397
427
|
function Comp() {
|
|
398
|
-
form = formFactory.useForm(
|
|
428
|
+
form = formFactory.useForm({
|
|
429
|
+
defaultValues: {
|
|
430
|
+
firstName: '',
|
|
431
|
+
lastName: '',
|
|
432
|
+
},
|
|
433
|
+
})
|
|
399
434
|
return (
|
|
400
435
|
<form.Provider>
|
|
401
436
|
<form.Field
|
|
@@ -433,7 +468,12 @@ describe('useField', () => {
|
|
|
433
468
|
const formFactory = createFormFactory<Person>()
|
|
434
469
|
let form: FormApi<Person> | null = null
|
|
435
470
|
function Comp() {
|
|
436
|
-
form = formFactory.useForm(
|
|
471
|
+
form = formFactory.useForm({
|
|
472
|
+
defaultValues: {
|
|
473
|
+
firstName: '',
|
|
474
|
+
lastName: '',
|
|
475
|
+
},
|
|
476
|
+
})
|
|
437
477
|
return (
|
|
438
478
|
<form.Provider>
|
|
439
479
|
<form.Field
|
|
@@ -462,4 +502,115 @@ describe('useField', () => {
|
|
|
462
502
|
const info = form!.fieldInfo
|
|
463
503
|
expect(Object.keys(info)).toHaveLength(0)
|
|
464
504
|
})
|
|
505
|
+
|
|
506
|
+
it('should handle strict mode properly with conditional fields', async () => {
|
|
507
|
+
function FieldInfo({ field }: { field: FieldApi<any, any> }) {
|
|
508
|
+
return (
|
|
509
|
+
<>
|
|
510
|
+
{field.state.meta.touchedErrors ? (
|
|
511
|
+
<em>{field.state.meta.touchedErrors}</em>
|
|
512
|
+
) : null}
|
|
513
|
+
{field.state.meta.isValidating ? 'Validating...' : null}
|
|
514
|
+
</>
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function Comp() {
|
|
519
|
+
const [showField, setShowField] = React.useState(true)
|
|
520
|
+
|
|
521
|
+
const form = useForm({
|
|
522
|
+
defaultValues: {
|
|
523
|
+
firstName: '',
|
|
524
|
+
lastName: '',
|
|
525
|
+
},
|
|
526
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
527
|
+
onSubmit: async () => {},
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<div>
|
|
532
|
+
<form.Provider>
|
|
533
|
+
<form
|
|
534
|
+
onSubmit={(e) => {
|
|
535
|
+
e.preventDefault()
|
|
536
|
+
e.stopPropagation()
|
|
537
|
+
void form.handleSubmit()
|
|
538
|
+
}}
|
|
539
|
+
>
|
|
540
|
+
<div>
|
|
541
|
+
{/* A type-safe field component*/}
|
|
542
|
+
{showField ? (
|
|
543
|
+
<form.Field
|
|
544
|
+
name="firstName"
|
|
545
|
+
validators={{
|
|
546
|
+
onChange: ({ value }) =>
|
|
547
|
+
!value ? 'A first name is required' : undefined,
|
|
548
|
+
}}
|
|
549
|
+
children={(field) => {
|
|
550
|
+
// Avoid hasty abstractions. Render props are great!
|
|
551
|
+
return (
|
|
552
|
+
<>
|
|
553
|
+
<label htmlFor={field.name}>First Name:</label>
|
|
554
|
+
<input
|
|
555
|
+
name={field.name}
|
|
556
|
+
value={field.state.value}
|
|
557
|
+
onBlur={field.handleBlur}
|
|
558
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
559
|
+
/>
|
|
560
|
+
<FieldInfo field={field} />
|
|
561
|
+
</>
|
|
562
|
+
)
|
|
563
|
+
}}
|
|
564
|
+
/>
|
|
565
|
+
) : null}
|
|
566
|
+
</div>
|
|
567
|
+
<div>
|
|
568
|
+
<form.Field
|
|
569
|
+
name="lastName"
|
|
570
|
+
children={(field) => (
|
|
571
|
+
<>
|
|
572
|
+
<label htmlFor={field.name}>Last Name:</label>
|
|
573
|
+
<input
|
|
574
|
+
name={field.name}
|
|
575
|
+
value={field.state.value}
|
|
576
|
+
onBlur={field.handleBlur}
|
|
577
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
578
|
+
/>
|
|
579
|
+
<FieldInfo field={field} />
|
|
580
|
+
</>
|
|
581
|
+
)}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
<form.Subscribe
|
|
585
|
+
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
|
586
|
+
children={([canSubmit, isSubmitting]) => (
|
|
587
|
+
<button type="submit" disabled={!canSubmit}>
|
|
588
|
+
{isSubmitting ? '...' : 'Submit'}
|
|
589
|
+
</button>
|
|
590
|
+
)}
|
|
591
|
+
/>
|
|
592
|
+
<button
|
|
593
|
+
type="button"
|
|
594
|
+
onClick={() => setShowField((prev) => !prev)}
|
|
595
|
+
>
|
|
596
|
+
{showField ? 'Hide field' : 'Show field'}
|
|
597
|
+
</button>
|
|
598
|
+
</form>
|
|
599
|
+
</form.Provider>
|
|
600
|
+
</div>
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const { getByText, findByText, queryByText } = render(
|
|
605
|
+
<React.StrictMode>
|
|
606
|
+
<Comp />
|
|
607
|
+
</React.StrictMode>,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
await user.click(getByText('Submit'))
|
|
611
|
+
expect(await findByText('A first name is required')).toBeInTheDocument()
|
|
612
|
+
await user.click(getByText('Hide field'))
|
|
613
|
+
await user.click(getByText('Submit'))
|
|
614
|
+
expect(queryByText('A first name is required')).not.toBeInTheDocument()
|
|
615
|
+
})
|
|
465
616
|
})
|
|
@@ -172,6 +172,10 @@ describe('useForm', () => {
|
|
|
172
172
|
|
|
173
173
|
function Comp() {
|
|
174
174
|
const form = formFactory.useForm({
|
|
175
|
+
defaultValues: {
|
|
176
|
+
firstName: '',
|
|
177
|
+
lastName: '',
|
|
178
|
+
},
|
|
175
179
|
validators: {
|
|
176
180
|
onChange() {
|
|
177
181
|
return error
|
|
@@ -217,6 +221,10 @@ describe('useForm', () => {
|
|
|
217
221
|
|
|
218
222
|
function Comp() {
|
|
219
223
|
const form = formFactory.useForm({
|
|
224
|
+
defaultValues: {
|
|
225
|
+
firstName: '',
|
|
226
|
+
lastName: '',
|
|
227
|
+
},
|
|
220
228
|
validators: {
|
|
221
229
|
onChange: ({ value }) =>
|
|
222
230
|
value.firstName === 'other' ? error : undefined,
|
|
@@ -262,6 +270,10 @@ describe('useForm', () => {
|
|
|
262
270
|
|
|
263
271
|
function Comp() {
|
|
264
272
|
const form = formFactory.useForm({
|
|
273
|
+
defaultValues: {
|
|
274
|
+
firstName: '',
|
|
275
|
+
lastName: '',
|
|
276
|
+
},
|
|
265
277
|
validators: {
|
|
266
278
|
onChange: ({ value }) =>
|
|
267
279
|
value.firstName === 'other' ? error : undefined,
|
|
@@ -362,6 +374,10 @@ describe('useForm', () => {
|
|
|
362
374
|
|
|
363
375
|
function Comp() {
|
|
364
376
|
const form = formFactory.useForm({
|
|
377
|
+
defaultValues: {
|
|
378
|
+
firstName: '',
|
|
379
|
+
lastName: '',
|
|
380
|
+
},
|
|
365
381
|
validators: {
|
|
366
382
|
onChangeAsync: async () => {
|
|
367
383
|
await sleep(10)
|
|
@@ -412,6 +428,10 @@ describe('useForm', () => {
|
|
|
412
428
|
|
|
413
429
|
function Comp() {
|
|
414
430
|
const form = formFactory.useForm({
|
|
431
|
+
defaultValues: {
|
|
432
|
+
firstName: '',
|
|
433
|
+
lastName: '',
|
|
434
|
+
},
|
|
415
435
|
validators: {
|
|
416
436
|
onChangeAsync: async () => {
|
|
417
437
|
await sleep(10)
|
|
@@ -472,6 +492,10 @@ describe('useForm', () => {
|
|
|
472
492
|
|
|
473
493
|
function Comp() {
|
|
474
494
|
const form = formFactory.useForm({
|
|
495
|
+
defaultValues: {
|
|
496
|
+
firstName: '',
|
|
497
|
+
lastName: '',
|
|
498
|
+
},
|
|
475
499
|
validators: {
|
|
476
500
|
onChangeAsyncDebounceMs: 100,
|
|
477
501
|
onChangeAsync: async () => {
|
package/src/useField.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from 'rehackt'
|
|
2
2
|
import { useStore } from '@tanstack/react-store'
|
|
3
3
|
import { FieldApi, functionalUpdate } from '@tanstack/form-core'
|
|
4
4
|
import { formContext, useFormContext } from './formContext'
|
|
5
5
|
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
|
|
6
|
-
import { useIsomorphicEffectOnce } from './useIsomorphicEffectOnce'
|
|
7
6
|
import type { UseFieldOptions } from './types'
|
|
8
7
|
import type {
|
|
9
8
|
DeepKeys,
|
|
@@ -88,6 +87,8 @@ export function useField<
|
|
|
88
87
|
return api
|
|
89
88
|
})
|
|
90
89
|
|
|
90
|
+
useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])
|
|
91
|
+
|
|
91
92
|
/**
|
|
92
93
|
* fieldApi.update should not have any side effects. Think of it like a `useRef`
|
|
93
94
|
* that we need to keep updated every render with the most up-to-date information.
|
|
@@ -104,19 +105,6 @@ export function useField<
|
|
|
104
105
|
}
|
|
105
106
|
: undefined,
|
|
106
107
|
)
|
|
107
|
-
const unmountFn = useRef<(() => void) | null>(null)
|
|
108
|
-
|
|
109
|
-
useIsomorphicEffectOnce(() => {
|
|
110
|
-
return () => {
|
|
111
|
-
unmountFn.current?.()
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
// We have to mount it right as soon as it renders, otherwise we get:
|
|
116
|
-
// https://github.com/TanStack/form/issues/523
|
|
117
|
-
if (!unmountFn.current) {
|
|
118
|
-
unmountFn.current = fieldApi.mount()
|
|
119
|
-
}
|
|
120
108
|
|
|
121
109
|
return fieldApi as never
|
|
122
110
|
}
|
package/src/useForm.tsx
CHANGED
|
@@ -22,6 +22,17 @@ declare module '@tanstack/form-core' {
|
|
|
22
22
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
|
|
23
23
|
) => TSelected
|
|
24
24
|
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
|
|
25
|
+
/**
|
|
26
|
+
TypeScript versions <=5.0.4 have a bug that prevents
|
|
27
|
+
the type of the `TSelected` generic from being inferred
|
|
28
|
+
from the return type of this method.
|
|
29
|
+
|
|
30
|
+
In these versions, `TSelected` will fall back to the default
|
|
31
|
+
type (or `unknown` if that's not defined).
|
|
32
|
+
|
|
33
|
+
@see {@link https://github.com/TanStack/form/pull/606/files#r1506715714 | This discussion on GitHub for the details}
|
|
34
|
+
@see {@link https://github.com/microsoft/TypeScript/issues/52786 | The bug report in `microsoft/TypeScript`}
|
|
35
|
+
*/
|
|
25
36
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected
|
|
26
37
|
children: ((state: NoInfer<TSelected>) => ReactNode) | ReactNode
|
|
27
38
|
}) => JSX.Element
|
|
@@ -35,7 +46,6 @@ export function useForm<
|
|
|
35
46
|
opts?: FormOptions<TFormData, TFormValidator>,
|
|
36
47
|
): FormApi<TFormData, TFormValidator> {
|
|
37
48
|
const [formApi] = useState(() => {
|
|
38
|
-
// @ts-ignore
|
|
39
49
|
const api = new FormApi<TFormData, TFormValidator>(opts)
|
|
40
50
|
|
|
41
51
|
api.Provider = function Provider(props) {
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const rehackt = require("rehackt");
|
|
4
|
-
const useIsomorphicLayoutEffect = require("./useIsomorphicLayoutEffect.cjs");
|
|
5
|
-
const useIsomorphicEffectOnce = (effect) => {
|
|
6
|
-
const destroyFunc = rehackt.useRef();
|
|
7
|
-
const effectCalled = rehackt.useRef(false);
|
|
8
|
-
const renderAfterCalled = rehackt.useRef(false);
|
|
9
|
-
const [val, setVal] = rehackt.useState(0);
|
|
10
|
-
if (effectCalled.current) {
|
|
11
|
-
renderAfterCalled.current = true;
|
|
12
|
-
}
|
|
13
|
-
useIsomorphicLayoutEffect.useIsomorphicLayoutEffect(() => {
|
|
14
|
-
if (!effectCalled.current) {
|
|
15
|
-
destroyFunc.current = effect();
|
|
16
|
-
effectCalled.current = true;
|
|
17
|
-
}
|
|
18
|
-
setVal((v) => v + 1);
|
|
19
|
-
return () => {
|
|
20
|
-
if (!renderAfterCalled.current) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (destroyFunc.current) {
|
|
24
|
-
destroyFunc.current();
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
}, []);
|
|
28
|
-
};
|
|
29
|
-
exports.useIsomorphicEffectOnce = useIsomorphicEffectOnce;
|
|
30
|
-
//# sourceMappingURL=useIsomorphicEffectOnce.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useIsomorphicEffectOnce.cjs","sources":["../../src/useIsomorphicEffectOnce.ts"],"sourcesContent":["import { useRef, useState } from 'rehackt'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { EffectCallback } from 'rehackt'\n\n/**\n * This hook handles StrictMode and prod mode\n */\nexport const useIsomorphicEffectOnce = (effect: EffectCallback) => {\n const destroyFunc = useRef<void | (() => void)>()\n const effectCalled = useRef(false)\n const renderAfterCalled = useRef(false)\n const [val, setVal] = useState(0)\n\n if (effectCalled.current) {\n renderAfterCalled.current = true\n }\n\n useIsomorphicLayoutEffect(() => {\n // only execute the effect first time around\n if (!effectCalled.current) {\n destroyFunc.current = effect()\n effectCalled.current = true\n }\n\n // this forces one render after the effect is run\n setVal((v) => v + 1)\n\n return () => {\n // if the comp didn't render since the useEffect was called,\n // we know it's the dummy React cycle\n if (!renderAfterCalled.current) {\n return\n }\n if (destroyFunc.current) {\n destroyFunc.current()\n }\n }\n }, [])\n}\n"],"names":["useRef","useState","useIsomorphicLayoutEffect"],"mappings":";;;;AAOa,MAAA,0BAA0B,CAAC,WAA2B;AACjE,QAAM,cAAcA,QAAAA;AACd,QAAA,eAAeA,eAAO,KAAK;AAC3B,QAAA,oBAAoBA,eAAO,KAAK;AACtC,QAAM,CAAC,KAAK,MAAM,IAAIC,iBAAS,CAAC;AAEhC,MAAI,aAAa,SAAS;AACxB,sBAAkB,UAAU;AAAA,EAC9B;AAEAC,4BAAAA,0BAA0B,MAAM;AAE1B,QAAA,CAAC,aAAa,SAAS;AACzB,kBAAY,UAAU;AACtB,mBAAa,UAAU;AAAA,IACzB;AAGO,WAAA,CAAC,MAAM,IAAI,CAAC;AAEnB,WAAO,MAAM;AAGP,UAAA,CAAC,kBAAkB,SAAS;AAC9B;AAAA,MACF;AACA,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ;AAAA,MACtB;AAAA,IAAA;AAAA,EAEJ,GAAG,CAAE,CAAA;AACP;;"}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { useRef, useState } from "rehackt";
|
|
2
|
-
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
|
|
3
|
-
const useIsomorphicEffectOnce = (effect) => {
|
|
4
|
-
const destroyFunc = useRef();
|
|
5
|
-
const effectCalled = useRef(false);
|
|
6
|
-
const renderAfterCalled = useRef(false);
|
|
7
|
-
const [val, setVal] = useState(0);
|
|
8
|
-
if (effectCalled.current) {
|
|
9
|
-
renderAfterCalled.current = true;
|
|
10
|
-
}
|
|
11
|
-
useIsomorphicLayoutEffect(() => {
|
|
12
|
-
if (!effectCalled.current) {
|
|
13
|
-
destroyFunc.current = effect();
|
|
14
|
-
effectCalled.current = true;
|
|
15
|
-
}
|
|
16
|
-
setVal((v) => v + 1);
|
|
17
|
-
return () => {
|
|
18
|
-
if (!renderAfterCalled.current) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
if (destroyFunc.current) {
|
|
22
|
-
destroyFunc.current();
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
}, []);
|
|
26
|
-
};
|
|
27
|
-
export {
|
|
28
|
-
useIsomorphicEffectOnce
|
|
29
|
-
};
|
|
30
|
-
//# sourceMappingURL=useIsomorphicEffectOnce.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useIsomorphicEffectOnce.js","sources":["../../src/useIsomorphicEffectOnce.ts"],"sourcesContent":["import { useRef, useState } from 'rehackt'\nimport { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'\nimport type { EffectCallback } from 'rehackt'\n\n/**\n * This hook handles StrictMode and prod mode\n */\nexport const useIsomorphicEffectOnce = (effect: EffectCallback) => {\n const destroyFunc = useRef<void | (() => void)>()\n const effectCalled = useRef(false)\n const renderAfterCalled = useRef(false)\n const [val, setVal] = useState(0)\n\n if (effectCalled.current) {\n renderAfterCalled.current = true\n }\n\n useIsomorphicLayoutEffect(() => {\n // only execute the effect first time around\n if (!effectCalled.current) {\n destroyFunc.current = effect()\n effectCalled.current = true\n }\n\n // this forces one render after the effect is run\n setVal((v) => v + 1)\n\n return () => {\n // if the comp didn't render since the useEffect was called,\n // we know it's the dummy React cycle\n if (!renderAfterCalled.current) {\n return\n }\n if (destroyFunc.current) {\n destroyFunc.current()\n }\n }\n }, [])\n}\n"],"names":[],"mappings":";;AAOa,MAAA,0BAA0B,CAAC,WAA2B;AACjE,QAAM,cAAc;AACd,QAAA,eAAe,OAAO,KAAK;AAC3B,QAAA,oBAAoB,OAAO,KAAK;AACtC,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,CAAC;AAEhC,MAAI,aAAa,SAAS;AACxB,sBAAkB,UAAU;AAAA,EAC9B;AAEA,4BAA0B,MAAM;AAE1B,QAAA,CAAC,aAAa,SAAS;AACzB,kBAAY,UAAU;AACtB,mBAAa,UAAU;AAAA,IACzB;AAGO,WAAA,CAAC,MAAM,IAAI,CAAC;AAEnB,WAAO,MAAM;AAGP,UAAA,CAAC,kBAAkB,SAAS;AAC9B;AAAA,MACF;AACA,UAAI,YAAY,SAAS;AACvB,oBAAY,QAAQ;AAAA,MACtB;AAAA,IAAA;AAAA,EAEJ,GAAG,CAAE,CAAA;AACP;"}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useRef, useState } from 'rehackt'
|
|
2
|
-
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'
|
|
3
|
-
import type { EffectCallback } from 'rehackt'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* This hook handles StrictMode and prod mode
|
|
7
|
-
*/
|
|
8
|
-
export const useIsomorphicEffectOnce = (effect: EffectCallback) => {
|
|
9
|
-
const destroyFunc = useRef<void | (() => void)>()
|
|
10
|
-
const effectCalled = useRef(false)
|
|
11
|
-
const renderAfterCalled = useRef(false)
|
|
12
|
-
const [val, setVal] = useState(0)
|
|
13
|
-
|
|
14
|
-
if (effectCalled.current) {
|
|
15
|
-
renderAfterCalled.current = true
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
useIsomorphicLayoutEffect(() => {
|
|
19
|
-
// only execute the effect first time around
|
|
20
|
-
if (!effectCalled.current) {
|
|
21
|
-
destroyFunc.current = effect()
|
|
22
|
-
effectCalled.current = true
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// this forces one render after the effect is run
|
|
26
|
-
setVal((v) => v + 1)
|
|
27
|
-
|
|
28
|
-
return () => {
|
|
29
|
-
// if the comp didn't render since the useEffect was called,
|
|
30
|
-
// we know it's the dummy React cycle
|
|
31
|
-
if (!renderAfterCalled.current) {
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
if (destroyFunc.current) {
|
|
35
|
-
destroyFunc.current()
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}, [])
|
|
39
|
-
}
|