mirror of
https://github.com/LukeHagar/form.git
synced 2025-12-06 04:19:43 +00:00
feat: solid.js form (#471)
* solid commit -a * fix failing tests and formatting * comments + removed unneeded computed * updated changes * prettierd * chore: add Solid Form to script to be deployed * fix: fix typing of solid's Subscribe data * chore: remove errant createEffect * chore: rename Solid's useForm and useField to createForm and createField * chore: remove old mention of React's memoization * chore: add Solid simple example * chore: add Solid yup example * chore: add Zod Solid example * docs: add initial docs for Solid package --------- Co-authored-by: Corbin Crutchley <git@crutchcorn.dev>
This commit is contained in:
@@ -157,6 +157,58 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"framework": "solid",
|
||||||
|
"menuItems": [
|
||||||
|
{
|
||||||
|
"label": "Getting Started",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "Quick Start",
|
||||||
|
"to": "framework/solid/quick-start"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "API Reference",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "useForm",
|
||||||
|
"to": "framework/solid/reference/createForm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "useField",
|
||||||
|
"to": "framework/solid/reference/createField"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Field",
|
||||||
|
"to": "framework/solid/reference/Field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "FormApi",
|
||||||
|
"to": "framework/solid/reference/formApi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Examples",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "Simple",
|
||||||
|
"to": "framework/solid/examples/simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Yup",
|
||||||
|
"to": "framework/solid/examples/yup"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Zod",
|
||||||
|
"to": "framework/solid/examples/zod"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { useForm } from '@tanstack/react-form'
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
// Memoize your default values to prevent re-renders
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fullName: '',
|
fullName: '',
|
||||||
},
|
},
|
||||||
|
|||||||
54
docs/framework/solid/quick-start.md
Normal file
54
docs/framework/solid/quick-start.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
id: quick-start
|
||||||
|
title: Quick Start
|
||||||
|
---
|
||||||
|
|
||||||
|
The bare minimum to get started with TanStack Form is to create a form and add a field. Keep in mind that this example does not include any validation or error handling... yet.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { createForm } from '@tanstack/solid-form'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const form = createForm(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
fullName: '',
|
||||||
|
},
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
// Do something with form data
|
||||||
|
console.log(values)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Simple Form Example</h1>
|
||||||
|
<form.Provider>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
void form.handleSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<form.Field
|
||||||
|
name="fullName"
|
||||||
|
children={(field) => (
|
||||||
|
<input
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</form.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From here, you'll be ready to explore all of the other features of TanStack Form!
|
||||||
6
docs/framework/solid/reference/Field.md
Normal file
6
docs/framework/solid/reference/Field.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
id: field
|
||||||
|
title: Field
|
||||||
|
---
|
||||||
|
|
||||||
|
Please see [/packages/solid-form/src/createField.tsx](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createField.tsx)
|
||||||
6
docs/framework/solid/reference/createField.md
Normal file
6
docs/framework/solid/reference/createField.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
id: createField
|
||||||
|
title: createField
|
||||||
|
---
|
||||||
|
|
||||||
|
Please see [/packages/solid-form/src/createField.tsx](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createField.tsx)
|
||||||
6
docs/framework/solid/reference/createForm.md
Normal file
6
docs/framework/solid/reference/createForm.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
id: createForm
|
||||||
|
title: createForm
|
||||||
|
---
|
||||||
|
|
||||||
|
Please see [/packages/solid-form/src/createForm.tsx](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createForm.tsx)
|
||||||
6
docs/framework/solid/reference/createFormFactory.md
Normal file
6
docs/framework/solid/reference/createFormFactory.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
id: createFormFactory
|
||||||
|
title: createFormFactory
|
||||||
|
---
|
||||||
|
|
||||||
|
Please see [/packages/solid-form/src/createFormFactory.ts](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createFormFactory.ts)
|
||||||
6
docs/framework/solid/reference/fieldApi.md
Normal file
6
docs/framework/solid/reference/fieldApi.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
id: fieldApi
|
||||||
|
title: Field API
|
||||||
|
---
|
||||||
|
|
||||||
|
Please see [/packages/solid-form/src/createField.tsx](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createField.tsx)
|
||||||
6
docs/framework/solid/reference/formApi.md
Normal file
6
docs/framework/solid/reference/formApi.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
id: formApi
|
||||||
|
title: Form API
|
||||||
|
---
|
||||||
|
|
||||||
|
Please see [/packages/solid-form/src/createForm.tsx](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createForm.tsx)
|
||||||
@@ -11,7 +11,6 @@ The bare minimum to get started with TanStack Form is to create a form and add a
|
|||||||
import { useForm } from '@tanstack/vue-form'
|
import { useForm } from '@tanstack/vue-form'
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
// Memoize your default values to prevent re-renders
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fullName: '',
|
fullName: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ id: installation
|
|||||||
title: Installation
|
title: Installation
|
||||||
---
|
---
|
||||||
|
|
||||||
TanStack Form is compatible with various front-end frameworks, including React and Vue. To use TanStack Form with your desired framework, install the corresponding adapter via NPM:
|
TanStack Form is compatible with various front-end frameworks, including React, Vue, and Solid. To use TanStack Form with your desired framework, install the corresponding adapter via NPM:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm i @tanstack/react-form
|
$ npm i @tanstack/react-form
|
||||||
# or
|
# or
|
||||||
$ pnpm add @tanstack/vue-form
|
$ pnpm add @tanstack/vue-form
|
||||||
|
# or
|
||||||
|
$ yarn add @tanstack/solid-form
|
||||||
```
|
```
|
||||||
|
|
||||||
> Depending on your environment, you might need to add polyfills. If you want to support older browsers, you need to transpile the library from `node_modules` yourselves.
|
> Depending on your environment, you might need to add polyfills. If you want to support older browsers, you need to transpile the library from `node_modules` yourselves.
|
||||||
@@ -16,7 +18,7 @@ $ pnpm add @tanstack/vue-form
|
|||||||
In addition, we support both Zod and Yup as validators through official validator packages:
|
In addition, we support both Zod and Yup as validators through official validator packages:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ yarn add @tanstack/zod-form-adapter zod
|
$ npm i @tanstack/zod-form-adapter zod
|
||||||
# or
|
# or
|
||||||
$ npm i @tanstack/yup-form-adapter yup
|
$ npm i @tanstack/yup-form-adapter yup
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Simple Form Example</h1>
|
<h1>Simple Form Example</h1>
|
||||||
{/* A pre-bound form component */}
|
|
||||||
<form.Provider>
|
<form.Provider>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -67,7 +66,7 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* A type-safe and pre-bound field component*/}
|
{/* A type-safe field component*/}
|
||||||
<form.Field
|
<form.Field
|
||||||
name="firstName"
|
name="firstName"
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ function FieldInfo({ field }: { field: FieldApi<any, any, unknown, unknown> }) {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
// Memoize your default values to prevent re-renders
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@@ -30,7 +29,6 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Simple Form Example</h1>
|
<h1>Simple Form Example</h1>
|
||||||
{/* A pre-bound form component */}
|
|
||||||
<form.Provider>
|
<form.Provider>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -40,7 +38,7 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* A type-safe and pre-bound field component*/}
|
{/* A type-safe field component*/}
|
||||||
<form.Field
|
<form.Field
|
||||||
name="firstName"
|
name="firstName"
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ function FieldInfo({ field }: { field: FieldApi<any, any, unknown, unknown> }) {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
// Memoize your default values to prevent re-renders
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@@ -33,8 +32,7 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Simple Form Example</h1>
|
<h1>Yup Form Example</h1>
|
||||||
{/* A pre-bound form component */}
|
|
||||||
<form.Provider>
|
<form.Provider>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -44,7 +42,7 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* A type-safe and pre-bound field component*/}
|
{/* A type-safe field component*/}
|
||||||
<form.Field
|
<form.Field
|
||||||
name="firstName"
|
name="firstName"
|
||||||
onChange={yup
|
onChange={yup
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ function FieldInfo({ field }: { field: FieldApi<any, any, unknown, unknown> }) {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
// Memoize your default values to prevent re-renders
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@@ -33,8 +32,7 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Simple Form Example</h1>
|
<h1>Zod Form Example</h1>
|
||||||
{/* A pre-bound form component */}
|
|
||||||
<form.Provider>
|
<form.Provider>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -44,7 +42,7 @@ export default function App() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{/* A type-safe and pre-bound field component*/}
|
{/* A type-safe field component*/}
|
||||||
<form.Field
|
<form.Field
|
||||||
name="firstName"
|
name="firstName"
|
||||||
onChange={z
|
onChange={z
|
||||||
|
|||||||
24
examples/solid/simple/.gitignore
vendored
Normal file
24
examples/solid/simple/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
28
examples/solid/simple/README.md
Normal file
28
examples/solid/simple/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm run dev`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)
|
||||||
12
examples/solid/simple/index.html
Normal file
12
examples/solid/simple/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>TanStack Form Solid Simple Example App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
examples/solid/simple/package.json
Normal file
37
examples/solid/simple/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@tanstack/form-example-solid-simple",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"test:types": "tsc --noEmit",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/form-core": "0.4.1",
|
||||||
|
"@tanstack/solid-form": "0.4.1",
|
||||||
|
"solid-js": "^1.7.8",
|
||||||
|
"@tanstack/react-form": "0.4.1",
|
||||||
|
"@tanstack/zod-form-adapter": "0.4.1",
|
||||||
|
"@tanstack/yup-form-adapter": "0.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5",
|
||||||
|
"vite-plugin-solid": "^2.7.0"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"implicitDependencies": [
|
||||||
|
"@tanstack/form-core",
|
||||||
|
"@tanstack/solid-form"
|
||||||
|
],
|
||||||
|
"targets": {
|
||||||
|
"test:types": {
|
||||||
|
"dependsOn": [
|
||||||
|
"build"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
examples/solid/simple/src/index.tsx
Normal file
117
examples/solid/simple/src/index.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from 'solid-js/web'
|
||||||
|
|
||||||
|
import { createForm, type FieldApi } from '@tanstack/solid-form'
|
||||||
|
|
||||||
|
interface FieldInfoProps {
|
||||||
|
field: FieldApi<any, any, unknown, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldInfo(props: FieldInfoProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.field.state.meta.touchedErrors ? (
|
||||||
|
<em>{props.field.state.meta.touchedErrors}</em>
|
||||||
|
) : null}
|
||||||
|
{props.field.state.meta.isValidating ? 'Validating...' : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const form = createForm(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
},
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
// Do something with form data
|
||||||
|
console.log(values)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Simple Form Example</h1>
|
||||||
|
<form.Provider>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
void form.handleSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* A type-safe field component*/}
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<label for={field().name}>First Name:</label>
|
||||||
|
<input
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FieldInfo field={field()} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form.Field
|
||||||
|
name="lastName"
|
||||||
|
children={(field) => (
|
||||||
|
<>
|
||||||
|
<label for={field().name}>Last Name:</label>
|
||||||
|
<input
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FieldInfo field={field()} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
canSubmit: state.canSubmit,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
})}
|
||||||
|
children={(state) => {
|
||||||
|
return (
|
||||||
|
<button type="submit" disabled={!state().canSubmit}>
|
||||||
|
{state().isSubmitting ? '...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</form.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.getElementById('root')
|
||||||
|
|
||||||
|
render(() => <App />, root!)
|
||||||
1
examples/solid/simple/src/vite-env.d.ts
vendored
Normal file
1
examples/solid/simple/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
26
examples/solid/simple/tsconfig.json
Normal file
26
examples/solid/simple/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
examples/solid/simple/tsconfig.node.json
Normal file
10
examples/solid/simple/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
6
examples/solid/simple/vite.config.ts
Normal file
6
examples/solid/simple/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import solid from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid()],
|
||||||
|
})
|
||||||
24
examples/solid/yup/.gitignore
vendored
Normal file
24
examples/solid/yup/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
28
examples/solid/yup/README.md
Normal file
28
examples/solid/yup/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm run dev`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)
|
||||||
12
examples/solid/yup/index.html
Normal file
12
examples/solid/yup/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>TanStack Form Solid Yup Example App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
39
examples/solid/yup/package.json
Normal file
39
examples/solid/yup/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@tanstack/form-example-solid-yup",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"test:types": "tsc --noEmit",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/form-core": "0.4.1",
|
||||||
|
"@tanstack/solid-form": "0.4.1",
|
||||||
|
"@tanstack/yup-form-adapter": "0.4.1",
|
||||||
|
"solid-js": "^1.7.8",
|
||||||
|
"yup": "^1.3.2",
|
||||||
|
"@tanstack/react-form": "0.4.1",
|
||||||
|
"@tanstack/zod-form-adapter": "0.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5",
|
||||||
|
"vite-plugin-solid": "^2.7.0"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"implicitDependencies": [
|
||||||
|
"@tanstack/form-core",
|
||||||
|
"@tanstack/solid-form",
|
||||||
|
"@tanstack/yup-form-adapter"
|
||||||
|
],
|
||||||
|
"targets": {
|
||||||
|
"test:types": {
|
||||||
|
"dependsOn": [
|
||||||
|
"build"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
examples/solid/yup/src/index.tsx
Normal file
121
examples/solid/yup/src/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from 'solid-js/web'
|
||||||
|
|
||||||
|
import { createForm, type FieldApi } from '@tanstack/solid-form'
|
||||||
|
import { yupValidator } from '@tanstack/yup-form-adapter'
|
||||||
|
import * as yup from 'yup'
|
||||||
|
|
||||||
|
interface FieldInfoProps {
|
||||||
|
field: FieldApi<any, any, unknown, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldInfo(props: FieldInfoProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.field.state.meta.touchedErrors ? (
|
||||||
|
<em>{props.field.state.meta.touchedErrors}</em>
|
||||||
|
) : null}
|
||||||
|
{props.field.state.meta.isValidating ? 'Validating...' : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const form = createForm(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
},
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
// Do something with form data
|
||||||
|
console.log(values)
|
||||||
|
},
|
||||||
|
// Add a validator to support Yup usage in Form and Field
|
||||||
|
validator: yupValidator,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Yup Form Example</h1>
|
||||||
|
<form.Provider>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
void form.handleSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* A type-safe field component*/}
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
onChange={yup
|
||||||
|
.string()
|
||||||
|
.min(3, 'First name must be at least 3 characters')}
|
||||||
|
onChangeAsyncDebounceMs={500}
|
||||||
|
onChangeAsync={yup
|
||||||
|
.string()
|
||||||
|
.test(
|
||||||
|
'no error',
|
||||||
|
"No 'error' allowed in first name",
|
||||||
|
async (value) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
return !value?.includes('error')
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
children={(field) => {
|
||||||
|
// Avoid hasty abstractions. Render props are great!
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label for={field().name}>First Name:</label>
|
||||||
|
<input
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FieldInfo field={field()} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form.Field
|
||||||
|
name="lastName"
|
||||||
|
children={(field) => (
|
||||||
|
<>
|
||||||
|
<label for={field().name}>Last Name:</label>
|
||||||
|
<input
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FieldInfo field={field()} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
canSubmit: state.canSubmit,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
})}
|
||||||
|
children={(state) => {
|
||||||
|
return (
|
||||||
|
<button type="submit" disabled={!state().canSubmit}>
|
||||||
|
{state().isSubmitting ? '...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</form.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.getElementById('root')
|
||||||
|
|
||||||
|
render(() => <App />, root!)
|
||||||
1
examples/solid/yup/src/vite-env.d.ts
vendored
Normal file
1
examples/solid/yup/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
26
examples/solid/yup/tsconfig.json
Normal file
26
examples/solid/yup/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
examples/solid/yup/tsconfig.node.json
Normal file
10
examples/solid/yup/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
6
examples/solid/yup/vite.config.ts
Normal file
6
examples/solid/yup/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import solid from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid()],
|
||||||
|
})
|
||||||
24
examples/solid/zod/.gitignore
vendored
Normal file
24
examples/solid/zod/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
28
examples/solid/zod/README.md
Normal file
28
examples/solid/zod/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install # or pnpm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs)
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm run dev`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)
|
||||||
12
examples/solid/zod/index.html
Normal file
12
examples/solid/zod/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>TanStack Form Solid Zod Example App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
39
examples/solid/zod/package.json
Normal file
39
examples/solid/zod/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@tanstack/form-example-solid-zod",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"test:types": "tsc --noEmit",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/form-core": "0.4.1",
|
||||||
|
"@tanstack/solid-form": "0.4.1",
|
||||||
|
"@tanstack/zod-form-adapter": "0.4.1",
|
||||||
|
"solid-js": "^1.7.8",
|
||||||
|
"zod": "^3.21.4",
|
||||||
|
"@tanstack/react-form": "0.4.1",
|
||||||
|
"@tanstack/yup-form-adapter": "0.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5",
|
||||||
|
"vite-plugin-solid": "^2.7.0"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"implicitDependencies": [
|
||||||
|
"@tanstack/form-core",
|
||||||
|
"@tanstack/solid-form",
|
||||||
|
"@tanstack/zod-form-adapter"
|
||||||
|
],
|
||||||
|
"targets": {
|
||||||
|
"test:types": {
|
||||||
|
"dependsOn": [
|
||||||
|
"build"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
examples/solid/zod/src/index.tsx
Normal file
120
examples/solid/zod/src/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from 'solid-js/web'
|
||||||
|
|
||||||
|
import { createForm, type FieldApi } from '@tanstack/solid-form'
|
||||||
|
import { zodValidator } from '@tanstack/zod-form-adapter'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
interface FieldInfoProps {
|
||||||
|
field: FieldApi<any, any, unknown, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldInfo(props: FieldInfoProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.field.state.meta.touchedErrors ? (
|
||||||
|
<em>{props.field.state.meta.touchedErrors}</em>
|
||||||
|
) : null}
|
||||||
|
{props.field.state.meta.isValidating ? 'Validating...' : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const form = createForm(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
},
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
// Do something with form data
|
||||||
|
console.log(values)
|
||||||
|
},
|
||||||
|
// Add a validator to support Zod usage in Form and Field
|
||||||
|
validator: zodValidator,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Zod Form Example</h1>
|
||||||
|
<form.Provider>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
void form.handleSubmit()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{/* A type-safe field component*/}
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
onChange={z
|
||||||
|
.string()
|
||||||
|
.min(3, 'First name must be at least 3 characters')}
|
||||||
|
onChangeAsyncDebounceMs={500}
|
||||||
|
onChangeAsync={z.string().refine(
|
||||||
|
async (value) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
return !value.includes('error')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "No 'error' allowed in first name",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
children={(field) => {
|
||||||
|
// Avoid hasty abstractions. Render props are great!
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label for={field().name}>First Name:</label>
|
||||||
|
<input
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FieldInfo field={field()} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form.Field
|
||||||
|
name="lastName"
|
||||||
|
children={(field) => (
|
||||||
|
<>
|
||||||
|
<label for={field().name}>Last Name:</label>
|
||||||
|
<input
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FieldInfo field={field()} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
canSubmit: state.canSubmit,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
})}
|
||||||
|
children={(state) => {
|
||||||
|
return (
|
||||||
|
<button type="submit" disabled={!state().canSubmit}>
|
||||||
|
{state().isSubmitting ? '...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</form.Provider>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.getElementById('root')
|
||||||
|
|
||||||
|
render(() => <App />, root!)
|
||||||
1
examples/solid/zod/src/vite-env.d.ts
vendored
Normal file
1
examples/solid/zod/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
26
examples/solid/zod/tsconfig.json
Normal file
26
examples/solid/zod/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
examples/solid/zod/tsconfig.node.json
Normal file
10
examples/solid/zod/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
6
examples/solid/zod/vite.config.ts
Normal file
6
examples/solid/zod/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import solid from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid()],
|
||||||
|
})
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"@rollup/plugin-commonjs": "^25.0.0",
|
"@rollup/plugin-commonjs": "^25.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
"@rollup/plugin-replace": "^5.0.2",
|
"@rollup/plugin-replace": "^5.0.2",
|
||||||
|
"@solidjs/testing-library": "^0.8.4",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
@@ -115,6 +116,7 @@
|
|||||||
"@tanstack/form-core": "workspace:*",
|
"@tanstack/form-core": "workspace:*",
|
||||||
"@tanstack/react-form": "workspace:*",
|
"@tanstack/react-form": "workspace:*",
|
||||||
"@tanstack/vue-form": "workspace:*",
|
"@tanstack/vue-form": "workspace:*",
|
||||||
|
"@tanstack/solid-form": "workspace:*",
|
||||||
"@tanstack/yup-form-adapter": "workspace:*",
|
"@tanstack/yup-form-adapter": "workspace:*",
|
||||||
"@tanstack/zod-form-adapter": "workspace:*"
|
"@tanstack/zod-form-adapter": "workspace:*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,6 @@ export class FieldApi<
|
|||||||
mount = () => {
|
mount = () => {
|
||||||
const info = this.getInfo()
|
const info = this.getInfo()
|
||||||
info.instances[this.uid] = this as never
|
info.instances[this.uid] = this as never
|
||||||
|
|
||||||
const unsubscribe = this.form.store.subscribe(() => {
|
const unsubscribe = this.form.store.subscribe(() => {
|
||||||
this.store.batch(() => {
|
this.store.batch(() => {
|
||||||
const nextValue = this.getValue()
|
const nextValue = this.getValue()
|
||||||
|
|||||||
11
packages/solid-form/.eslintrc.cjs
Normal file
11
packages/solid-form/.eslintrc.cjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
const config = {
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config
|
||||||
35
packages/solid-form/README.md
Normal file
35
packages/solid-form/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Hooks for managing form state in Solid
|
||||||
|
|
||||||
|
<a href="https://twitter.com/intent/tweet?button_hashtag=TanStack" target="\_parent">
|
||||||
|
<img alt="#TanStack" src="https://img.shields.io/twitter/url?color=%2308a0e9&label=%23TanStack&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Fbutton_hashtag%3DTanStack">
|
||||||
|
</a><a href="https://discord.com/invite/WrRKjPJ" target="\_parent">
|
||||||
|
<img alt="" src="https://img.shields.io/badge/Discord-TanStack-%235865F2" />
|
||||||
|
</a><a href="https://github.com/TanStack/form/actions?query=workflow%3A%22solid-form+tests%22">
|
||||||
|
<img src="https://github.com/TanStack/form/workflows/solid-form%20tests/badge.svg" />
|
||||||
|
</a><a href="https://www.npmjs.com/package/@tanstack/form-core" target="\_parent">
|
||||||
|
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/form-core.svg" />
|
||||||
|
</a><a href="https://bundlephobia.com/package/@tanstack/solid-form@latest" target="\_parent">
|
||||||
|
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/solid-form" />
|
||||||
|
</a><a href="#badge">
|
||||||
|
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||||
|
</a><a href="https://github.com/TanStack/form/discussions">
|
||||||
|
<img alt="Join the discussion on Github" src="https://img.shields.io/badge/Github%20Discussions%20%26%20Support-Chat%20now!-blue" />
|
||||||
|
</a><a href="https://bestofjs.org/projects/tanstack-form"><img alt="Best of JS" src="https://img.shields.io/endpoint?url=https://bestofjs-serverless.now.sh/api/project-badge?fullName=TanStack%form%26since=daily" /></a><a href="https://github.com/TanStack/form/" target="\_parent">
|
||||||
|
<img alt="" src="https://img.shields.io/github/stars/TanStack/form.svg?style=social&label=Star" />
|
||||||
|
</a><a href="https://twitter.com/tannerlinsley" target="\_parent">
|
||||||
|
<img alt="" src="https://img.shields.io/twitter/follow/tannerlinsley.svg?style=social&label=Follow" />
|
||||||
|
</a> <a href="https://gitpod.io/from-referrer/">
|
||||||
|
<img src="https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod" alt="Gitpod Ready-to-Code"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Enjoy this library? Try the entire [TanStack](https://tanstack.com)! [TanStack Table](https://github.com/TanStack/table), [TanStack Router](https://github.com/tanstack/router), [TanStack Virtual](https://github.com/tanstack/virtual), [React Charts](https://github.com/TanStack/react-charts), [React Ranger](https://github.com/TanStack/ranger)
|
||||||
|
|
||||||
|
## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!
|
||||||
|
|
||||||
|
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
|
||||||
|
|
||||||
|
<!-- Use the force, Luke -->
|
||||||
72
packages/solid-form/package.json
Normal file
72
packages/solid-form/package.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "@tanstack/solid-form",
|
||||||
|
"version": "0.4.1",
|
||||||
|
"description": "Powerful, type-safe forms for Solid.",
|
||||||
|
"author": "tannerlinsley",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "tanstack/form",
|
||||||
|
"homepage": "https://tanstack.com/form",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf ./build && rimraf ./coverage",
|
||||||
|
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
||||||
|
"test:types": "tsc --noEmit",
|
||||||
|
"test:lib": "vitest run --coverage",
|
||||||
|
"test:lib:dev": "pnpm run test:lib --watch",
|
||||||
|
"test:build": "publint --strict",
|
||||||
|
"build": "tsup"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"build",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"main": "./build/index.cjs",
|
||||||
|
"module": "./build/index.js",
|
||||||
|
"types": "./build/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
"development": {
|
||||||
|
"import": {
|
||||||
|
"types": "./build/index.d.ts",
|
||||||
|
"default": "./build/dev.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./build/index.d.cts",
|
||||||
|
"default": "./build/dev.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./build/index.d.ts",
|
||||||
|
"default": "./build/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./build/index.d.cts",
|
||||||
|
"default": "./build/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"test:build": {
|
||||||
|
"dependsOn": [
|
||||||
|
"build"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"solid-js": "^1.7.8",
|
||||||
|
"tsup-preset-solid": "^2.1.0",
|
||||||
|
"vite-plugin-solid": "^2.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/form-core": "workspace:*",
|
||||||
|
"@tanstack/solid-store": "^0.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"solid-js": "^1.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
196
packages/solid-form/src/createField.tsx
Normal file
196
packages/solid-form/src/createField.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { FieldApi } from '@tanstack/form-core'
|
||||||
|
import {
|
||||||
|
createComponent,
|
||||||
|
createComputed,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
} from 'solid-js'
|
||||||
|
import { formContext, useFormContext } from './formContext'
|
||||||
|
|
||||||
|
import type { DeepKeys, DeepValue, Narrow } from '@tanstack/form-core'
|
||||||
|
import type { JSXElement } from 'solid-js'
|
||||||
|
import type { CreateFieldOptions } from './types'
|
||||||
|
|
||||||
|
declare module '@tanstack/form-core' {
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
interface FieldApi<
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||||
|
> {
|
||||||
|
Field: FieldComponent<TData, FormValidator>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateField<TParentData> = <
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
>(
|
||||||
|
opts: () => { name: Narrow<TName> } & CreateFieldOptions<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator
|
||||||
|
>,
|
||||||
|
) => () => FieldApi<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
DeepValue<TParentData, TName>
|
||||||
|
>
|
||||||
|
|
||||||
|
// ugly way to trick solid into triggering updates for changes on the fieldApi
|
||||||
|
function makeFieldReactive<FieldApiT extends FieldApi<any, any, any, any>>(
|
||||||
|
fieldApi: FieldApiT,
|
||||||
|
): () => FieldApiT {
|
||||||
|
const [flag, setFlag] = createSignal(false)
|
||||||
|
const fieldApiMemo = createMemo(() => [flag(), fieldApi] as const)
|
||||||
|
const unsubscribeStore = fieldApi.store.subscribe(() => setFlag((f) => !f))
|
||||||
|
onCleanup(unsubscribeStore)
|
||||||
|
return () => fieldApiMemo()[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createField<
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
>(
|
||||||
|
opts: () => CreateFieldOptions<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator
|
||||||
|
>,
|
||||||
|
): () => FieldApi<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator
|
||||||
|
// Omit<typeof opts, 'onMount'> & {
|
||||||
|
// form: FormApi<TParentData>
|
||||||
|
// }
|
||||||
|
> {
|
||||||
|
// Get the form API either manually or from context
|
||||||
|
const { formApi, parentFieldName } = useFormContext()
|
||||||
|
|
||||||
|
const options = opts()
|
||||||
|
const name = (
|
||||||
|
typeof options.index === 'number'
|
||||||
|
? [parentFieldName, options.index, options.name]
|
||||||
|
: [parentFieldName, options.name]
|
||||||
|
)
|
||||||
|
.filter((d) => d !== undefined)
|
||||||
|
.join('.')
|
||||||
|
|
||||||
|
const fieldApi = new FieldApi({
|
||||||
|
...options,
|
||||||
|
form: formApi,
|
||||||
|
name: name as typeof options.name,
|
||||||
|
})
|
||||||
|
fieldApi.Field = Field as never
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fieldApi.update should not have any side effects. Think of it like a `useRef`
|
||||||
|
* that we need to keep updated every render with the most up-to-date information.
|
||||||
|
*
|
||||||
|
* createComputed to make sure this effect runs before render effects
|
||||||
|
*/
|
||||||
|
createComputed(() => fieldApi.update({ ...opts(), form: formApi }))
|
||||||
|
|
||||||
|
// Instantiates field meta and removes it when unrendered
|
||||||
|
onMount(() => onCleanup(fieldApi.mount()))
|
||||||
|
|
||||||
|
return makeFieldReactive(fieldApi)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldComponentProps<
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||||
|
> = {
|
||||||
|
children: (
|
||||||
|
fieldApi: () => FieldApi<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
TData
|
||||||
|
>,
|
||||||
|
) => JSXElement
|
||||||
|
} & (TParentData extends any[]
|
||||||
|
? {
|
||||||
|
name?: TName
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: TName
|
||||||
|
index?: never
|
||||||
|
}) &
|
||||||
|
Omit<
|
||||||
|
CreateFieldOptions<TParentData, TName, ValidatorType, FormValidator>,
|
||||||
|
'name' | 'index'
|
||||||
|
>
|
||||||
|
|
||||||
|
export type FieldComponent<TParentData, FormValidator> = <
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
ValidatorType,
|
||||||
|
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||||
|
>({
|
||||||
|
children,
|
||||||
|
...fieldOptions
|
||||||
|
}: FieldComponentProps<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
TData
|
||||||
|
>) => JSXElement
|
||||||
|
|
||||||
|
export function Field<
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
>(
|
||||||
|
props: {
|
||||||
|
children: (
|
||||||
|
fieldApi: () => FieldApi<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator
|
||||||
|
>,
|
||||||
|
) => JSXElement
|
||||||
|
} & CreateFieldOptions<TParentData, TName, ValidatorType, FormValidator>,
|
||||||
|
) {
|
||||||
|
const fieldApi = createField<
|
||||||
|
TParentData,
|
||||||
|
TName,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator
|
||||||
|
>(() => {
|
||||||
|
const { children, ...fieldOptions } = props
|
||||||
|
return fieldOptions
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<formContext.Provider
|
||||||
|
value={{
|
||||||
|
formApi: fieldApi().form,
|
||||||
|
parentFieldName: String(fieldApi().name),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* createComponent to make sure the signals in the children component are not tracked */}
|
||||||
|
{createComponent(() => props.children(fieldApi), {})}
|
||||||
|
</formContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
53
packages/solid-form/src/createForm.tsx
Normal file
53
packages/solid-form/src/createForm.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { FormOptions, FormState } from '@tanstack/form-core'
|
||||||
|
import { FormApi, functionalUpdate } from '@tanstack/form-core'
|
||||||
|
import { createComputed, type JSXElement } from 'solid-js'
|
||||||
|
import { useStore } from '@tanstack/solid-store'
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
createField,
|
||||||
|
type CreateField,
|
||||||
|
type FieldComponent,
|
||||||
|
} from './createField'
|
||||||
|
import { formContext } from './formContext'
|
||||||
|
|
||||||
|
type NoInfer<T> = [T][T extends any ? 0 : never]
|
||||||
|
|
||||||
|
declare module '@tanstack/form-core' {
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
interface FormApi<TFormData, ValidatorType> {
|
||||||
|
Provider: (props: { children: any }) => any
|
||||||
|
Field: FieldComponent<TFormData, ValidatorType>
|
||||||
|
createField: CreateField<TFormData>
|
||||||
|
useStore: <TSelected = NoInfer<FormState<TFormData>>>(
|
||||||
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected,
|
||||||
|
) => () => TSelected
|
||||||
|
Subscribe: <TSelected = NoInfer<FormState<TFormData>>>(props: {
|
||||||
|
selector?: (state: NoInfer<FormState<TFormData>>) => TSelected
|
||||||
|
children: ((state: () => NoInfer<TSelected>) => JSXElement) | JSXElement
|
||||||
|
}) => any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createForm<TData, FormValidator>(
|
||||||
|
opts?: () => FormOptions<TData, FormValidator>,
|
||||||
|
): FormApi<TData, FormValidator> {
|
||||||
|
const options = opts?.()
|
||||||
|
const formApi = new FormApi<TData, FormValidator>(options)
|
||||||
|
|
||||||
|
formApi.Provider = function Provider(props) {
|
||||||
|
return <formContext.Provider {...props} value={{ formApi: formApi }} />
|
||||||
|
}
|
||||||
|
formApi.Field = Field as any
|
||||||
|
formApi.createField = createField as CreateField<TData>
|
||||||
|
formApi.useStore = (selector) => useStore(formApi.store, selector)
|
||||||
|
formApi.Subscribe = (props) =>
|
||||||
|
functionalUpdate(props.children, useStore(formApi.store, props.selector))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* formApi.update should not have any side effects. Think of it like a `useRef`
|
||||||
|
* that we need to keep updated every render with the most up-to-date information.
|
||||||
|
*/
|
||||||
|
createComputed(() => formApi.update(opts?.()))
|
||||||
|
|
||||||
|
return formApi
|
||||||
|
}
|
||||||
31
packages/solid-form/src/createFormFactory.ts
Normal file
31
packages/solid-form/src/createFormFactory.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { FormApi, FormOptions } from '@tanstack/form-core'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CreateField,
|
||||||
|
type FieldComponent,
|
||||||
|
Field,
|
||||||
|
createField,
|
||||||
|
} from './createField'
|
||||||
|
import { createForm } from './createForm'
|
||||||
|
import { mergeProps } from 'solid-js'
|
||||||
|
|
||||||
|
export type FormFactory<TFormData, FormValidator> = {
|
||||||
|
createForm: (
|
||||||
|
opts?: () => FormOptions<TFormData, FormValidator>,
|
||||||
|
) => FormApi<TFormData, FormValidator>
|
||||||
|
createField: CreateField<TFormData>
|
||||||
|
Field: FieldComponent<TFormData, FormValidator>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFormFactory<TFormData, FormValidator>(
|
||||||
|
defaultOpts?: () => FormOptions<TFormData, FormValidator>,
|
||||||
|
): FormFactory<TFormData, FormValidator> {
|
||||||
|
return {
|
||||||
|
createForm: (opts) =>
|
||||||
|
createForm<TFormData, FormValidator>(() =>
|
||||||
|
mergeProps(defaultOpts?.() ?? {}, opts?.() ?? {}),
|
||||||
|
),
|
||||||
|
createField,
|
||||||
|
Field: Field as never,
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/solid-form/src/formContext.ts
Normal file
20
packages/solid-form/src/formContext.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createContext, useContext } from 'solid-js'
|
||||||
|
import type { FormApi } from '@tanstack/form-core'
|
||||||
|
|
||||||
|
type FormContextType =
|
||||||
|
| undefined
|
||||||
|
| {
|
||||||
|
formApi: FormApi<any, any>
|
||||||
|
parentFieldName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formContext = createContext<FormContextType>(undefined)
|
||||||
|
|
||||||
|
export function useFormContext() {
|
||||||
|
const formApi: FormContextType = useContext(formContext)
|
||||||
|
|
||||||
|
if (!formApi)
|
||||||
|
throw new Error(`You are trying to use the form API outside of a form!`)
|
||||||
|
|
||||||
|
return formApi
|
||||||
|
}
|
||||||
27
packages/solid-form/src/index.ts
Normal file
27
packages/solid-form/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export type {
|
||||||
|
DeepKeys,
|
||||||
|
DeepValue,
|
||||||
|
FieldApiOptions,
|
||||||
|
FieldInfo,
|
||||||
|
FieldMeta,
|
||||||
|
FieldOptions,
|
||||||
|
FieldState,
|
||||||
|
FormOptions,
|
||||||
|
FormState,
|
||||||
|
RequiredByKey,
|
||||||
|
Updater,
|
||||||
|
UpdaterFn,
|
||||||
|
ValidationCause,
|
||||||
|
ValidationError,
|
||||||
|
ValidationMeta,
|
||||||
|
} from '@tanstack/form-core'
|
||||||
|
|
||||||
|
export { FormApi, FieldApi, functionalUpdate } from '@tanstack/form-core'
|
||||||
|
|
||||||
|
export { createForm } from './createForm'
|
||||||
|
|
||||||
|
export type { CreateField, FieldComponent } from './createField'
|
||||||
|
export { createField, Field } from './createField'
|
||||||
|
|
||||||
|
export type { FormFactory } from './createFormFactory'
|
||||||
|
export { createFormFactory } from './createFormFactory'
|
||||||
68
packages/solid-form/src/tests/createField.test-d.tsx
Normal file
68
packages/solid-form/src/tests/createField.test-d.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { assertType } from 'vitest'
|
||||||
|
import { createForm } from '../createForm'
|
||||||
|
|
||||||
|
it('should type state.value properly', () => {
|
||||||
|
function Comp() {
|
||||||
|
const form = createForm(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: 'test',
|
||||||
|
age: 84,
|
||||||
|
},
|
||||||
|
}) as const,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
children={(field) => {
|
||||||
|
assertType<'test'>(field().state.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<form.Field
|
||||||
|
name="age"
|
||||||
|
children={(field) => {
|
||||||
|
assertType<84>(field().state.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should type onChange properly', () => {
|
||||||
|
function Comp() {
|
||||||
|
const form = createForm(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: 'test',
|
||||||
|
age: 84,
|
||||||
|
},
|
||||||
|
}) as const,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
onChange={(val) => {
|
||||||
|
assertType<'test'>(val)
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
children={(field) => null}
|
||||||
|
/>
|
||||||
|
<form.Field
|
||||||
|
name="age"
|
||||||
|
onChange={(val) => {
|
||||||
|
assertType<84>(val)
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
children={(field) => null}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
376
packages/solid-form/src/tests/createField.test.tsx
Normal file
376
packages/solid-form/src/tests/createField.test.tsx
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import { render, waitFor } from '@solidjs/testing-library'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { createFormFactory } from '..'
|
||||||
|
import { sleep } from './utils'
|
||||||
|
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
describe('createField', () => {
|
||||||
|
it('should allow to set default value', () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultValue="FirstName"
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId } = render(() => <Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
expect(input).toHaveValue('FirstName')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use field default value first', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: 'FirstName',
|
||||||
|
lastName: 'LastName',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultValue="otherName"
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onChange={(e) => field().handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
expect(input).toHaveValue('otherName')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not validate on change if isTouched is false', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
const error = 'Please enter a different value'
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
onChange={(value) => (value.includes('other') ? error : undefined)}
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().setValue(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<p>{field().getMeta().errors}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId, queryByText } = render(() => <Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
await user.type(input, 'other')
|
||||||
|
expect(queryByText(error)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate on change if isTouched is true', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
const error = 'Please enter a different value'
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultMeta={{ isTouched: true }}
|
||||||
|
onChange={(value) => (value.includes('other') ? error : undefined)}
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().setValue(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<p>{field().getMeta().errorMap.onChange}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId, getByText, queryByText } = render(() => <Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
expect(queryByText(error)).not.toBeInTheDocument()
|
||||||
|
await user.type(input, 'other')
|
||||||
|
expect(getByText(error)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate on change and on blur', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
const onChangeError = 'Please enter a different value (onChangeError)'
|
||||||
|
const onBlurError = 'Please enter a different value (onBlurError)'
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultMeta={{ isTouched: true }}
|
||||||
|
onChange={(value) =>
|
||||||
|
value.includes('other') ? onChangeError : undefined
|
||||||
|
}
|
||||||
|
onBlur={(value) =>
|
||||||
|
value.includes('other') ? onBlurError : undefined
|
||||||
|
}
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<p>{field().getMeta().errorMap.onChange}</p>
|
||||||
|
<p>{field().getMeta().errorMap.onBlur}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId, getByText, queryByText } = render(() => <Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
expect(queryByText(onChangeError)).not.toBeInTheDocument()
|
||||||
|
expect(queryByText(onBlurError)).not.toBeInTheDocument()
|
||||||
|
await user.type(input, 'other')
|
||||||
|
expect(getByText(onChangeError)).toBeInTheDocument()
|
||||||
|
// @ts-expect-error unsure why the 'vitest/globals' in tsconfig doesnt work here
|
||||||
|
await user.click(document.body)
|
||||||
|
expect(queryByText(onBlurError)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate async on change', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
const error = 'Please enter a different value'
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultMeta={{ isTouched: true }}
|
||||||
|
onChangeAsync={async () => {
|
||||||
|
await sleep(10)
|
||||||
|
return error
|
||||||
|
}}
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<p>{field().getMeta().errorMap.onChange}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId, getByText, queryByText } = render(() => <Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
expect(queryByText(error)).not.toBeInTheDocument()
|
||||||
|
await user.type(input, 'other')
|
||||||
|
await waitFor(() => getByText(error))
|
||||||
|
expect(getByText(error)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate async on change and async on blur', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
const onChangeError = 'Please enter a different value (onChangeError)'
|
||||||
|
const onBlurError = 'Please enter a different value (onBlurError)'
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultMeta={{ isTouched: true }}
|
||||||
|
onChangeAsync={async () => {
|
||||||
|
await sleep(10)
|
||||||
|
return onChangeError
|
||||||
|
}}
|
||||||
|
onBlurAsync={async () => {
|
||||||
|
await sleep(10)
|
||||||
|
return onBlurError
|
||||||
|
}}
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<p>{field().getMeta().errorMap?.onChange}</p>
|
||||||
|
<p>{field().getMeta().errorMap?.onBlur}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId, getByText, queryByText } = render(() => <Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
|
||||||
|
expect(queryByText(onChangeError)).not.toBeInTheDocument()
|
||||||
|
expect(queryByText(onBlurError)).not.toBeInTheDocument()
|
||||||
|
await user.type(input, 'other')
|
||||||
|
await waitFor(() => getByText(onChangeError))
|
||||||
|
expect(getByText(onChangeError)).toBeInTheDocument()
|
||||||
|
// @ts-expect-error unsure why the 'vitest/globals' in tsconfig doesnt work here
|
||||||
|
await user.click(document.body)
|
||||||
|
await waitFor(() => getByText(onBlurError))
|
||||||
|
expect(getByText(onBlurError)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate async on change with debounce', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
const mockFn = vi.fn()
|
||||||
|
const error = 'Please enter a different value'
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultMeta={{ isTouched: true }}
|
||||||
|
onChangeAsyncDebounceMs={100}
|
||||||
|
onChangeAsync={async () => {
|
||||||
|
mockFn()
|
||||||
|
await sleep(10)
|
||||||
|
return error
|
||||||
|
}}
|
||||||
|
children={(field) => (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
name={field().name}
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onInput={(e) => field().handleChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<p>{field().getMeta().errors}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId, getByText } = render(() => <Comp />)
|
||||||
|
const input = getByTestId('fieldinput')
|
||||||
|
await user.type(input, 'other')
|
||||||
|
// mockFn will have been called 5 times without onChangeAsyncDebounceMs
|
||||||
|
expect(mockFn).toHaveBeenCalledTimes(0)
|
||||||
|
await waitFor(() => getByText(error))
|
||||||
|
expect(getByText(error)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
116
packages/solid-form/src/tests/createForm.test.tsx
Normal file
116
packages/solid-form/src/tests/createForm.test.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { render, screen, waitFor } from '@solidjs/testing-library'
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { createFormFactory, createForm } from '..'
|
||||||
|
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
describe('createForm', () => {
|
||||||
|
it('preserves field state', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
defaultValue={''}
|
||||||
|
children={(field) => (
|
||||||
|
<input
|
||||||
|
data-testid="fieldinput"
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onChange={(e) => field().handleChange(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render(() => <Comp />)
|
||||||
|
const input = screen.getByTestId('fieldinput')
|
||||||
|
expect(screen.queryByText('FirstName')).not.toBeInTheDocument()
|
||||||
|
await user.type(input, 'FirstName')
|
||||||
|
expect(input).toHaveValue('FirstName')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow default values to be set', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>()
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: 'FirstName',
|
||||||
|
lastName: 'LastName',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
children={(field) => {
|
||||||
|
return <p>{field().state.value}</p>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { findByText, queryByText } = render(() => <Comp />)
|
||||||
|
expect(await findByText('FirstName')).toBeInTheDocument()
|
||||||
|
expect(queryByText('LastName')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle submitting properly', async () => {
|
||||||
|
let submittedData = null as { firstName: string } | null
|
||||||
|
function Comp() {
|
||||||
|
const form = createForm(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: 'FirstName',
|
||||||
|
},
|
||||||
|
onSubmit: (data) => {
|
||||||
|
submittedData = data
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
value={field().state.value}
|
||||||
|
onBlur={field().handleBlur}
|
||||||
|
onChange={(e) => field().handleChange(e.target.value)}
|
||||||
|
placeholder={'First name'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button onClick={form.handleSubmit}>Submit</button>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { findByPlaceholderText, getByText } = render(() => <Comp />)
|
||||||
|
const input = await findByPlaceholderText('First name')
|
||||||
|
await user.clear(input)
|
||||||
|
await user.type(input, 'OtherName')
|
||||||
|
await user.click(getByText('Submit'))
|
||||||
|
expect(submittedData?.firstName).toEqual('OtherName')
|
||||||
|
})
|
||||||
|
})
|
||||||
38
packages/solid-form/src/tests/createFormFactory.test.tsx
Normal file
38
packages/solid-form/src/tests/createFormFactory.test.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { render } from '@solidjs/testing-library'
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { createFormFactory } from '..'
|
||||||
|
|
||||||
|
describe('createFormFactory', () => {
|
||||||
|
it('should allow default values to be set', async () => {
|
||||||
|
type Person = {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const formFactory = createFormFactory<Person, unknown>(() => ({
|
||||||
|
defaultValues: {
|
||||||
|
firstName: 'FirstName',
|
||||||
|
lastName: 'LastName',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
function Comp() {
|
||||||
|
const form = formFactory.createForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form.Provider>
|
||||||
|
<form.Field
|
||||||
|
name="firstName"
|
||||||
|
children={(field) => {
|
||||||
|
return <p>{field().state.value}</p>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { findByText, queryByText } = render(() => <Comp />)
|
||||||
|
expect(await findByText('FirstName')).toBeInTheDocument()
|
||||||
|
expect(queryByText('LastName')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
5
packages/solid-form/src/tests/utils.ts
Normal file
5
packages/solid-form/src/tests/utils.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export function sleep(timeout: number): Promise<void> {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
setTimeout(resolve, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
11
packages/solid-form/src/types.ts
Normal file
11
packages/solid-form/src/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { FieldOptions, DeepKeys, DeepValue } from '@tanstack/form-core'
|
||||||
|
|
||||||
|
export type CreateFieldOptions<
|
||||||
|
TParentData,
|
||||||
|
TName extends DeepKeys<TParentData>,
|
||||||
|
ValidatorType,
|
||||||
|
FormValidator,
|
||||||
|
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
||||||
|
> = FieldOptions<TParentData, TName, ValidatorType, FormValidator, TData> & {
|
||||||
|
mode?: 'value' | 'array'
|
||||||
|
}
|
||||||
7
packages/solid-form/tsconfig.eslint.json
Normal file
7
packages/solid-form/tsconfig.eslint.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts", "**/*.tsx", ".eslintrc.cjs", "tsup.config.js"]
|
||||||
|
}
|
||||||
10
packages/solid-form/tsconfig.json
Normal file
10
packages/solid-form/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"types": ["vitest/globals"],
|
||||||
|
"outDir": "./build/lib"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
24
packages/solid-form/tsup.config.js
Normal file
24
packages/solid-form/tsup.config.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { defineConfig } from 'tsup'
|
||||||
|
import { generateTsupOptions, parsePresetOptions } from 'tsup-preset-solid'
|
||||||
|
|
||||||
|
const preset_options = {
|
||||||
|
entries: {
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
dev_entry: true,
|
||||||
|
},
|
||||||
|
cjs: true,
|
||||||
|
drop_console: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig(() => {
|
||||||
|
const parsed_data = parsePresetOptions(preset_options)
|
||||||
|
const tsup_options = generateTsupOptions(parsed_data)
|
||||||
|
|
||||||
|
tsup_options.forEach((tsup_option) => {
|
||||||
|
tsup_option.outDir = 'build'
|
||||||
|
})
|
||||||
|
|
||||||
|
return tsup_options
|
||||||
|
})
|
||||||
15
packages/solid-form/vitest.config.ts
Normal file
15
packages/solid-form/vitest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import solid from 'vite-plugin-solid'
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
name: 'solid-form',
|
||||||
|
dir: './src',
|
||||||
|
watch: false,
|
||||||
|
setupFiles: [],
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
coverage: { provider: 'istanbul' },
|
||||||
|
},
|
||||||
|
plugins: [solid()],
|
||||||
|
})
|
||||||
4961
pnpm-lock.yaml
generated
4961
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,4 @@ packages:
|
|||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
- 'examples/react/**'
|
- 'examples/react/**'
|
||||||
- 'examples/solid/**'
|
- 'examples/solid/**'
|
||||||
- 'examples/svelte/**'
|
|
||||||
- 'examples/vue/**'
|
- 'examples/vue/**'
|
||||||
- '!examples/vue/2*'
|
|
||||||
- '!examples/vue/nuxt*'
|
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ export const packages: Package[] = [
|
|||||||
name: '@tanstack/yup-form-adapter',
|
name: '@tanstack/yup-form-adapter',
|
||||||
packageDir: 'yup-form-adapter',
|
packageDir: 'yup-form-adapter',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '@tanstack/solid-form',
|
||||||
|
packageDir: 'solid-form',
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// name: '@tanstack/solid-store',
|
// name: '@tanstack/svelte-form',
|
||||||
// packageDir: 'solid-store',
|
// packageDir: 'svelte-form',
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: '@tanstack/svelte-store',
|
|
||||||
// packageDir: 'svelte-store',
|
|
||||||
// },
|
// },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@tanstack/form-core": ["packages/form-core"],
|
"@tanstack/form-core": ["packages/form-core"],
|
||||||
"@tanstack/react-form": ["packages/react-form"],
|
"@tanstack/react-form": ["packages/react-form"],
|
||||||
"@tanstack/vue-form": ["packages/vue-form"],
|
"@tanstack/vue-form": ["packages/vue-form"],
|
||||||
|
"@tanstack/solid-form": ["packages/solid-form"],
|
||||||
"@tanstack/yup-form-adapter": ["packages/yup-form-adapter"],
|
"@tanstack/yup-form-adapter": ["packages/yup-form-adapter"],
|
||||||
"@tanstack/zod-form-adapter": ["packages/zod-form-adapter"]
|
"@tanstack/zod-form-adapter": ["packages/zod-form-adapter"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "packages/form-core" },
|
{ "path": "packages/form-core" },
|
||||||
{ "path": "packages/react-form" },
|
{ "path": "packages/react-form" },
|
||||||
{ "path": "packages/vue-form" }
|
{ "path": "packages/vue-form" },
|
||||||
|
{ "path": "packages/solid-form" }
|
||||||
]
|
]
|
||||||
// "include": ["examples/*"]
|
// "include": ["examples/*"]
|
||||||
// "exclude": ["node_modules"]
|
// "exclude": ["node_modules"]
|
||||||
|
|||||||
Reference in New Issue
Block a user