From 56438b5909c73496ad7a13418aba75b70b8c2430 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sat, 6 Jul 2024 19:00:01 -0700 Subject: [PATCH 1/6] chore: WIP --- examples/react/simple/src/index.tsx | 121 ++++++++++++++-------------- packages/react-form/src/index.ts | 2 +- packages/react-form/src/useForm.tsx | 2 +- 3 files changed, 61 insertions(+), 64 deletions(-) diff --git a/examples/react/simple/src/index.tsx b/examples/react/simple/src/index.tsx index 8e57f3352..7e8b981d5 100644 --- a/examples/react/simple/src/index.tsx +++ b/examples/react/simple/src/index.tsx @@ -1,16 +1,58 @@ -import type { FieldApi } from '@tanstack/react-form' -import { useForm } from '@tanstack/react-form' import * as React from 'react' import { createRoot } from 'react-dom/client' +import { useForm } from '@tanstack/react-form' +import type { DeepKeys, DeepValue, ReactFormApi } from '@tanstack/react-form' + +// Our types (to move into `core`) +type SelfKeys = { + [K in keyof T]: K +}[keyof T] + +// Utility type to narrow allowed TName values to only specific types +// IE: DeepKeyValueName<{ foo: string; bar: number }, string> = 'foo' +type DeepKeyValueName = SelfKeys<{ + [K in DeepKeys as DeepValue extends TField + ? K + : never]: K +}> + +export type Pretty = { + [K in keyof T]: T[K] +} & {} + +// The rest of the app +type InputFieldProps< + TFormData extends object, + TName extends DeepKeyValueName, +> = { + form: ReactFormApi + name: TName + // Your custom props + size?: 'small' | 'large' +} -function FieldInfo({ field }: { field: FieldApi }) { +function TextInputField< + TFormData extends object, + TName extends DeepKeyValueName, +>({ form, name }: InputFieldProps) { return ( - <> - {field.state.meta.isTouched && field.state.meta.errors.length ? ( - {field.state.meta.errors.join(',')} - ) : null} - {field.state.meta.isValidating ? 'Validating...' : null} - + { + return ( + <> + + field.handleChange(e.target.value)} + /> + + ) + }} + /> ) } @@ -18,7 +60,9 @@ export default function App() { const form = useForm({ defaultValues: { firstName: '', - lastName: '', + age: 0, + 0: '', + foo: [] as Array<{ bar: string; baz: number }>, }, onSubmit: async ({ value }) => { // Do something with form data @@ -28,7 +72,7 @@ export default function App() { return (
-

Simple Form Example

+

Wrapped Fields Form Example

{ e.preventDefault() @@ -37,59 +81,12 @@ export default function App() { }} >
- {/* A type-safe field component*/} - - !value - ? 'A first name is required' - : value.length < 3 - ? 'First name must be at least 3 characters' - : undefined, - onChangeAsyncDebounceMs: 500, - onChangeAsync: async ({ value }) => { - await new Promise((resolve) => setTimeout(resolve, 1000)) - return ( - value.includes('error') && 'No "error" allowed in first name' - ) - }, - }} - children={(field) => { - // Avoid hasty abstractions. Render props are great! - return ( - <> - - field.handleChange(e.target.value)} - /> - - - ) - }} - /> + {/* A type-safe, wrapped field component*/} +
- ( - <> - - field.handleChange(e.target.value)} - /> - - - )} - /> + {/* Correctly throws a warning when the wrong data type is passed */} +
[state.canSubmit, state.isSubmitting]} diff --git a/packages/react-form/src/index.ts b/packages/react-form/src/index.ts index e35e2c602..80234f658 100644 --- a/packages/react-form/src/index.ts +++ b/packages/react-form/src/index.ts @@ -1,6 +1,6 @@ export * from '@tanstack/form-core' -export { useForm } from './useForm' +export { useForm, type ReactFormApi } from './useForm' export type { UseField, FieldComponent } from './useField' export { useField, Field } from './useField' diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 173a505ad..cab1f19cc 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -10,7 +10,7 @@ import type { NodeType } from './types' /** * Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm` */ -interface ReactFormApi< +export interface ReactFormApi< TFormData, TFormValidator extends Validator | undefined = undefined, > { From bd31494f8ce2a63a7e988f2a21fc934689d5eb61 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sat, 6 Jul 2024 20:05:47 -0700 Subject: [PATCH 2/6] chore: getting closer... --- examples/react/simple/src/index.tsx | 39 ++++++++++++++++++---------- packages/form-core/src/util-types.ts | 3 +-- packages/react-form/src/useField.tsx | 21 +-------------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/examples/react/simple/src/index.tsx b/examples/react/simple/src/index.tsx index 7e8b981d5..f9eebac90 100644 --- a/examples/react/simple/src/index.tsx +++ b/examples/react/simple/src/index.tsx @@ -1,7 +1,12 @@ import * as React from 'react' import { createRoot } from 'react-dom/client' import { useForm } from '@tanstack/react-form' -import type { DeepKeys, DeepValue, ReactFormApi } from '@tanstack/react-form' +import type { + DeepKeys, + DeepValue, + FieldOptions, + ReactFormApi, +} from '@tanstack/react-form' // Our types (to move into `core`) type SelfKeys = { @@ -16,27 +21,23 @@ type DeepKeyValueName = SelfKeys<{ : never]: K }> -export type Pretty = { - [K in keyof T]: T[K] -} & {} - // The rest of the app -type InputFieldProps< - TFormData extends object, +interface TextInputFieldProps< + TFormData extends unknown, TName extends DeepKeyValueName, -> = { +> extends FieldOptions { form: ReactFormApi - name: TName // Your custom props size?: 'small' | 'large' } function TextInputField< - TFormData extends object, + TFormData extends unknown, TName extends DeepKeyValueName, ->({ form, name }: InputFieldProps) { +>({ form, name, size, ...fieldProps }: TextInputFieldProps) { return ( - + {...fieldProps} name={name} children={(field) => { return ( @@ -61,7 +62,6 @@ export default function App() { defaultValues: { firstName: '', age: 0, - 0: '', foo: [] as Array<{ bar: string; baz: number }>, }, onSubmit: async ({ value }) => { @@ -82,7 +82,18 @@ export default function App() { >
{/* A type-safe, wrapped field component*/} - + { + if (value.length < 3) { + return 'Name must be at least 3 characters long' + } + return undefined + }, + }} + />
{/* Correctly throws a warning when the wrong data type is passed */} diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index 8a9a107cd..220dff685 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -111,7 +111,6 @@ export type DeepValue< TValue, // A string representing the path of the property we're trying to access TAccessor, - TNullable extends boolean = IsNullable, > = // If TValue is any it will recurse forever, this terminates the recursion unknown extends TValue @@ -138,7 +137,7 @@ export type DeepValue< : TAccessor extends `${infer TBefore}.${infer TAfter}` ? DeepValue, TAfter> : TAccessor extends string - ? TNullable extends true + ? IsNullable extends true ? Nullable : TValue[TAccessor] : never diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 9bb8c77a1..25401f86e 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -5,18 +5,6 @@ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' import type { NodeType, UseFieldOptions } from './types' import type { DeepKeys, DeepValue, Validator } from '@tanstack/form-core' -interface ReactFieldApi< - TParentData, - TFormValidator extends - | Validator - | undefined = undefined, -> { - /** - * A pre-bound and type-safe sub-field component using this field as a root. - */ - Field: FieldComponent -} - /** * A type representing a hook for using a field in a form with the given form data type. * @@ -66,18 +54,11 @@ export function useField< >, ) { const [fieldApi] = useState(() => { - const api = new FieldApi({ + return new FieldApi({ ...opts, form: opts.form, name: opts.name, }) - - const extendedApi: typeof api & ReactFieldApi = - api as never - - extendedApi.Field = Field as never - - return extendedApi }) useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]) From 673d1a17b5ec85d1a3909c8f0704abb8880a55dc Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sat, 6 Jul 2024 23:14:05 -0700 Subject: [PATCH 3/6] chore: move working demo to custom-component-wrapper --- .../custom-component-wrapper/.eslintrc.cjs | 11 ++ .../react/custom-component-wrapper/.gitignore | 27 ++++ .../react/custom-component-wrapper/README.md | 6 + .../react/custom-component-wrapper/index.html | 16 +++ .../custom-component-wrapper/package.json | 34 +++++ .../public/emblem-light.svg | 13 ++ .../custom-component-wrapper/src/index.tsx | 126 ++++++++++++++++++ .../custom-component-wrapper/tsconfig.json | 9 ++ examples/react/simple/src/index.tsx | 122 ++++++++--------- 9 files changed, 299 insertions(+), 65 deletions(-) create mode 100644 examples/react/custom-component-wrapper/.eslintrc.cjs create mode 100644 examples/react/custom-component-wrapper/.gitignore create mode 100644 examples/react/custom-component-wrapper/README.md create mode 100644 examples/react/custom-component-wrapper/index.html create mode 100644 examples/react/custom-component-wrapper/package.json create mode 100644 examples/react/custom-component-wrapper/public/emblem-light.svg create mode 100644 examples/react/custom-component-wrapper/src/index.tsx create mode 100644 examples/react/custom-component-wrapper/tsconfig.json diff --git a/examples/react/custom-component-wrapper/.eslintrc.cjs b/examples/react/custom-component-wrapper/.eslintrc.cjs new file mode 100644 index 000000000..35853b617 --- /dev/null +++ b/examples/react/custom-component-wrapper/.eslintrc.cjs @@ -0,0 +1,11 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = { + extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'], + rules: { + 'react/no-children-prop': 'off', + }, +} + +module.exports = config diff --git a/examples/react/custom-component-wrapper/.gitignore b/examples/react/custom-component-wrapper/.gitignore new file mode 100644 index 000000000..4673b022e --- /dev/null +++ b/examples/react/custom-component-wrapper/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +pnpm-lock.yaml +yarn.lock +package-lock.json + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/react/custom-component-wrapper/README.md b/examples/react/custom-component-wrapper/README.md new file mode 100644 index 000000000..1cf889265 --- /dev/null +++ b/examples/react/custom-component-wrapper/README.md @@ -0,0 +1,6 @@ +# Example + +To run this example: + +- `npm install` +- `npm run dev` diff --git a/examples/react/custom-component-wrapper/index.html b/examples/react/custom-component-wrapper/index.html new file mode 100644 index 000000000..5d0e76cd4 --- /dev/null +++ b/examples/react/custom-component-wrapper/index.html @@ -0,0 +1,16 @@ + + + + + + + + + TanStack Form React Simple Example App + + + +
+ + + diff --git a/examples/react/custom-component-wrapper/package.json b/examples/react/custom-component-wrapper/package.json new file mode 100644 index 000000000..91c3c4103 --- /dev/null +++ b/examples/react/custom-component-wrapper/package.json @@ -0,0 +1,34 @@ +{ + "name": "@tanstack/form-example-react-simple", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3001", + "build": "vite build", + "preview": "vite preview", + "test:types": "tsc" + }, + "dependencies": { + "@tanstack/react-form": "^0.25.3", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "vite": "^5.1.4" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/react/custom-component-wrapper/public/emblem-light.svg b/examples/react/custom-component-wrapper/public/emblem-light.svg new file mode 100644 index 000000000..a58e69ad5 --- /dev/null +++ b/examples/react/custom-component-wrapper/public/emblem-light.svg @@ -0,0 +1,13 @@ + + + + emblem-light + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/examples/react/custom-component-wrapper/src/index.tsx b/examples/react/custom-component-wrapper/src/index.tsx new file mode 100644 index 000000000..b702f04b2 --- /dev/null +++ b/examples/react/custom-component-wrapper/src/index.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import { createRoot } from 'react-dom/client' +import { useForm } from '@tanstack/react-form' +import type { + DeepKeys, + DeepValue, + FieldOptions, + ReactFormApi, +} from '@tanstack/react-form' + +// Our types (to move into `core`) +type SelfKeys = { + [K in keyof T]: K +}[keyof T] + +// Utility type to narrow allowed TName values to only specific types +// IE: DeepKeyValueName<{ foo: string; bar: number }, string> = 'foo' +type DeepKeyValueName = SelfKeys<{ + [K in DeepKeys as DeepValue extends TField + ? K + : never]: K +}> + +// The rest of the app +interface TextInputFieldProps< + TFormData extends unknown, + TName extends DeepKeyValueName, +> extends FieldOptions { + form: ReactFormApi + // Your custom props + size?: 'small' | 'large' +} + +function TextInputField< + TFormData extends unknown, + TName extends DeepKeyValueName, +>({ form, name, size, ...fieldProps }: TextInputFieldProps) { + return ( + + {...fieldProps} + name={name} + children={(field) => { + return ( + <> + + field.handleChange(e.target.value)} + /> + + ) + }} + /> + ) +} + +export default function App() { + const form = useForm({ + defaultValues: { + firstName: '', + age: 0, + foo: [] as Array<{ bar: string; baz: number }>, + }, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value) + }, + }) + + return ( +
+

Wrapped Fields Form Example

+ { + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > +
+ {/* A type-safe, wrapped field component*/} + { + if (value.length < 3) { + return 'Name must be at least 3 characters long' + } + return undefined + }, + }} + /> +
+
+ {/* Correctly throws a warning when the wrong data type is passed */} + +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + <> + + + + )} + /> + +
+ ) +} + +const rootElement = document.getElementById('root')! + +createRoot(rootElement).render( + + + , +) diff --git a/examples/react/custom-component-wrapper/tsconfig.json b/examples/react/custom-component-wrapper/tsconfig.json new file mode 100644 index 000000000..666a0ea71 --- /dev/null +++ b/examples/react/custom-component-wrapper/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "jsx": "react", + "noEmit": true, + "strict": true, + "esModuleInterop": true, + "lib": ["DOM", "DOM.Iterable", "ES2020"] + } +} diff --git a/examples/react/simple/src/index.tsx b/examples/react/simple/src/index.tsx index f9eebac90..8e57f3352 100644 --- a/examples/react/simple/src/index.tsx +++ b/examples/react/simple/src/index.tsx @@ -1,59 +1,16 @@ +import type { FieldApi } from '@tanstack/react-form' +import { useForm } from '@tanstack/react-form' import * as React from 'react' import { createRoot } from 'react-dom/client' -import { useForm } from '@tanstack/react-form' -import type { - DeepKeys, - DeepValue, - FieldOptions, - ReactFormApi, -} from '@tanstack/react-form' - -// Our types (to move into `core`) -type SelfKeys = { - [K in keyof T]: K -}[keyof T] - -// Utility type to narrow allowed TName values to only specific types -// IE: DeepKeyValueName<{ foo: string; bar: number }, string> = 'foo' -type DeepKeyValueName = SelfKeys<{ - [K in DeepKeys as DeepValue extends TField - ? K - : never]: K -}> -// The rest of the app -interface TextInputFieldProps< - TFormData extends unknown, - TName extends DeepKeyValueName, -> extends FieldOptions { - form: ReactFormApi - // Your custom props - size?: 'small' | 'large' -} - -function TextInputField< - TFormData extends unknown, - TName extends DeepKeyValueName, ->({ form, name, size, ...fieldProps }: TextInputFieldProps) { +function FieldInfo({ field }: { field: FieldApi }) { return ( - - {...fieldProps} - name={name} - children={(field) => { - return ( - <> - - field.handleChange(e.target.value)} - /> - - ) - }} - /> + <> + {field.state.meta.isTouched && field.state.meta.errors.length ? ( + {field.state.meta.errors.join(',')} + ) : null} + {field.state.meta.isValidating ? 'Validating...' : null} + ) } @@ -61,8 +18,7 @@ export default function App() { const form = useForm({ defaultValues: { firstName: '', - age: 0, - foo: [] as Array<{ bar: string; baz: number }>, + lastName: '', }, onSubmit: async ({ value }) => { // Do something with form data @@ -72,7 +28,7 @@ export default function App() { return (
-

Wrapped Fields Form Example

+

Simple Form Example

{ e.preventDefault() @@ -81,23 +37,59 @@ export default function App() { }} >
- {/* A type-safe, wrapped field component*/} - { - if (value.length < 3) { - return 'Name must be at least 3 characters long' - } - return undefined + onChange: ({ value }) => + !value + ? 'A first name is required' + : value.length < 3 + ? 'First name must be at least 3 characters' + : undefined, + onChangeAsyncDebounceMs: 500, + onChangeAsync: async ({ value }) => { + await new Promise((resolve) => setTimeout(resolve, 1000)) + return ( + value.includes('error') && 'No "error" allowed in first name' + ) }, }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( + <> + + field.handleChange(e.target.value)} + /> + + + ) + }} />
- {/* Correctly throws a warning when the wrong data type is passed */} - + ( + <> + + field.handleChange(e.target.value)} + /> + + + )} + />
[state.canSubmit, state.isSubmitting]} From 3f8000009ad0f9cca4c275a69bc43bf6d1146674 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sat, 6 Jul 2024 23:20:14 -0700 Subject: [PATCH 4/6] chore: move exported types to right location --- .../custom-component-wrapper/package.json | 2 +- .../custom-component-wrapper/src/index.tsx | 26 ++++++----------- packages/form-core/src/util-types.ts | 12 ++++++++ packages/form-core/tests/util-types.test-d.ts | 29 ++++++++++++++++++- pnpm-lock.yaml | 25 ++++++++++++++++ 5 files changed, 75 insertions(+), 19 deletions(-) diff --git a/examples/react/custom-component-wrapper/package.json b/examples/react/custom-component-wrapper/package.json index 91c3c4103..cb0b34ad0 100644 --- a/examples/react/custom-component-wrapper/package.json +++ b/examples/react/custom-component-wrapper/package.json @@ -1,5 +1,5 @@ { - "name": "@tanstack/form-example-react-simple", + "name": "@tanstack/form-example-react-custom-component-wrapper", "private": true, "type": "module", "scripts": { diff --git a/examples/react/custom-component-wrapper/src/index.tsx b/examples/react/custom-component-wrapper/src/index.tsx index b702f04b2..a15607f68 100644 --- a/examples/react/custom-component-wrapper/src/index.tsx +++ b/examples/react/custom-component-wrapper/src/index.tsx @@ -2,26 +2,14 @@ import * as React from 'react' import { createRoot } from 'react-dom/client' import { useForm } from '@tanstack/react-form' import type { - DeepKeys, - DeepValue, + DeepKeyValueName, FieldOptions, ReactFormApi, } from '@tanstack/react-form' -// Our types (to move into `core`) -type SelfKeys = { - [K in keyof T]: K -}[keyof T] - -// Utility type to narrow allowed TName values to only specific types -// IE: DeepKeyValueName<{ foo: string; bar: number }, string> = 'foo' -type DeepKeyValueName = SelfKeys<{ - [K in DeepKeys as DeepValue extends TField - ? K - : never]: K -}> - -// The rest of the app +/** + * Export this to your design system or a dedicated component location + */ interface TextInputFieldProps< TFormData extends unknown, TName extends DeepKeyValueName, @@ -36,6 +24,8 @@ function TextInputField< TName extends DeepKeyValueName, >({ form, name, size, ...fieldProps }: TextInputFieldProps) { return ( + // Manually type-cast form.Field to work around this issue: + // https://twitter.com/crutchcorn/status/1809827621485900049 {...fieldProps} name={name} @@ -57,12 +47,14 @@ function TextInputField< ) } +/** + * Then use it in your application + */ export default function App() { const form = useForm({ defaultValues: { firstName: '', age: 0, - foo: [] as Array<{ bar: string; baz: number }>, }, onSubmit: async ({ value }) => { // Do something with form data diff --git a/packages/form-core/src/util-types.ts b/packages/form-core/src/util-types.ts index 220dff685..e00642173 100644 --- a/packages/form-core/src/util-types.ts +++ b/packages/form-core/src/util-types.ts @@ -143,3 +143,15 @@ export type DeepValue< : never : // Do not allow `TValue` to be anything else never + +type SelfKeys = { + [K in keyof T]: K +}[keyof T] + +// Utility type to narrow allowed TName values to only specific types +// IE: DeepKeyValueName<{ foo: string; bar: number }, string> = 'foo' +export type DeepKeyValueName = SelfKeys<{ + [K in DeepKeys as DeepValue extends TField + ? K + : never]: K +}> diff --git a/packages/form-core/tests/util-types.test-d.ts b/packages/form-core/tests/util-types.test-d.ts index cf692c845..8870709f5 100644 --- a/packages/form-core/tests/util-types.test-d.ts +++ b/packages/form-core/tests/util-types.test-d.ts @@ -1,5 +1,5 @@ import { assertType } from 'vitest' -import type { DeepKeys, DeepValue } from '../src/index' +import { DeepKeys, DeepKeyValueName, DeepValue } from '../src/index' /** * Properly recognizes that `0` is not an object and should not have subkeys @@ -169,3 +169,30 @@ type DoubleDeepArray = DeepValue< > assertType(0 as never as DoubleDeepArray) + +type FooBarOther = { + foo: string + bar: string + other: number +} + +type StringFromFooBar = DeepKeyValueName + +assertType<'foo' | 'bar'>(0 as never as StringFromFooBar) + +type DeepFooBarOther = { + foo: string + bar: string + other: number + one: { + foo: string + bar: string + other: number + } +} + +type StringFromDeepFooBar = DeepKeyValueName + +assertType<'foo' | 'bar' | 'one.foo' | 'one.bar'>( + 0 as never as StringFromDeepFooBar, +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91a7241fb..3b4605b1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -443,6 +443,31 @@ importers: specifier: ^5.1.4 version: 5.2.10(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.32))(terser@5.29.1) + examples/react/custom-component-wrapper: + dependencies: + '@tanstack/react-form': + specifier: ^0.25.3 + version: link:../../../packages/react-form + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.3 + version: 18.3.3 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.0 + '@vitejs/plugin-react': + specifier: ^4.3.0 + version: 4.3.1(vite@5.2.10(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.32))(terser@5.29.1)) + vite: + specifier: ^5.1.4 + version: 5.2.10(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.32))(terser@5.29.1) + examples/react/next-server-actions: dependencies: '@tanstack/react-form': From 0bd9576b04d8cc2cb5484cd215e1c921a7387ce4 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 8 Jul 2024 12:33:03 -0700 Subject: [PATCH 5/6] chore: make usage look nicer --- .../custom-component-wrapper/src/index.tsx | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/examples/react/custom-component-wrapper/src/index.tsx b/examples/react/custom-component-wrapper/src/index.tsx index a15607f68..7bc91d2e4 100644 --- a/examples/react/custom-component-wrapper/src/index.tsx +++ b/examples/react/custom-component-wrapper/src/index.tsx @@ -8,7 +8,7 @@ import type { } from '@tanstack/react-form' /** - * Export this to your design system or a dedicated component location + * Export these components to your design system or a dedicated component location */ interface TextInputFieldProps< TFormData extends unknown, @@ -16,13 +16,13 @@ interface TextInputFieldProps< > extends FieldOptions { form: ReactFormApi // Your custom props - size?: 'small' | 'large' + label: string } function TextInputField< TFormData extends unknown, TName extends DeepKeyValueName, ->({ form, name, size, ...fieldProps }: TextInputFieldProps) { +>({ form, name, label, ...fieldProps }: TextInputFieldProps) { return ( // Manually type-cast form.Field to work around this issue: // https://twitter.com/crutchcorn/status/1809827621485900049 @@ -31,8 +31,10 @@ function TextInputField< name={name} children={(field) => { return ( - <> - +
+
+ +
field.handleChange(e.target.value)} /> - + {field.state.meta.isValidating ? ( +
Validating...
+ ) : null} + {field.state.meta.isTouched && field.state.meta.errors.length ? ( +
+ {field.state.meta.errors.join(', ')} +
+ ) : null} +
) }} /> ) } +function SubmitButton({ form }: { form: ReactFormApi }) { + return ( + [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + <> + + + )} + /> + ) +} + /** * Then use it in your application */ @@ -72,38 +97,27 @@ export default function App() { form.handleSubmit() }} > -
- {/* A type-safe, wrapped field component*/} - { - if (value.length < 3) { - return 'Name must be at least 3 characters long' - } - return undefined - }, - }} - /> -
-
- {/* Correctly throws a warning when the wrong data type is passed */} - -
- [state.canSubmit, state.isSubmitting]} - children={([canSubmit, isSubmitting]) => ( - <> - - - - )} + {/* A type-safe, wrapped field component*/} + { + await new Promise((resolve) => setTimeout(resolve, 1000)) + if (value.length < 3) { + return 'Name must be at least 3 characters long' + } + return undefined + }, + }} /> + {/* Correctly throws a warning when the wrong data type is passed */} + + +
) From bb26093c633b7499f995b7a0882aca1ca51141b0 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Tue, 9 Jul 2024 23:39:58 +1000 Subject: [PATCH 6/6] Fix pnpm-lock --- examples/react/custom-component-wrapper/package.json | 6 +++--- pnpm-lock.yaml | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/react/custom-component-wrapper/package.json b/examples/react/custom-component-wrapper/package.json index cb0b34ad0..16d4e22ff 100644 --- a/examples/react/custom-component-wrapper/package.json +++ b/examples/react/custom-component-wrapper/package.json @@ -9,15 +9,15 @@ "test:types": "tsc" }, "dependencies": { - "@tanstack/react-form": "^0.25.3", + "@tanstack/react-form": "^0.26.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.0", - "vite": "^5.1.4" + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.3.3" }, "browserslist": { "production": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1429fb8c9..5ded8f2b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -446,7 +446,7 @@ importers: examples/react/custom-component-wrapper: dependencies: '@tanstack/react-form': - specifier: ^0.25.3 + specifier: ^0.26.1 version: link:../../../packages/react-form react: specifier: ^18.3.1 @@ -462,11 +462,11 @@ importers: specifier: ^18.3.0 version: 18.3.0 '@vitejs/plugin-react': - specifier: ^4.3.0 - version: 4.3.1(vite@5.2.10(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.32))(terser@5.29.1)) + specifier: ^4.3.1 + version: 4.3.1(vite@5.3.3(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.39))(terser@5.29.1)) vite: - specifier: ^5.1.4 - version: 5.2.10(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.32))(terser@5.29.1) + specifier: ^5.3.3 + version: 5.3.3(@types/node@20.10.6)(less@4.2.0)(sass@1.72.0)(sugarss@4.0.1(postcss@8.4.39))(terser@5.29.1) examples/react/next-server-actions: dependencies: