Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Shared Component Library Docs #825

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions examples/react/custom-component-wrapper/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions examples/react/custom-component-wrapper/.gitignore
Original file line number Diff line number Diff line change
@@ -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*
6 changes: 6 additions & 0 deletions examples/react/custom-component-wrapper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Example

To run this example:

- `npm install`
- `npm run dev`
16 changes: 16 additions & 0 deletions examples/react/custom-component-wrapper/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/emblem-light.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

<title>TanStack Form React Simple Example App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions examples/react/custom-component-wrapper/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@tanstack/form-example-react-custom-component-wrapper",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=3001",
"build": "vite build",
"preview": "vite preview",
"test:types": "tsc"
},
"dependencies": {
"@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.1",
"vite": "^5.3.3"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
13 changes: 13 additions & 0 deletions examples/react/custom-component-wrapper/public/emblem-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
132 changes: 132 additions & 0 deletions examples/react/custom-component-wrapper/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { useForm } from '@tanstack/react-form'
import type {
DeepKeyValueName,
FieldOptions,
ReactFormApi,
} from '@tanstack/react-form'

/**
* Export these components to your design system or a dedicated component location
*/
interface TextInputFieldProps<
TFormData extends unknown,
TName extends DeepKeyValueName<TFormData, string>,
> extends FieldOptions<TFormData, TName> {
form: ReactFormApi<TFormData, any>
// Your custom props
label: string
}

function TextInputField<
TFormData extends unknown,
TName extends DeepKeyValueName<TFormData, string>,
>({ form, name, label, ...fieldProps }: TextInputFieldProps<TFormData, TName>) {
return (
// Manually type-cast form.Field to work around this issue:
// https://twitter.com/crutchcorn/status/1809827621485900049
<form.Field<TName, any, any>
{...fieldProps}
name={name}
children={(field) => {
return (
<div style={{ marginBottom: '1rem' }}>
<div>
<label htmlFor={field.name}>{label}</label>
</div>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isValidating ? (
<div style={{ color: 'gray' }}>Validating...</div>
) : null}
{field.state.meta.isTouched && field.state.meta.errors.length ? (
<div style={{ color: 'red' }}>
{field.state.meta.errors.join(', ')}
</div>
) : null}
</div>
)
}}
/>
)
}

function SubmitButton({ form }: { form: ReactFormApi<any, any> }) {
return (
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<>
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
</>
)}
/>
)
}

/**
* Then use it in your application
*/
export default function App() {
const form = useForm({
defaultValues: {
firstName: '',
age: 0,
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})

return (
<div>
<h1>Wrapped Fields Form Example</h1>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
{/* A type-safe, wrapped field component*/}
<TextInputField
label={'First name:'}
form={form}
name="firstName"
validators={{
onChangeAsync: async ({ value }) => {
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 */}
<TextInputField label={'Age:'} form={form} name="age" />
<SubmitButton form={form} />
<button type="reset" onClick={() => form.reset()}>
Reset
</button>
</form>
</div>
)
}

const rootElement = document.getElementById('root')!

createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
9 changes: 9 additions & 0 deletions examples/react/custom-component-wrapper/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"jsx": "react",
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"lib": ["DOM", "DOM.Iterable", "ES2020"]
}
}
15 changes: 13 additions & 2 deletions packages/form-core/src/util-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TValue>,
> =
// If TValue is any it will recurse forever, this terminates the recursion
unknown extends TValue
Expand All @@ -138,9 +137,21 @@ export type DeepValue<
: TAccessor extends `${infer TBefore}.${infer TAfter}`
? DeepValue<DeepValue<TValue, TBefore>, TAfter>
: TAccessor extends string
? TNullable extends true
? IsNullable<TValue> extends true
? Nullable<TValue[TAccessor]>
: TValue[TAccessor]
: never
: // Do not allow `TValue` to be anything else
never

type SelfKeys<T> = {
[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<TFormData, TField> = SelfKeys<{
[K in DeepKeys<TFormData> as DeepValue<TFormData, K> extends TField
? K
: never]: K
}>
29 changes: 28 additions & 1 deletion packages/form-core/tests/util-types.test-d.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -169,3 +169,30 @@ type DoubleDeepArray = DeepValue<
>

assertType<string>(0 as never as DoubleDeepArray)

type FooBarOther = {
foo: string
bar: string
other: number
}

type StringFromFooBar = DeepKeyValueName<FooBarOther, string>

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<DeepFooBarOther, string>

assertType<'foo' | 'bar' | 'one.foo' | 'one.bar'>(
0 as never as StringFromDeepFooBar,
)
2 changes: 1 addition & 1 deletion packages/react-form/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
21 changes: 1 addition & 20 deletions packages/react-form/src/useField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TParentData, unknown>
| undefined = undefined,
> {
/**
* A pre-bound and type-safe sub-field component using this field as a root.
*/
Field: FieldComponent<TParentData, TFormValidator>
}

/**
* A type representing a hook for using a field in a form with the given form data type.
*
Expand Down Expand Up @@ -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<TParentData, TFormValidator> =
api as never

extendedApi.Field = Field as never

return extendedApi
})

useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi])
Expand Down
2 changes: 1 addition & 1 deletion packages/react-form/src/useForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TFormData, unknown> | undefined = undefined,
> {
Expand Down
Loading
Loading