big changes
16
.babelrc
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"modules": false,
|
||||
"exclude": [
|
||||
"@babel/plugin-transform-async-to-generator",
|
||||
"@babel/plugin-transform-regenerator"
|
||||
]
|
||||
}
|
||||
],
|
||||
"@babel/react"
|
||||
],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||
}
|
||||
7
.browserslistrc
Normal file
@@ -0,0 +1,7 @@
|
||||
# Browsers we support
|
||||
Chrome >= 73
|
||||
Firefox >= 78
|
||||
Edge >= 79
|
||||
Safari >= 12.0
|
||||
iOS >= 12.0
|
||||
opera >= 53
|
||||
6
.codesandbox/ci.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"installCommand": "install:csb",
|
||||
"sandboxes": ["/examples/react/basic-typescript", "/examples/solid/basic-typescript", "/examples/svelte/basic", "/examples/vue/basic"],
|
||||
"packages": ["packages/**"],
|
||||
"node": "16"
|
||||
}
|
||||
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
55
.eslintrc
@@ -1,3 +1,56 @@
|
||||
{
|
||||
"extends": ["react-app", "prettier"]
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"react-app",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.base.json",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": true,
|
||||
"typescript": {
|
||||
"project": "packages/*/tsconfig.json"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"react/jsx-key": ["error", { "checkFragmentShorthand": true }],
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{
|
||||
"ignoreParameters": true
|
||||
}
|
||||
],
|
||||
"no-shadow": "error",
|
||||
"import/no-cycle": "error",
|
||||
"import/no-unresolved": ["error", { "ignore": ["^@tanstack\/"] }],
|
||||
"import/no-unused-modules": ["off", { "unusedExports": true }],
|
||||
"no-redeclare": "off",
|
||||
"react-hooks/exhaustive-deps": "error"
|
||||
}
|
||||
}
|
||||
|
||||
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto eol=lf
|
||||
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [tannerlinsley, tkdodo]
|
||||
132
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: '🐛 Bug report'
|
||||
description: Report a reproducible bug or regression
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for reporting an issue :pray:.
|
||||
|
||||
This issue tracker is for reporting reproducible bugs or regression's found in [TanStack Form](https://github.com/TanStack/form)
|
||||
If you have a question about how to achieve something and are struggling, please post a question
|
||||
inside of TanStack Form's [Discussions tab](https://github.com/TanStack/form/discussions)
|
||||
|
||||
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
|
||||
- TanStack Form's [Discussions tab](https://github.com/TanStack/form/discussions)
|
||||
- TanStack Form's [Open Issues](https://github.com/TanStack/form/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
|
||||
- TanStack Form's [Closed Issues](https://github.com/TanStack/form/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)
|
||||
|
||||
The more information you fill in, the better the community can help you.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: Provide a clear and concise description of the challenge you are running into.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: link
|
||||
attributes:
|
||||
label: Your minimal, reproducible example
|
||||
description: |
|
||||
Please add a link to a minimal reproduction.
|
||||
Note:
|
||||
- Your bug may get fixed much faster if we can run your code and it doesn't have dependencies other than React/Solid/Vue/Svelte.
|
||||
- To create a shareable code example for web, you can use CodeSandbox (https://codesandbox.io/s/new) or Stackblitz (https://stackblitz.com/).
|
||||
- Please make sure the example is complete and runnable - e.g. avoid localhost URLs.
|
||||
- To stub out real api requests - Promise.resolve and Promise.reject are good options for easy reproduction
|
||||
- Feel free to fork any of the official CodeSandbox examples to reproduce your issue: https://tanstack.com/form/v4/docs/examples/react/simple
|
||||
- For React Native, you can use: https://snack.expo.dev/
|
||||
- For TypeScript related issues only, a TypeScript Playground link might be sufficient: https://www.typescriptlang.org/play
|
||||
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
|
||||
placeholder: |
|
||||
e.g. Code Sandbox, Stackblitz, Expo Snack or TypeScript playground
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Describe the steps we have to take to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Provide a clear and concise description of what you expected to happen.
|
||||
placeholder: |
|
||||
As a user, I expected ___ behavior but i am seeing ___
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: How often does this bug happen?
|
||||
description: |
|
||||
Following the repro steps above, how easily are you able to reproduce this bug?
|
||||
options:
|
||||
- Every time
|
||||
- Often
|
||||
- Sometimes
|
||||
- Only once
|
||||
- type: textarea
|
||||
id: screenshots_or_videos
|
||||
attributes:
|
||||
label: Screenshots or Videos
|
||||
description: |
|
||||
If applicable, add screenshots or a video to help explain your problem.
|
||||
For more information on the supported file image/file types and the file size limits, please refer
|
||||
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
|
||||
placeholder: |
|
||||
You can drag your video or image files inside of this editor ↓
|
||||
- type: textarea
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: |
|
||||
Please let us know which Operting System, Browser and Browser version you were using when the issue occurred.
|
||||
placeholder: |
|
||||
- OS: [e.g. macOS, Windows, Linux, iOS, Android]
|
||||
- Browser: [e.g. Chrome, Safari, Firefox, React Native]
|
||||
- Version: [e.g. 91.1]
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: adapter
|
||||
attributes:
|
||||
label: Tanstack Form adapter
|
||||
description: |
|
||||
Please let us know which adapter of TanStack Form you were using when the issue occurred.
|
||||
options:
|
||||
- react-form
|
||||
- solid-form
|
||||
- svelte-form
|
||||
- vue-form
|
||||
- vanilla
|
||||
- type: input
|
||||
id: rq-version
|
||||
attributes:
|
||||
label: TanStack Form version
|
||||
description: |
|
||||
Please let us know the exact version of TanStack Form you were using when the issue occurred. Please don't just put in "latest", as this is subject to change.
|
||||
placeholder: |
|
||||
e.g. v3.30.1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: ts-version
|
||||
attributes:
|
||||
label: TypeScript version
|
||||
description: |
|
||||
If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred.
|
||||
placeholder: |
|
||||
e.g. v4.5.4
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Requests & Questions
|
||||
url: https://github.com/TanStack/form/discussions
|
||||
about: Please ask and answer questions here.
|
||||
- name: Community Chat
|
||||
url: https://discord.gg/mQd7egN
|
||||
about: A dedicated discord server hosted by Tanner Linsley
|
||||
58
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: ci
|
||||
concurrency:
|
||||
group: publish-${{ github.github.base_ref }}
|
||||
cancel-in-progress: true
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: override release tag
|
||||
required: false
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'alpha'
|
||||
- 'beta'
|
||||
env:
|
||||
NX_DAEMON: false
|
||||
NX_VERBOSE_LOGGING: true
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
jobs:
|
||||
test-and-publish:
|
||||
if: github.repository == 'TanStack/form' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/alpha' || github.ref == 'refs/heads/beta')
|
||||
name: 'Test & Publish'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.2
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter "./packages/**" --filter form --prefer-offline install
|
||||
- name: Run Tests
|
||||
uses: nick-fields/retry@v2.8.3
|
||||
with:
|
||||
command: pnpm run test:ci --base=${{ github.event.before }}
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
- name: Publish
|
||||
run: |
|
||||
git config --global user.name 'Tanner Linsley'
|
||||
git config --global user.email 'tannerlinsley@users.noreply.github.com'
|
||||
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
|
||||
pnpm run cipublish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
133
.github/workflows/pr.yml
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
name: pr
|
||||
on: [pull_request]
|
||||
env:
|
||||
NX_DAEMON: false
|
||||
NX_VERBOSE_LOGGING: true
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
jobs:
|
||||
test:
|
||||
name: 'Test'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.2
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter "./packages/**" --filter form --prefer-offline install
|
||||
- name: Run Tests
|
||||
uses: nick-fields/retry@v2.8.3
|
||||
with:
|
||||
command: pnpm test:lib --base=${{ github.event.pull_request.base.sha }}
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
lint:
|
||||
name: 'Lint'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.2
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter "./packages/**" --filter form --prefer-offline install
|
||||
- run: pnpm run test:eslint --base=${{ github.event.pull_request.base.sha }}
|
||||
typecheck:
|
||||
name: 'Typecheck'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.2
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter "./packages/**" --filter form --prefer-offline install
|
||||
- run: pnpm run test:types --base=${{ github.event.pull_request.base.sha }}
|
||||
format:
|
||||
name: 'Format'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.2
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter "./packages/**" --filter form --prefer-offline install
|
||||
- run: pnpm run test:format --base=${{ github.event.pull_request.base.sha }}
|
||||
test-react-17:
|
||||
name: 'Test React 17'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.2
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter "./packages/**" --filter form --prefer-offline install
|
||||
- name: Run Tests
|
||||
uses: nick-fields/retry@v2.8.3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
max_attempts: 3
|
||||
command: pnpm run test:react:17 --base=${{ github.event.pull_request.base.sha }}
|
||||
env:
|
||||
REACTJS_VERSION: 17
|
||||
test-build:
|
||||
name: 'Test Build'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.14.2
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm --filter "./packages/**" --filter form --prefer-offline install
|
||||
- run: pnpm run test:build
|
||||
env:
|
||||
BUNDLEWATCH_GITHUB_TOKEN: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }}
|
||||
55
.gitignore
vendored
@@ -1,14 +1,47 @@
|
||||
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
/docs-dist
|
||||
/index.js
|
||||
/build
|
||||
/dist
|
||||
/demo/dist
|
||||
/es
|
||||
/lib
|
||||
/umd
|
||||
.tmp/
|
||||
.idea
|
||||
.gitconfig
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# builds
|
||||
types
|
||||
build
|
||||
*/build
|
||||
dist
|
||||
lib
|
||||
es
|
||||
artifacts
|
||||
.rpt2_cache
|
||||
coverage
|
||||
*.tgz
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.next
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.history
|
||||
size-plugin.json
|
||||
stats-hydration.json
|
||||
stats.json
|
||||
stats.html
|
||||
.vscode/settings.json
|
||||
|
||||
*.log
|
||||
.DS_Store
|
||||
node_modules
|
||||
.cache
|
||||
dist
|
||||
.idea
|
||||
|
||||
nx-cloud.env
|
||||
|
||||
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
/packages/svelte-form/.svelte-kit
|
||||
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"pluginSearchDirs": false,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 4.0.1
|
||||
|
||||
- Fix bug where bracket syntax could result in a bad field path
|
||||
|
||||
## 4.0.0
|
||||
|
||||
- First stable release for v4!
|
||||
135
CONTRIBUTING.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Contributing
|
||||
|
||||
## Questions
|
||||
|
||||
If you have questions about implementation details, help or support, then please use our dedicated community forum at [GitHub Discussions](https://github.com/TanStack/form/discussions) **PLEASE NOTE:** If you choose to instead open an issue for your question, your issue will be immediately closed and redirected to the forum.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/form/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/form/discussions)
|
||||
|
||||
## Suggesting new features
|
||||
|
||||
If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented.
|
||||
|
||||
## Development
|
||||
|
||||
If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started:
|
||||
|
||||
- Fork this repository.
|
||||
- Install dependencies by running `$ pnpm install`.
|
||||
- We use [pnpm](https://pnpm.io/) v7 for package management.
|
||||
- We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc`.
|
||||
- Run development server using `pnpm run watch`.
|
||||
- Implement your changes and tests to files in the `src/` directory and corresponding test files.
|
||||
- Document your changes in the appropriate doc page.
|
||||
- Git stage your required changes and commit (see below commit guidelines).
|
||||
- Submit PR for review.
|
||||
|
||||
### Running examples
|
||||
- Make sure you've installed the dependencies by running `$ pnpm install` in the repo's root directory.
|
||||
- If you want to run the example against your local changes, run `pnpm run watch` in the repo's root directory. Otherwise, it will be run against the latest TanStack Form release.
|
||||
- Run `pnpm run dev` in the selected examples' directory.
|
||||
|
||||
#### Note on `examples/react-native`
|
||||
React Native example requires Expo to work. Please follow the instructions from example's README.md file to learn more.
|
||||
|
||||
#### Note on standalone execution
|
||||
If you want to run an example without installing dependencies for the whole repo, just follow instructions from the example's README.md file. It will be then run against the latest TanStack Form release.
|
||||
|
||||
## Online one-click setup
|
||||
|
||||
You can use Gitpod (An Online Open Source VS Code like IDE which is free for Open Source) for developing online. With a single click it will start a workspace and automatically:
|
||||
|
||||
- clone the `TanStack/form` repo.
|
||||
- install all the dependencies in `/` and `/docs`.
|
||||
- run `npm start` in the root(`/`) to Auto-build files.
|
||||
- run `npm run dev` in `/docs`.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/TanStack/form)
|
||||
|
||||
## Commit message conventions
|
||||
|
||||
`TanStack/form` is using [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines).
|
||||
|
||||
We have very precise rules over how our git commit messages can be formatted. This leads to **more readable messages** that are easy to follow when looking through the **project history**.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
|
||||
format that includes a **type**, a **scope** and a **subject**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The **header** is mandatory and the **scope** of the header is optional.
|
||||
|
||||
Any line of the commit message cannot be longer than 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools.
|
||||
|
||||
### Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- **feat**: A new feature
|
||||
- **fix**: A bug fix
|
||||
- **docs**: Documentation only changes
|
||||
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
semi-colons, etc)
|
||||
- **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
- **perf**: A code change that improves performance
|
||||
- **test**: Adding missing or correcting existing tests
|
||||
- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
|
||||
generation
|
||||
|
||||
### Scope
|
||||
|
||||
The scope could be anything specifying place of the commit change. For example `useForm`, `useMutation` etc...
|
||||
|
||||
You can use `*` when the change affects more than a single scope.
|
||||
|
||||
### Subject
|
||||
|
||||
The subject contains succinct description of the change:
|
||||
|
||||
- use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
- don't capitalize first letter
|
||||
- no dot (.) at the end
|
||||
|
||||
### Body
|
||||
|
||||
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
### Footer
|
||||
|
||||
The footer should contain any information about **Breaking Changes** and is also the place to [reference GitHub issues that this commit closes](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue).
|
||||
|
||||
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
|
||||
|
||||
### Example
|
||||
|
||||
Here is an example of the release type that will be done based on a commit messages:
|
||||
|
||||
| Commit message | Release type |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- |
|
||||
| `fix(pencil): stop graphite breaking when too much pressure applied` | Patch Release |
|
||||
| `feat(pencil): add 'graphiteWidth' option` | ~~Minor~~ Feature Release |
|
||||
| `perf(pencil): remove graphiteWidth option`<br><br>`BREAKING CHANGE: The graphiteWidth option has been removed.`<br>`The default graphite width of 10mm is always used for performance reasons.` | ~~Major~~ Breaking Release |
|
||||
|
||||
### Revert
|
||||
|
||||
If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface.
|
||||
|
||||
Use an appropriate commit type. Be especially careful with breaking changes.
|
||||
|
||||
## Releases
|
||||
|
||||
For each new commit added to `main` with `git push` or by merging a pull request or merging from another branch, a GitHub action is triggered and runs the `semantic-release` command to make a release if there are codebase changes since the last release that affect the package functionalities.
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-present Tanner Linsley
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
187
README.md
@@ -1,161 +1,54 @@
|
||||

|
||||
<img src="https://static.scarf.sh/a.png?x-pxid=be2d8a11-9712-4c1d-9963-580b2d4fb133" />
|
||||
|
||||
<img src='https://github.com/tannerlinsley/react-form/raw/master/media/logo.png' width='300'/>
|
||||

|
||||
|
||||
Hooks for managing form state and validation in React
|
||||
Hooks for fetching, caching and updating asynchronous data in React, Solid, Svelte and Vue
|
||||
|
||||
<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://npmjs.com/package/react-form" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/npm/dm/react-form.svg" />
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/result?p=react-form@next" target="\_parent">
|
||||
<img alt="" src="https://badgen.net/bundlephobia/minzip/react-form@next" />
|
||||
</a>
|
||||
<a href="https://spectrum.chat/react-form">
|
||||
<img alt="Join the community on Spectrum" src="https://withspectrum.github.io/badge/badge.svg" />
|
||||
</a>
|
||||
<a href="https://github.com/tannerlinsley/react-form" target="\_parent">
|
||||
<img alt="" src="https://img.shields.io/github/stars/tannerlinsley/react-form.svg?style=social&label=Star" />
|
||||
</a>
|
||||
<a href="https://twitter.com/tannerlinsley" target="\_parent">
|
||||
</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%22form+tests%22">
|
||||
<img src="https://github.com/TanStack/form/workflows/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/form@latest" target="\_parent">
|
||||
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/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%2Fform%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>
|
||||
<br />
|
||||
|
||||
Enjoy this library? Try them all! [React Table](https://github.com/tannerlinsley/react-table), [React Query](https://github.com/tannerlinsley/react-query), [React Charts](https://github.com/tannerlinsley/react-charts), [React Virtual](https://github.com/tannerlinsley/react-virtual)
|
||||
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)
|
||||
|
||||
## Features
|
||||
## Visit [tanstack.com/form](https://tanstack.com/form) for docs, guides, API and more!
|
||||
|
||||
- Built **with** React hooks **for** React hooks
|
||||
- Highly practical validation API with 1st-class asynchronous support
|
||||
- Built-in validation debouncing with auto cancellation for stale validations
|
||||
- Field Scoping for deeply nested form values
|
||||
- No nonsense meta management for both forms and form fields
|
||||
- Fully memoized for frequent and fast rerenders
|
||||
- Flexible form API at the field, scope, and form levels
|
||||
Still on **React Form v2**? No problem! Check out the v2 docs here: https://github.com/TanStack/form/tree/2.x/docs/src/pages/docs. <br />
|
||||
Still on **React Form v3**? No problem! Check out the v3 docs here: https://tanstack.com/form/v3/docs/.
|
||||
|
||||
## Sponsors
|
||||
## Quick Features
|
||||
|
||||
This library is being built and maintained by me, @tannerlinsley and I am always in need of more support to keep projects like this afloat. If you would like to get premium support, add your logo or name on this README, or simply just contribute to my open source Sponsorship goal, [visit my Github Sponsors page!](https://github.com/sponsors/tannerlinsley/)
|
||||
- Transport/protocol/backend agnostic data fetching (REST, GraphQL, promises, whatever!)
|
||||
- Auto Caching + Refetching (stale-while-revalidate, Window Refocus, Polling/Realtime)
|
||||
- Parallel + Dependent Queries
|
||||
- Mutations + Reactive Form Refetching
|
||||
- Multi-layer Cache + Automatic Garbage Collection
|
||||
- Paginated + Cursor-based Queries
|
||||
- Load-More + Infinite Scroll Queries w/ Scroll Recovery
|
||||
- Request Cancellation
|
||||
- [React Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html) + Fetch-As-You-Render Form Prefetching
|
||||
- Dedicated Devtools
|
||||
- <a href="https://bundlephobia.com/package/@tanstack/form@latest" target="\_parent">
|
||||
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/form" />
|
||||
</a> (depending on features imported)
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley" target="_blank">
|
||||
<img width='150' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/diamond.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley" target="_blank">
|
||||
Become a Sponsor!
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley/" target="_blank">
|
||||
<img width='150' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/platinum.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley" target="_blank">
|
||||
Become a Sponsor!
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley/" target="_blank">
|
||||
<img width='150' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/gold.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley" target="_blank">
|
||||
Become a Sponsor!
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley/" target="_blank">
|
||||
<img width='150' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/silver.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://github.com/sponsors/tannerlinsley" target="_blank">
|
||||
Become a Sponsor!
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<a href="https://github.com/sponsors/tannerlinsley/">
|
||||
<img width='150' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/supporters.png" />
|
||||
</a>
|
||||
</td>
|
||||
<!-- <td>
|
||||
<ul>
|
||||
<li><a href=""><a></li>
|
||||
</ul>
|
||||
</td> -->
|
||||
<td>
|
||||
<a href="https://github.com/sponsors/tannerlinsley" target="_blank">
|
||||
Become a Supporter!
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td valign="top">
|
||||
<a href="https://github.com/sponsors/tannerlinsley/">
|
||||
<img width='150' src="https://raw.githubusercontent.com/tannerlinsley/files/master/images/patreon/fans.png" />
|
||||
</a>
|
||||
</td>
|
||||
<!-- <td>
|
||||
<ul>
|
||||
<li></li>
|
||||
</ul>
|
||||
</td> -->
|
||||
<td>
|
||||
<a href="https://github.com/sponsors/tannerlinsley" target="_blank">
|
||||
Become a Fan!
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
# Documentation
|
||||
|
||||
- [Installation](./docs/installation.md)
|
||||
- [Examples](./docs/examples.md)
|
||||
- [API](./docs/api.md)
|
||||
- [Validation Guide](./docs/validation.md)
|
||||
- [Field Scoping Guide](./docs/field-scoping.md)
|
||||
<!-- Use the force, Luke -->
|
||||
|
||||
44
babel.config.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { NODE_ENV, BABEL_ENV } = process.env
|
||||
const cjs = NODE_ENV === 'test' || BABEL_ENV === 'commonjs'
|
||||
const es = BABEL_ENV === 'es'
|
||||
const loose = true
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
loose,
|
||||
modules: false,
|
||||
exclude: [
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/plugin-transform-parameters',
|
||||
],
|
||||
},
|
||||
],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: [
|
||||
cjs && ['@babel/transform-modules-commonjs', { loose }],
|
||||
es && ['babel-plugin-add-import-extension', { extension: 'mjs' }],
|
||||
// no runtime for umd builds
|
||||
BABEL_ENV && [
|
||||
'@babel/transform-runtime',
|
||||
{
|
||||
version: require('./package.json').dependencies[
|
||||
'@babel/runtime'
|
||||
].replace(/^[^0-9]*/, ''),
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
overrides: [
|
||||
{
|
||||
exclude: ['./packages/solid-form/**', './packages/svelte-form/**', './packages/vue-form/**'],
|
||||
presets: ['@babel/react'],
|
||||
},
|
||||
{
|
||||
include: './packages/solid-form/**',
|
||||
presets: ['babel-preset-solid'],
|
||||
},
|
||||
],
|
||||
}
|
||||
6
codecov.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 90%
|
||||
threshold: 1%
|
||||
317
docs/api.md
@@ -1,317 +0,0 @@
|
||||
# API
|
||||
|
||||
- [`useForm`](#useform)
|
||||
- [`useField`](#usefield)
|
||||
- [`splitFormProps`](#splitformprops)
|
||||
- [`useFormContext`](#useformcontext)
|
||||
|
||||
## `useForm`
|
||||
|
||||
```js
|
||||
import { useForm } from 'react-form'
|
||||
|
||||
const instance = useForm(options)
|
||||
```
|
||||
|
||||
### Form Options
|
||||
|
||||
- `defaultValues: any`
|
||||
- Use defaultValues to set the default values state for the form. If this object ever changes, the state will be reset to the new object. Thus, its important to use React.useMemo to make this object only change when necessary
|
||||
- `onSubmit(values, instance) => Promise`
|
||||
- `onSubmit` is called when the form's Form element is submitted or when `instance.handleSubmit` is called.
|
||||
- `values` is the submitted values object for the form
|
||||
- `instance` is the latest version of the form instance (the same instance that is returned from `useForm`)
|
||||
- If the promise returned from this function resolves, `instance.isSubmitted` will be set to `true`
|
||||
- When the promise returned from this function settles, `instance.isSubmitting` will be set to `false`
|
||||
- `validate async (values, instance) => String | false | undefined`
|
||||
- `validate` is an asynchronous function that is called when the form becomes dirty and is given the opportunity to set/clear/update errors and/or manage general form state
|
||||
- `instance` is the latest version of the form instance (the same instance that is returned from `useForm`)
|
||||
- [See below](#form-validation) for more information on form validation
|
||||
- Field level validation is also available. [See `useField`](#useField)
|
||||
- `validatePristine: Bool`
|
||||
- If you want validation to run when instance.isDirty is false, you can set the `validatePristine` option to `true`
|
||||
- `debugForm: Bool`
|
||||
- If set to `true` the form instance will be serialized and rendered after the `Form` element returned by the instance
|
||||
|
||||
### Form Instance
|
||||
|
||||
An `object` with the following components, properties and methods:
|
||||
|
||||
- `Form: ReactComponent<form>`
|
||||
- **Required**
|
||||
- This component is a ready-to-use wrapper around the html `form` element.
|
||||
- It will proxy any props you pass it, excluding `onSubmit`.
|
||||
- It properly supplies the form context to any `useField` instances inside it.
|
||||
- `values: any`
|
||||
- The current values for the entire form
|
||||
- `meta: Object`
|
||||
|
||||
- `error: String | any`
|
||||
- If an error is returned by the form's `validation` function, or if it is set programmatically, it will be stored here.
|
||||
- `isSubmitting: Bool`
|
||||
- Will be set to `true` if the form is currently submitting
|
||||
- `isDirty: Bool`
|
||||
- Will be set to `true` if the form is dirty
|
||||
- `isSubmitted: Bool`
|
||||
- Will be set to `true` if the form has been submitted successfully
|
||||
- `submissionAttempts: Int`
|
||||
- Will be incremented, starting from `0`, every time a form submission is attempted
|
||||
- `isValid: Bool`
|
||||
- Will be set to `true` if `isDirty === true` and `isValid === true`, meaning that the form has been touched and there are no field-level or form-level errors
|
||||
- `fieldsAreValidating`
|
||||
- Will be `true` if there are any fields validating (`field.meta.isValidating`)
|
||||
- `fieldsAreValid`
|
||||
- Will be `true` if there are no fields with an error (`field.meta.error`)
|
||||
- `isValid`
|
||||
- Will be `true` if every field is valid and there is no form error (`instance.meta.allFieldsValid && !instance.meta.error`)
|
||||
- `canSubmit: Bool`
|
||||
- Will be `true` if the form is valid and not in the middle of a submission attempt (`instance.meta.isValid && !instance.meta.isSubmitting`)
|
||||
- This can be used to enable/disable a submit button for the form.
|
||||
- `...any`
|
||||
- Any other meta information can be stored here via the `field.setMeta` or `instance.setFieldMeta` functions
|
||||
|
||||
- `formContext: FormInstance`
|
||||
- This can be used to manually link `useField` instances to a parent form. This is useful if `useField` is in the same block scope as `useForm`.
|
||||
- Simply pass `formContext` to `useField` like so: `useField(fieldName, { formContext })`. That field is now linked to the form that provided the `formContext`.
|
||||
- `reset() => void`
|
||||
- This function will reset the form's state, and set `instance.values` to the `defaultValues` option
|
||||
- `setMeta((updater(previousMeta: Object) => newMeta: Object) | newMeta: Object)`
|
||||
- Use this function to update the form's `meta` value.
|
||||
- If a callback is passed, it will be given the previous value and be expected to return a new one, similar to React's `setState` callback style.
|
||||
- Unlike React's setState pattern though, if a new object is passed, it will **NOT** replace the meta object entirely. Since this is a common use case, it will be shallowly merged. If you need to replace the entire meta object, please use the functional callback style above.
|
||||
- `handleSubmit(formSubmitEvent) => void`
|
||||
- This function is used to submit the form.
|
||||
- It is automatically called when using the `Form` component returned by `useTable`.
|
||||
- If you build your form using your own `form` element, this function is required and should be set as the callback to the `form` via the `onSubmit` prop
|
||||
- `debounce(Function, wait: Int) => Promise`
|
||||
- This function can be used to debounce any function at the specified wait time. Calls to this function will be deduped per form-instance.
|
||||
- Typically, this function is used in the `validate` callback to control validation timing and flow
|
||||
- A new promise is returned for each call to this function
|
||||
- Over the course of multiple overlapping calls to this function, only the latest active promise that both fires and finishes will actually resolve or reject. In other words, if you call this function before a previous call to it resolves, all previous calls will be discarded and never resolve or reject so as to avoid unwanted side effects from stale promises. Turns out this is easier than cancelling promises :)
|
||||
- `setValues(updater(previousValues: Object) => newValues: Object) | newValues: Object`
|
||||
- Use this function to update the form's `meta` value.
|
||||
- If a callback is passed, it will be given the previous value and be expected to return a new one, similar to React's `setState` callback style.
|
||||
- If a value is passed, it **will replace the entire values** object, similar to React's `setState` callback style.
|
||||
- `runValidation() => void`
|
||||
- Use this function to manually run the form-level validation.
|
||||
- This function does not run validation for fields. If that is a requirement you have, the please file a feature request and I'll make it happen.
|
||||
- `getFieldValue(fieldPath: String) => any`
|
||||
- This function returns the `value` from the form's `instance.values` object located at the `fieldPath` string that was passed.
|
||||
- `fieldPath` is a string that supports object notation, eg. `foo[3].bar[1].baz`
|
||||
- `getFieldMeta(fieldPath: String) => Object: { error: String | null, ...any }`
|
||||
- This function returns a field's `meta` object from the form.
|
||||
- `fieldPath` is a string that supports object notation, eg. `foo[3].bar[1].baz`
|
||||
- `setFieldValue(fieldPath: String, (updater(previousValue: any) => newValue: any) | newValue: any, options: Object { isTouched: Bool } )`
|
||||
- Use this function to update a field's corresponding `value` that is stored in the `instance.values` object.
|
||||
- If a callback is passed, it will be given the previous value and be expected to return a new one, similar to React's `setState` callback style.
|
||||
- Unlike React's setState pattern though, if a new object is passed, it will **NOT** replace the meta object entirely. Since this is a common use case, it will be shallowly merged. If you need to replace the entire meta object, please use the functional callback style above.
|
||||
- Optionally, an `options` object can be passed.
|
||||
- `isTouched: Bool`
|
||||
- Defaults to `true`
|
||||
- If set to `false`, this operation will not trigger the field's `isTouched` to automatically be set to `true`
|
||||
- `setFieldMeta(fieldPath: String, (updater(previousMeta: Object) => newMeta: Object) | newMeta: Object)`
|
||||
- Use this function to update a fields `meta` value.
|
||||
- If a callback is passed, it will be given the previous value and be expected to return a new one, similar to React's `setState` callback style.
|
||||
- Unlike React's setState pattern though, if a new object is passed, it will **NOT** replace the meta object entirely. Since this is a common use case, it will be shallowly merged. If you need to replace the entire meta object, please use the functional callback style above.
|
||||
- `pushFieldValue(fieldPath: String, newValue: any)`
|
||||
- Use this function to push a new value into a field that has an array-like value.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
- `insertFieldValue(fieldPath: String, insertIndex: Int, value: any)`
|
||||
- Use this function to insert a new value into a field that has an array-like value at the specified index.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
- `removeFieldValue(fieldPath: String, removalIndex: Int)`
|
||||
- Use this function to remove a value from a field that has an array-like value at the specified index.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
- `swapFieldValues(fieldPath: String, firstIndex: Int, secondIndex: Int)`
|
||||
- Use this function to swap two values inside of a field that has an array-like value at the specified indices.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
|
||||
## `useField`
|
||||
|
||||
```js
|
||||
import { useField } from 'react-form'
|
||||
|
||||
const fieldInstance = useField(fieldPath, options)
|
||||
```
|
||||
|
||||
### Field Options
|
||||
|
||||
- `fieldPath: String`
|
||||
- **Required**
|
||||
- The path the value for this field.
|
||||
- Supports deeply nested object notation, eg. `foo[3].bar[1].baz`
|
||||
- Any integers that can be reliably detected between notation boundaries will be treated as array indices: `1`, `[1]`, `1.`, `.1.` or `.1`
|
||||
- `options` - An optional object to configure the field
|
||||
- `defaultValue: any`
|
||||
- Use `defaultValue` to set the default `value` state for the field.
|
||||
- If this object ever changes, the field meta will be reset to the new value. Thus, its important to use `React.useMemo` to make this object only change when necessary
|
||||
- `defaultError: String | undefined`
|
||||
- Use `defaultError` to set the default `error` state for the field.
|
||||
- If this object ever changes, the field meta will be reset to the new value. Thus, its important to use `React.useMemo` to make this object only change when necessary
|
||||
- `defaultIsTouched: Bool | undefined`
|
||||
- Use `defaultIsTouched` to set the default `isTouched` state for the field.
|
||||
- If this object ever changes, the field meta will be reset to the new value. Thus, its important to use `React.useMemo` to make this object only change when necessary
|
||||
- `defaultMeta: Object | undefined`
|
||||
- Use `defaultMeta` to set any additional default `meta` state for the field.
|
||||
- Unlike `defaultValue`, `defaultError` and `defaultIsTouched`, changing this object will not trigger the field meta to be updated. It is only updated when the `useField` hook mounts and `meta` for that field has not yet been initialized (`meta === undefined`)
|
||||
- `validate async (value, instance) => String | false | undefined`
|
||||
- `validate` is an asynchronous function that is called when the field becomes dirty and is given the opportunity to set/clear/update errors and/or manage general field meta
|
||||
- `instance` is the latest version of the field instance (the same instance that is returned from `useField`)
|
||||
- [See below](#validation) for more information on field validation
|
||||
- Form level validation is also available. [See `useForm`](#useform)
|
||||
- `filterValue: (value, instance) => newValue`
|
||||
- The `filterValue` function is used to manipulate new values before they are set via `field.setValue` and `instance.setFieldValue`.
|
||||
- This is useful for restricting fields to specific values, eg. min/max, length, regex replacement, disallowing invalid values (as opposed to warning about them), among limitless others.
|
||||
- `validatePristine: Bool`
|
||||
- If you want validation to run when instance.isDirty is false, you can set the `validatePristine` option to `true`
|
||||
|
||||
### Field Instance
|
||||
|
||||
An `object` with the following components, properties and methods:
|
||||
|
||||
- `form: FormInstance`
|
||||
- The root form `instance`
|
||||
- `fieldName: String`
|
||||
- The full fieldName (from the root of the form) for this field
|
||||
- `value: any`
|
||||
- The current value of this field
|
||||
- `meta: Object {}`
|
||||
- `error: String | any`
|
||||
- If an error is returned by the field's `validation` function, or if it is set programmatically, it will be stored here.
|
||||
- `isTouched: Bool`
|
||||
- Will be set to `true` if the field has been touched.
|
||||
- `...any`
|
||||
- Any other meta information can be stored here via the `field.setMeta` or `instance.setFieldMeta` functions
|
||||
- `FieldScope: ReactComponent<Provider>`
|
||||
- Optional
|
||||
- This component does not render any markup
|
||||
- It set the new base field scope to this field's `fieldpath`
|
||||
- It supplies this new base field scope, along with the form scope to any `useField` instances inside it.
|
||||
- Any `useField(fieldPath)` instances used insde of `FieldScope` will inherit this field's `fieldPath` as a parent.
|
||||
- See [Field Scoping](#field-scoping) for more information
|
||||
- `debounce(Function, wait: Int) => Promise`
|
||||
- This function can be used to debounce any function at the specified wait time. Calls to this function will be deduped per `useField` instance.
|
||||
- Typically, this function is used in the `validate` callback to control validation timing and flow
|
||||
- A new promise is returned for each call to this function
|
||||
- Over the course of multiple overlapping calls to this function, only the latest active promise that both fires and finishes will actually resolve or reject. In other words, if you call this function before a previous call to it resolves, all previous calls will be discarded and never resolve or reject so as to avoid unwanted side effects from stale promises. Turns out this is easier than cancelling promises :)
|
||||
- `runValidation() => void`
|
||||
- Use this function to manually run this field's validation.
|
||||
- This function does not run validation for fields.
|
||||
- `getInputProps(props: Object {}) => enhanced props Object`
|
||||
- `onChange: ChangeEventHandler`
|
||||
- `onBlur: FormEventHandler`
|
||||
- `...rest` - any other props that will be spread into the enhanced props
|
||||
- Use this function to set props on input elements. If set, `onChange` and `onBlur` will be wrapped to update the field `value`.
|
||||
- The enhanced props will always contain `value`, `onChange`, and `onBlur`. Any of the rest that are passed in will also be returned.
|
||||
|
||||
#### Field-Specific Methods
|
||||
|
||||
The following methods do not require the use of a `fieldPath`. This field's `fieldPath` will automatically be used.
|
||||
|
||||
- `setValue((updater(previousValue: any) => newValue: any) | newValue: any, options: Object { isTouched: Bool } )`
|
||||
- Use this function to update a field's corresponding `value` that is stored in the `instance.values` object.
|
||||
- If a callback is passed, it will be given the previous value and be expected to return a new one, similar to React's `setState` callback style.
|
||||
- Unlike React's setState pattern though, if a new object is passed, it will **NOT** replace the meta object entirely. Since this is a common use case, it will be shallowly merged. If you need to replace the entire meta object, please use the functional callback style above.
|
||||
- Optionally, an `options` object can be passed.
|
||||
- `isTouched: Bool`
|
||||
- Defaults to `true`
|
||||
- If set to `false`, this operation will not trigger the field's `isTouched` to automatically be set to `true`
|
||||
- `setMeta((updater(previousMeta: Object) => newMeta: Object) | newMeta: Object)`
|
||||
- Use this function to update a fields `meta` value.
|
||||
- If a callback is passed, it will be given the previous value and be expected to return a new one, similar to React's `setState` callback style.
|
||||
- Unlike React's setState pattern though, if a new object is passed, it will **NOT** replace the meta object entirely. Since this is a common use case, it will be shallowly merged. If you need to replace the entire meta object, please use the functional callback style above.
|
||||
- `pushValue(newValue: any)`
|
||||
- Use this function to push a new value into a field that has an array-like value.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
- `insertValue(insertIndex: Int, value: any)`
|
||||
- Use this function to insert a new value into a field that has an array-like value at the specified index.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
- `removeValue(removalIndex: Int)`
|
||||
- Use this function to remove a value from a field that has an array-like value at the specified index.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
- `swapValues(firstIndex: Int, secondIndex: Int)`
|
||||
|
||||
- Use this function to swap two values inside of a field that has an array-like value at the specified indices.
|
||||
- An error will be thrown if the field's current value is not an array.
|
||||
|
||||
#### Field-Scope Specific Methods
|
||||
|
||||
The following methods are almost exactly the same as their top-level form `instance` counterparts, except for any `fieldPath` that is passed to them will be prefixed with this field's `fieldPath`
|
||||
|
||||
For example, if our field had the `fieldPath` of `foo`, then `setFieldValue('[0]', true)` would be similar to calling `instance.setFieldValue('foo[0]', true)`
|
||||
|
||||
- `setFieldValue(subFieldPath, ...)` - [See Form Instance](#form-instance)
|
||||
- `setFieldMeta(subFieldPath, ...)` - [See Form Instance](#form-instance)
|
||||
- `pushFieldValue(subFieldPath, ...)` - [See Form Instance](#form-instance)
|
||||
- `insertFieldValue(subFieldPath, ...)` - [See Form Instance](#form-instance)
|
||||
- `removeFieldValue(subFieldPath, ...)` - [See Form Instance](#form-instance)
|
||||
- `swapFieldValues(subFieldPath, ...)` - [See Form Instance](#form-instance)
|
||||
|
||||
#### Using `useField` in the same scope as `useForm`
|
||||
|
||||
If you get into a situation where you need to use `useForm` and `useField` in the same block scope, you may see a missing form context error. This is because your `useField` usage is not inside of a `<Form>` component. To get around this error for this use case, you can pass the form's `instance.formContext` value to the `useField` options to manually link them together:
|
||||
|
||||
```js
|
||||
function App() {
|
||||
const { Form, formContext } = useForm()
|
||||
|
||||
// This field will now be manually linked to the form above
|
||||
const fieldInstance = useField('age', {
|
||||
formContext,
|
||||
})
|
||||
|
||||
return <Form>...</Form>
|
||||
}
|
||||
```
|
||||
|
||||
## `splitFormProps`
|
||||
|
||||
A utility function for filter React-Form-related props from an object.
|
||||
|
||||
```js
|
||||
import { splitFormProps } from 'react-form'
|
||||
|
||||
function TextField(props) {
|
||||
const [field, options, rest] = splitFormProps(props)
|
||||
|
||||
// options === {
|
||||
// defaultValue,
|
||||
// defaultIsTouched,
|
||||
// defaultError,
|
||||
// defaultMeta,
|
||||
// validatePristine,
|
||||
// validate,
|
||||
// onSubmit,
|
||||
// defaultValues,
|
||||
// debugForm,
|
||||
// }
|
||||
|
||||
const fieldInstance = useField(field, options)
|
||||
|
||||
return <input {...rest} />
|
||||
}
|
||||
```
|
||||
|
||||
## `useFormContext`
|
||||
|
||||
A hook for gaining access to the form state from within a `Form` component
|
||||
|
||||
```js
|
||||
import { useFormContext } from 'react-form'
|
||||
|
||||
function App() {
|
||||
const { Form } = useForm()
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Stuff />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
function Stuff() {
|
||||
const formInstance = useFormContext()
|
||||
|
||||
console.log(formInstance)
|
||||
}
|
||||
```
|
||||
58
docs/config.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"docSearch": {
|
||||
"appId": "",
|
||||
"apiKey": "",
|
||||
"indexName": "tanstack-form"
|
||||
},
|
||||
"menu": [
|
||||
{
|
||||
"framework": "react",
|
||||
"menuItems": [
|
||||
{
|
||||
"label": "Getting Started",
|
||||
"children": [
|
||||
{
|
||||
"label": "Overview",
|
||||
"to": "react/overview"
|
||||
},
|
||||
{
|
||||
"label": "Installation",
|
||||
"to": "react/installation"
|
||||
},
|
||||
{
|
||||
"label": "Quick Start",
|
||||
"to": "react/quick-start"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Guides & Concepts",
|
||||
"children": [
|
||||
{
|
||||
"label": "Important Defaults",
|
||||
"to": "react/guides/important-defaults"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Examples",
|
||||
"children": [
|
||||
{
|
||||
"label": "Simple",
|
||||
"to": "react/examples/react/simple"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "API Reference",
|
||||
"children": [
|
||||
{
|
||||
"label": "useForm",
|
||||
"to": "react/reference/useForm"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# Examples
|
||||
|
||||
- [Basic Form (Hooks Only)](https://codesandbox.io/s/react-form-demo-950ww)
|
||||
- This very basic example uses the `useForm` and `useField` hooks to create and manage a form and its values.
|
||||
- [Basic Form (With InputField)](https://codesandbox.io/s/react-form-demo-wrybd)
|
||||
- This very basic example uses the `useField` hook to create a reusable `<InputField>` component that can be used inside of a form created by `useForm`
|
||||
- [Validation Example](https://codesandbox.io/s/react-form-demo-q9mgm)
|
||||
- This example demonstrates a variety of validation strategies possible with React Form
|
||||
- [Custom Select & Multi-Select Inputs](https://codesandbox.io/s/react-form-custom-select-multi-select-inputs-q5ixs)
|
||||
- This example demonstrates how to build custom input fields, specifically a Select and Multi-Select input as examples.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Field Scoping
|
||||
|
||||
Field scoping is useful for building form inputs that don't require knowledge of the parent field name. Imagine a field component for managing some notes on a form:
|
||||
|
||||
```js
|
||||
function NotesField({ field }) {
|
||||
const fieldInstance = useField(field)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This approach would required us to define the nested `field` when we use the component:
|
||||
|
||||
```js
|
||||
function MyForm() {
|
||||
const { Form } = useForm()
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<NotesField field="notes" />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This isn't a problem for shallow forms, but if we are in a deeply nested part of a form UI, it get's more verbose:
|
||||
|
||||
```js
|
||||
function MyForm() {
|
||||
const { Form } = useForm()
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<ConfigField field="config" />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
function ConfigField({ field: parentField }) {
|
||||
return (
|
||||
<>
|
||||
<NotesField field={`${parentField}.notes`} />
|
||||
<OtherField field={`${parentField}.other`} />
|
||||
<FooField field={`${parentField}.foo`} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Instead of requiring that all deep fields be composed with their parent strings, you can use the `FieldScope` component returned by the `useField` hook to create a new field scope for any `useField` instances rendered inside of it:
|
||||
|
||||
```js
|
||||
function MyForm() {
|
||||
const { Form } = useForm()
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<ConfigField field="config" />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
function ConfigField({ field: parentField }) {
|
||||
const { FieldScope } = useField('config')
|
||||
return (
|
||||
<FieldScope>
|
||||
<NotesField field="notes" />
|
||||
<OtherField field="other" />
|
||||
<FooField field="foo" />
|
||||
</FieldScope>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -1,23 +0,0 @@
|
||||
# Installation
|
||||
|
||||
Installing and using React Form is simple. Start by installing the `react-form` dependency:
|
||||
|
||||
```bash
|
||||
$ yarn add react-form
|
||||
# or
|
||||
$ npm i react-form --save
|
||||
```
|
||||
|
||||
Once it is installed, you can import and use `react-form`'s hooks and utilities:
|
||||
|
||||
```js
|
||||
import { useForm, useField, splitFormProps } from 'react-form'
|
||||
```
|
||||
|
||||
**NOTE:** React Form does not transpile asynchronous functions or polyfill promises. If you need to target any browsers that don't support async functions or promises, you will need to transpile them with babel and/or a promise polyfill.
|
||||
|
||||
To learn how to use React Form, you should:
|
||||
|
||||
- [See a basic example]()
|
||||
- [Study a more complex example]()
|
||||
- [Get to know the API]()
|
||||
25
docs/react/comparison.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
id: comparison
|
||||
title: Comparison | TanStack Form
|
||||
---
|
||||
|
||||
> This comparison table strives to be as accurate and as unbiased as possible. If you use any of these libraries and feel the information could be improved, feel free to suggest changes (with notes or evidence of claims) using the "Edit this page on Github" link at the bottom of this page.
|
||||
|
||||
Feature/Capability Key:
|
||||
|
||||
- ✅ 1st-class, built-in, and ready to use with no added configuration or code
|
||||
- 🟡 Supported, but as an unofficial 3rd party or community library/contribution
|
||||
- 🔶 Supported and documented, but requires extra user-code to implement
|
||||
- 🛑 Not officially supported or documented.
|
||||
|
||||
| | TanStack Form |
|
||||
| -------------------- | -------------------------------------------- |
|
||||
| Github Repo / Stars | [![][stars-tanstack-form]][gh-tanstack-form] |
|
||||
| Supported Frameworks | React |
|
||||
| Bundle Size | [![][bp-tanstack-form]][bpl-tanstack-form] |
|
||||
| TODO | |
|
||||
|
||||
[bpl-tanstack-form]: https://bundlephobia.com/result?p=@tanstack/react-form
|
||||
[bp-tanstack-form]: https://badgen.net/bundlephobia/minzip/@tanstack/react-form?label=💾
|
||||
[gh-tanstack-form]: https://github.com/tannerlinsley/@tanstack/form
|
||||
[stars-tanstack-form]: https://img.shields.io/github/stars/@tanstack/form?label=%F0%9F%8C%9F
|
||||
6
docs/react/guides/important-defaults.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
id: important-defaults
|
||||
title: Important Defaults
|
||||
---
|
||||
|
||||
Out of the box, TanStack Form is configured with **aggressive but sane** defaults. **Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the user.** Keep them in mind as you continue to learn and use TanStack Form:
|
||||
49
docs/react/installation.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: installation
|
||||
title: Installation
|
||||
---
|
||||
|
||||
You can install TanStack Form via [NPM](https://npmjs.com),
|
||||
or a good ol' `<script>` via
|
||||
[unpkg.com](https://unpkg.com).
|
||||
|
||||
### NPM
|
||||
|
||||
```bash
|
||||
$ npm i @tanstack/react-form
|
||||
# or
|
||||
$ pnpm add @tanstack/react-form
|
||||
# or
|
||||
$ yarn add @tanstack/react-form
|
||||
```
|
||||
|
||||
TanStack Form is compatible with React v16.8+ and works with ReactDOM and React Native.
|
||||
|
||||
> Wanna give it a spin before you download? Try out the [simple](/form/v1/docs/examples/react/simple) or [basic](/form/v1/docs/examples/react/basic) examples!
|
||||
|
||||
### CDN
|
||||
|
||||
If you're not using a module bundler or package manager we also have a global ("UMD") build hosted on the [unpkg.com](https://unpkg.com) CDN. Simply add the following `<script>` tag to the bottom of your HTML file:
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/@tanstack/react-form@4/build/umd/index.production.js"></script>
|
||||
```
|
||||
|
||||
Once you've added this you will have access to the `window.ReactForm` object and its exports.
|
||||
|
||||
> This installation/usage requires the [React CDN script bundles](https://reactjs.org/docs/cdn-links.html) to be on the page as well.
|
||||
|
||||
### Requirements
|
||||
|
||||
TanStack Form is optimized for modern browsers. It is compatible with the following browsers config
|
||||
|
||||
```
|
||||
Chrome >= 73
|
||||
Firefox >= 78
|
||||
Edge >= 79
|
||||
Safari >= 12.0
|
||||
iOS >= 12.0
|
||||
opera >= 53
|
||||
```
|
||||
|
||||
> 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.
|
||||
22
docs/react/overview.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
id: overview
|
||||
title: Overview
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
TODO
|
||||
|
||||
## Enough talk, show me some code already!
|
||||
|
||||
In the example below, you can see React Form in its most basic and simple form being used to fetch the GitHub stats for the React Form GitHub project itself:
|
||||
|
||||
[Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-form/tree/main/examples/react/simple)
|
||||
|
||||
```tsx
|
||||
TODO
|
||||
```
|
||||
|
||||
## You talked me into it, so what now?
|
||||
|
||||
- Learn React Form at your own pace with our amazingly thorough [Walkthrough Guide](../installation) and [API Reference](../reference/useForm)
|
||||
8
docs/react/quick-start.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
id: quick-start
|
||||
title: Quick Start
|
||||
---
|
||||
|
||||
TODO
|
||||
|
||||
These concepts make up most of the core functionality of TanStack Form. The next sections of the documentation will go over each of these core concepts in great detail.
|
||||
12
docs/react/reference/useForm.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
id: useForm
|
||||
title: useForm
|
||||
---
|
||||
|
||||
```tsx
|
||||
const {} = useForm({})
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
**Returns**
|
||||
13
docs/react/typescript.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
id: typescript
|
||||
title: TypeScript
|
||||
---
|
||||
|
||||
TanStack Form is written 100% in **TypeScript** with the highest quality generics, constraints and interfaces to make sure the library and your projects are as type-safe as possible!
|
||||
|
||||
Things to keep in mind:
|
||||
|
||||
- Types currently require using TypeScript v4.1 or greater
|
||||
- Changes to types in this repository are considered **non-breaking** and are usually released as **patch** semver changes (otherwise every type enhancement would be a major version!).
|
||||
- It is **highly recommended that you lock your react-form package version to a specific patch release and upgrade with the expectation that types may be fixed or upgraded between any release**
|
||||
- The non-type-related public API of TanStack Form still follows semver very strictly.
|
||||
@@ -1,209 +0,0 @@
|
||||
# Validation
|
||||
|
||||
Validation in React Form supports a wide range of synchronous and asynchronous validation strategies for both individual fields and the entire form itself. Also included in the validation API is the ability to debounce sync and async validation attempts and even manage form and field meta manually.
|
||||
|
||||
- [When can a form be submitted?](#when-can-a-form-be-submitted)
|
||||
- [Submission Attempt Flow](#submission-attempt-flow)
|
||||
- [Synchronous Validation](#synchronous-validation)
|
||||
- [Asynchronous Validation](#asynchronous-validation)
|
||||
- [Mixed Sync + Async Validation:](#mixed-sync--async-validation)
|
||||
- [Debouncing Form Validation](#debouncing-form-validation)
|
||||
- [Sync Debouncing](#sync-debouncing)
|
||||
- [Async Debouncing](#async-debouncing)
|
||||
- [Mixed Sync/Async Debouncing](#mixed-syncasync-debouncing)
|
||||
- [Manually manage form `meta` and field `meta`](#manually-manage-form-meta-and-field-meta)
|
||||
|
||||
## When can a form be submitted?
|
||||
|
||||
A form submission can be attempted when either:
|
||||
|
||||
- The form has not been touched `!instance.meta.isTouched`
|
||||
|
||||
**OR**
|
||||
|
||||
- All fields **with a validation option**
|
||||
- Have been touched (`field.meta.isTouched`)
|
||||
- Are not validating (`!field.meta.isValidating`)
|
||||
- Do not have an error (`field.meta.error`)
|
||||
- The form has been touched `instance.meta.isTouched`
|
||||
- The form is not validating `!instance.meta.isValidating`
|
||||
- The form does not have an error `instance.meta.error`
|
||||
|
||||
To simplify handling this state, the following additional booleans are available on the `instance.meta`:
|
||||
|
||||
- `instance.meta.fieldsAreValidating`
|
||||
- `instance.meta.fieldsAreValid`
|
||||
- `instance.meta.isValid`
|
||||
- `instance.meta.canSubmit`
|
||||
|
||||
See [Form Instance](#form-instance) for more information
|
||||
|
||||
## Submission Attempt Flow
|
||||
|
||||
Every time a submission attempt is made, the following submission flow will takes place:
|
||||
|
||||
- If there are fields that have not been touched or the form has not been touched:
|
||||
- All fields will be touched (`field.meta.isTouched === true`)
|
||||
- The form is touched (`instance.meta.isTouched === true`)
|
||||
- All fields with a `validate` option that have not been touched will be validated
|
||||
- If the form `validate` option is set and has not been touched, the form will be validated
|
||||
- The submission attempt will wait for any field and form validations to resolve
|
||||
- If any field validations or the form validation throw a runtime error
|
||||
- The submission attempt will abort 🛑
|
||||
- Once all validations settle
|
||||
- A new submission will be attempted with the new post-validation state 🔁
|
||||
- If there are any field or form validation(s) errors
|
||||
- The current submission will abort 🛑
|
||||
- The form's `onSubmit` function will be called ✅
|
||||
|
||||
## Synchronous Validation
|
||||
|
||||
If you don't need to perform any async validation in your form or field, you can just return an error string directly (or `false` clear an error):
|
||||
|
||||
- If a validation function returns a `string`, the value returned will be stored in either the form's `instance.meta.error` or the field's `meta.error` value
|
||||
- If a validation function returns `false`, the error in either the form's `instance.meta.error` or the field's `meta.error` value the will be set to `null`
|
||||
- if a validation function returns `undefined`, no changes will happen
|
||||
|
||||
```js
|
||||
const options = {
|
||||
validate: value => {
|
||||
// To set an error:
|
||||
if (!somethingIsWrong) {
|
||||
return 'This form/field has a form-level error'
|
||||
}
|
||||
// To clear any errors:
|
||||
return false
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Asynchronous Validation
|
||||
|
||||
Asynchronous validation is as easy as returning a promise that resolves to the standard return types shown above in the synchronous validation example:
|
||||
|
||||
```js
|
||||
const options = {
|
||||
validate: async value => {
|
||||
const error = await validateOnServer(values)
|
||||
|
||||
if (error) {
|
||||
return error
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Mixed Sync + Async Validation:
|
||||
|
||||
You also mix both synchronous and asynchronous validation easily with this pattern as well:
|
||||
|
||||
```js
|
||||
const options = {
|
||||
validate: async value => {
|
||||
// First check for synchronous errors
|
||||
if (!values.foo || !values.bar) {
|
||||
return 'Foo and bar are required!'
|
||||
}
|
||||
|
||||
// Then return a promise that resolves any async errors
|
||||
const error = await validateOnServer(values)
|
||||
return error ? error : false
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Debouncing Form Validation
|
||||
|
||||
If you're validation is firing too often, you can debounce any stage of your validation function (sync or async) with React Form's built-in debounce utility. `instance.debounce` returns a promise that only resolves for the latest call after a given amount of time. This way, any outdated validation attempts are discarded automatically.
|
||||
|
||||
## Sync Debouncing
|
||||
|
||||
To debounce synchronous validation, return the promise from `debounce`, called with a synchronous function:
|
||||
|
||||
```js
|
||||
const options = {
|
||||
validate: (values, instance) => {
|
||||
return instance.debounce(() => {
|
||||
// Wait 1000 milliseconds before validating anything
|
||||
if (!values.foo || !values.bar) {
|
||||
return 'Foo and bar are required!'
|
||||
}
|
||||
|
||||
return false
|
||||
}, 1000)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Async Debouncing
|
||||
|
||||
To debounce asynchronous validation, return the promise from `debounce`, called with an asynchronous function:
|
||||
|
||||
```js
|
||||
const options = {
|
||||
validate: async (values, instance) => {
|
||||
return instance.debounce(async () => {
|
||||
// Wait 2 seconds before validating on the server
|
||||
const error = await validateOnServer(values)
|
||||
return error ? error : false
|
||||
}, 2000)
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Mixed Sync/Async Debouncing
|
||||
|
||||
Again, you can mix both sync/async and immediate/debounced behavior however you'd like!
|
||||
|
||||
> **Pro Tip**: This is my favorite and recommended approach to mixed validation.
|
||||
|
||||
```js
|
||||
const options = {
|
||||
validate: async (values, instance) => {
|
||||
// Check for synchronous errors immediately without debouncing them
|
||||
if (!values.foo || !values.bar) {
|
||||
return 'Foo and bar are required!'
|
||||
}
|
||||
|
||||
// Then, if sync validation passes
|
||||
return instance.debounce(() => {
|
||||
// Wait 2 seconds before validating on the server
|
||||
const error = await validateOnServer(values)
|
||||
return error ? error : false
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manually manage form `meta` and field `meta`
|
||||
|
||||
Returning an error string or false from validate is simply shorthand for setting/unsetting the `error` property on either the form's `instance.meta` object or a field's `meta` object. If you don't want to set an error and would rather set a success or warning message, you can use the `instance.setMeta` (for form-level validation) or the `instance.setMeta` function (for field-level validation). More than just the error field can be set/used on both the `instance.meta` object and each individual field's `meta` object. You could use this meta information for success messages, warnings, or any other information about a field. Only the `error` and `isTouched` meta properties are used internally by React Form to determine form validity.
|
||||
|
||||
```js
|
||||
const options = {
|
||||
validate: async (values, instance) => {
|
||||
const serverError = await validateOnServer(values)
|
||||
|
||||
if (serverError) {
|
||||
setMeta({
|
||||
error: serverError,
|
||||
message: null,
|
||||
errorStack: serverError.stack,
|
||||
})
|
||||
} else {
|
||||
setMeta({
|
||||
error: null,
|
||||
message: 'The form is good to be submitted!',
|
||||
errorStack: null,
|
||||
})
|
||||
}
|
||||
|
||||
// Make sure this function returns undefined if you are handling
|
||||
// meta manually.
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Using this approach, you can avoid having to compose deeply nested field names!
|
||||
7
examples/react/simple/.eslintrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["react-app", "prettier"],
|
||||
"rules": {
|
||||
// "eqeqeq": 0,
|
||||
// "jsx-a11y/anchor-is-valid": 0
|
||||
}
|
||||
}
|
||||
27
examples/react/simple/.gitignore
vendored
Normal file
@@ -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*
|
||||
1
examples/react/simple/.prettierrc
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
6
examples/react/simple/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Example
|
||||
|
||||
To run this example:
|
||||
|
||||
- `npm install`
|
||||
- `npm run dev`
|
||||
16
examples/react/simple/index.html
Normal file
@@ -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.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
32
examples/react/simple/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@tanstack/form-example-react-simple",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.jsx",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"@tanstack/react-form": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^2.0.0",
|
||||
"vite": "^3.0.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
13
examples/react/simple/public/emblem-light.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="190px" height="190px" viewBox="0 0 190 190" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>emblem-light</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<path d="M39.7239712,61.3436237 C36.631224,46.362877 35.9675112,34.8727722 37.9666331,26.5293551 C39.1555965,21.5671678 41.3293088,17.5190846 44.6346064,14.5984631 C48.1241394,11.5150478 52.5360327,10.0020122 57.493257,10.0020122 C65.6712013,10.0020122 74.2682602,13.7273214 83.4557246,20.8044264 C87.2031203,23.6910458 91.0924366,27.170411 95.1316515,31.2444746 C95.4531404,30.8310265 95.8165416,30.4410453 96.2214301,30.0806152 C107.64098,19.9149716 117.255245,13.5989272 125.478408,11.1636507 C130.367899,9.715636 134.958526,9.57768202 139.138936,10.983031 C143.551631,12.4664684 147.06766,15.5329489 149.548314,19.8281091 C153.642288,26.9166735 154.721918,36.2310983 153.195595,47.7320243 C152.573451,52.4199112 151.50985,57.5263831 150.007094,63.0593153 C150.574045,63.1277086 151.142416,63.2532808 151.705041,63.4395297 C166.193932,68.2358678 176.453582,73.3937462 182.665021,79.2882839 C186.360669,82.7953831 188.773972,86.6998434 189.646365,91.0218204 C190.567176,95.5836746 189.669313,100.159332 187.191548,104.451297 C183.105211,111.529614 175.591643,117.11221 164.887587,121.534031 C160.589552,123.309539 155.726579,124.917559 150.293259,126.363748 C150.541176,126.92292 150.733521,127.516759 150.862138,128.139758 C153.954886,143.120505 154.618598,154.61061 152.619477,162.954027 C151.430513,167.916214 149.256801,171.964297 145.951503,174.884919 C142.46197,177.968334 138.050077,179.48137 133.092853,179.48137 C124.914908,179.48137 116.31785,175.756061 107.130385,168.678956 C103.343104,165.761613 99.4108655,162.238839 95.3254337,158.108619 C94.9050753,158.765474 94.3889681,159.376011 93.7785699,159.919385 C82.3590198,170.085028 72.744755,176.401073 64.5215915,178.836349 C59.6321009,180.284364 55.0414736,180.422318 50.8610636,179.016969 C46.4483686,177.533532 42.9323404,174.467051 40.4516862,170.171891 C36.3577116,163.083327 35.2780823,153.768902 36.8044053,142.267976 C37.449038,137.410634 38.56762,132.103898 40.1575891,126.339009 C39.5361041,126.276104 38.9120754,126.144816 38.2949591,125.940529 C23.8060684,121.144191 13.5464184,115.986312 7.33497892,110.091775 C3.63933121,106.584675 1.22602752,102.680215 0.353635235,98.3582381 C-0.567176333,93.7963839 0.330686581,89.2207269 2.80845236,84.9287618 C6.89478863,77.8504443 14.4083565,72.2678481 25.1124133,67.8460273 C29.5385143,66.0176154 34.5637208,64.366822 40.1939394,62.8874674 C39.9933393,62.3969171 39.8349374,61.8811235 39.7239712,61.3436237 Z" fill="#002C4B" fill-rule="nonzero" transform="translate(95.000000, 95.000000) scale(-1, 1) translate(-95.000000, -95.000000) "></path>
|
||||
<path d="M80.3968824,64 L109.608177,64 C111.399254,64 113.053521,64.958025 113.944933,66.5115174 L128.577138,92.0115174 C129.461464,93.5526583 129.461464,95.4473417 128.577138,96.9884826 L113.944933,122.488483 C113.053521,124.041975 111.399254,125 109.608177,125 L80.3968824,125 C78.6058059,125 76.9515387,124.041975 76.0601262,122.488483 L61.4279211,96.9884826 C60.543596,95.4473417 60.543596,93.5526583 61.4279211,92.0115174 L76.0601262,66.5115174 C76.9515387,64.958025 78.6058059,64 80.3968824,64 Z M105.987827,70.2765273 C107.779849,70.2765273 109.434839,71.2355558 110.325899,72.7903404 L121.343038,92.0138131 C122.225607,93.5537825 122.225607,95.4462175 121.343038,96.9861869 L110.325899,116.20966 C109.434839,117.764444 107.779849,118.723473 105.987827,118.723473 L84.0172329,118.723473 C82.2252106,118.723473 80.5702207,117.764444 79.6791602,116.20966 L68.6620219,96.9861869 C67.7794521,95.4462175 67.7794521,93.5537825 68.6620219,92.0138131 L79.6791602,72.7903404 C80.5702207,71.2355558 82.2252106,70.2765273 84.0172329,70.2765273 L105.987827,70.2765273 Z M102.080648,77.1414791 L87.9244113,77.1414791 C86.1342282,77.1414791 84.4806439,78.0985567 83.5888998,79.6508285 L83.5888998,79.6508285 L76.4892166,92.0093494 C75.6032319,93.5515958 75.6032319,95.4484042 76.4892166,96.9906506 L76.4892166,96.9906506 L83.5888998,109.349172 C84.4806439,110.901443 86.1342282,111.858521 87.9244113,111.858521 L87.9244113,111.858521 L102.080648,111.858521 C103.870831,111.858521 105.524416,110.901443 106.41616,109.349172 L106.41616,109.349172 L113.515843,96.9906506 C114.401828,95.4484042 114.401828,93.5515958 113.515843,92.0093494 L113.515843,92.0093494 L106.41616,79.6508285 C105.524416,78.0985567 103.870831,77.1414791 102.080648,77.1414791 L102.080648,77.1414791 Z M98.3191856,83.7122186 C100.108028,83.7122186 101.760587,84.6678753 102.652827,86.2183156 L105.983552,92.0060969 C106.87203,93.5500005 106.87203,95.4499995 105.983552,96.9939031 L102.652827,102.781684 C101.760587,104.332125 100.108028,105.287781 98.3191856,105.287781 L91.685874,105.287781 C89.8970316,105.287781 88.2444725,104.332125 87.3522326,102.781684 L84.021508,96.9939031 C83.1330298,95.4499995 83.1330298,93.5500005 84.021508,92.0060969 L87.3522326,86.2183156 C88.2444725,84.6678753 89.8970316,83.7122186 91.685874,83.7122186 L98.3191856,83.7122186 Z M95.0037937,90.1848875 C93.459294,90.1848875 92.0343817,91.0072828 91.2630046,92.3424437 C90.4917325,93.6774232 90.4917325,95.3225768 91.2630046,96.6575563 C92.0343817,97.9927172 93.459294,98.8151125 95.0012659,98.8151125 L95.0012659,98.8151125 C96.5457656,98.8151125 97.9706779,97.9927172 98.7420549,96.6575563 C99.5133271,95.3225768 99.5133271,93.6774232 98.7420549,92.3424437 C97.9706779,91.0072828 96.5457656,90.1848875 95.0037937,90.1848875 L95.0037937,90.1848875 Z M60,94.5009646 L67.7677636,94.5009646" fill="#FFD94C"></path>
|
||||
<path d="M54.8601729,108.357758 C56.1715224,107.608286 57.8360246,108.074601 58.5779424,109.399303 L58.5779424,109.399303 L59.0525843,110.24352 C62.8563392,116.982993 66.8190116,123.380176 70.9406016,129.435068 C75.8078808,136.585427 81.28184,143.82411 87.3624792,151.151115 C88.3168778,152.30114 88.1849437,154.011176 87.065686,154.997937 L87.065686,154.997937 L86.4542085,155.534625 C66.3465389,173.103314 53.2778188,176.612552 47.2480482,166.062341 C41.3500652,155.742717 43.4844915,136.982888 53.6513274,109.782853 C53.876818,109.179582 54.3045861,108.675291 54.8601729,108.357758 Z M140.534177,129.041504 C141.986131,128.785177 143.375496,129.742138 143.65963,131.194242 L143.65963,131.194242 L143.812815,131.986376 C148.782365,157.995459 145.283348,171 133.315764,171 C121.609745,171 106.708724,159.909007 88.6127018,137.727022 C88.2113495,137.235047 87.9945723,136.617371 88,135.981509 C88.013158,134.480686 89.2357854,133.274651 90.730918,133.287756 L90.730918,133.287756 L91.6846544,133.294531 C99.3056979,133.335994 106.714387,133.071591 113.910723,132.501323 C122.409039,131.82788 131.283523,130.674607 140.534177,129.041504 Z M147.408726,73.8119663 C147.932139,72.4026903 149.508386,71.6634537 150.954581,72.149012 L150.954581,72.149012 L151.742552,72.4154854 C177.583763,81.217922 187.402356,90.8916805 181.198332,101.436761 C175.129904,111.751366 157.484347,119.260339 128.26166,123.963678 C127.613529,124.067994 126.948643,123.945969 126.382735,123.618843 C125.047025,122.846729 124.602046,121.158214 125.388848,119.847438 L125.388848,119.847438 L125.889328,119.0105 C129.877183,112.31633 133.481358,105.654262 136.701854,99.0242957 C140.50501,91.1948179 144.073967,82.7907081 147.408726,73.8119663 Z M61.7383398,66.0363218 C62.3864708,65.9320063 63.0513565,66.0540315 63.6172646,66.3811573 C64.9529754,67.153271 65.3979538,68.8417862 64.6111517,70.1525615 L64.6111517,70.1525615 L64.1106718,70.9895001 C60.1228168,77.6836699 56.5186416,84.3457379 53.2981462,90.9757043 C49.49499,98.8051821 45.9260328,107.209292 42.5912744,116.188034 C42.0678608,117.59731 40.4916142,118.336546 39.045419,117.850988 L39.045419,117.850988 L38.2574475,117.584515 C12.4162372,108.782078 2.59764398,99.1083195 8.80166786,88.5632391 C14.8700957,78.2486335 32.515653,70.7396611 61.7383398,66.0363218 Z M103.545792,34.4653746 C123.653461,16.8966864 136.722181,13.3874478 142.751952,23.9376587 C148.649935,34.2572826 146.515508,53.0171122 136.348673,80.2171474 C136.123182,80.8204179 135.695414,81.324709 135.139827,81.6422422 C133.828478,82.3917144 132.163975,81.9253986 131.422058,80.6006966 L131.422058,80.6006966 L130.947416,79.7564798 C127.143661,73.0170065 123.180988,66.6198239 119.059398,60.564932 C114.192119,53.4145727 108.71816,46.1758903 102.637521,38.8488847 C101.683122,37.6988602 101.815056,35.9888243 102.934314,35.0020629 L102.934314,35.0020629 Z M57.6842361,18 C69.3902551,18 84.2912758,29.0909926 102.387298,51.2729777 C102.788651,51.7649527 103.005428,52.3826288 103,53.0184911 C102.986842,54.5193144 101.764215,55.7253489 100.269082,55.7122445 L100.269082,55.7122445 L99.3153456,55.7054689 C91.6943021,55.6640063 84.2856126,55.9284091 77.0892772,56.4986773 C68.5909612,57.17212 59.7164767,58.325393 50.4658235,59.9584962 C49.0138691,60.2148231 47.6245044,59.2578618 47.3403697,57.805758 L47.3403697,57.805758 L47.1871846,57.0136235 C42.2176347,31.0045412 45.7166519,18 57.6842361,18 Z" fill="#FF4154"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
9
examples/react/simple/src/index.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
|
||||
export default function App() {}
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
ReactDOM.createRoot(rootElement).render(<App />);
|
||||
43
jest-preset.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const path = require('path')
|
||||
const { lstatSync, readdirSync } = require('fs')
|
||||
|
||||
// get listing of packages in the mono repo
|
||||
const basePath = path.resolve(__dirname, 'packages')
|
||||
const packages = readdirSync(basePath)
|
||||
.filter((name) => {
|
||||
return lstatSync(path.join(basePath, name)).isDirectory()
|
||||
})
|
||||
.sort((a, b) => b.length - a.length)
|
||||
|
||||
const { namespace } = require('./package.json')
|
||||
|
||||
const moduleNameMapper = {
|
||||
...packages.reduce(
|
||||
(acc, name) => ({
|
||||
...acc,
|
||||
[`${namespace}/${name}(.*)$`]: `<rootDir>/../../packages/./${name}/src/$1`,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
collectCoverage: true,
|
||||
coverageReporters: ['json', 'lcov', 'text', 'clover', 'text-summary'],
|
||||
testMatch: ['<rootDir>/**/src/**/*.test.[jt]s?(x)'],
|
||||
transform: { '^.+\\.(ts|tsx)$': 'ts-jest' },
|
||||
clearMocks: true,
|
||||
testEnvironment: 'jsdom',
|
||||
snapshotFormat: {
|
||||
printBasicPrototype: false,
|
||||
},
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
isolatedModules: true,
|
||||
diagnostics: {
|
||||
exclude: ['**'],
|
||||
},
|
||||
},
|
||||
},
|
||||
moduleNameMapper,
|
||||
}
|
||||
13
media/emblem-light.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="190px" height="190px" viewBox="0 0 190 190" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>emblem-light</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<path d="M39.7239712,61.3436237 C36.631224,46.362877 35.9675112,34.8727722 37.9666331,26.5293551 C39.1555965,21.5671678 41.3293088,17.5190846 44.6346064,14.5984631 C48.1241394,11.5150478 52.5360327,10.0020122 57.493257,10.0020122 C65.6712013,10.0020122 74.2682602,13.7273214 83.4557246,20.8044264 C87.2031203,23.6910458 91.0924366,27.170411 95.1316515,31.2444746 C95.4531404,30.8310265 95.8165416,30.4410453 96.2214301,30.0806152 C107.64098,19.9149716 117.255245,13.5989272 125.478408,11.1636507 C130.367899,9.715636 134.958526,9.57768202 139.138936,10.983031 C143.551631,12.4664684 147.06766,15.5329489 149.548314,19.8281091 C153.642288,26.9166735 154.721918,36.2310983 153.195595,47.7320243 C152.573451,52.4199112 151.50985,57.5263831 150.007094,63.0593153 C150.574045,63.1277086 151.142416,63.2532808 151.705041,63.4395297 C166.193932,68.2358678 176.453582,73.3937462 182.665021,79.2882839 C186.360669,82.7953831 188.773972,86.6998434 189.646365,91.0218204 C190.567176,95.5836746 189.669313,100.159332 187.191548,104.451297 C183.105211,111.529614 175.591643,117.11221 164.887587,121.534031 C160.589552,123.309539 155.726579,124.917559 150.293259,126.363748 C150.541176,126.92292 150.733521,127.516759 150.862138,128.139758 C153.954886,143.120505 154.618598,154.61061 152.619477,162.954027 C151.430513,167.916214 149.256801,171.964297 145.951503,174.884919 C142.46197,177.968334 138.050077,179.48137 133.092853,179.48137 C124.914908,179.48137 116.31785,175.756061 107.130385,168.678956 C103.343104,165.761613 99.4108655,162.238839 95.3254337,158.108619 C94.9050753,158.765474 94.3889681,159.376011 93.7785699,159.919385 C82.3590198,170.085028 72.744755,176.401073 64.5215915,178.836349 C59.6321009,180.284364 55.0414736,180.422318 50.8610636,179.016969 C46.4483686,177.533532 42.9323404,174.467051 40.4516862,170.171891 C36.3577116,163.083327 35.2780823,153.768902 36.8044053,142.267976 C37.449038,137.410634 38.56762,132.103898 40.1575891,126.339009 C39.5361041,126.276104 38.9120754,126.144816 38.2949591,125.940529 C23.8060684,121.144191 13.5464184,115.986312 7.33497892,110.091775 C3.63933121,106.584675 1.22602752,102.680215 0.353635235,98.3582381 C-0.567176333,93.7963839 0.330686581,89.2207269 2.80845236,84.9287618 C6.89478863,77.8504443 14.4083565,72.2678481 25.1124133,67.8460273 C29.5385143,66.0176154 34.5637208,64.366822 40.1939394,62.8874674 C39.9933393,62.3969171 39.8349374,61.8811235 39.7239712,61.3436237 Z" fill="#002C4B" fill-rule="nonzero" transform="translate(95.000000, 95.000000) scale(-1, 1) translate(-95.000000, -95.000000) "></path>
|
||||
<path d="M80.3968824,64 L109.608177,64 C111.399254,64 113.053521,64.958025 113.944933,66.5115174 L128.577138,92.0115174 C129.461464,93.5526583 129.461464,95.4473417 128.577138,96.9884826 L113.944933,122.488483 C113.053521,124.041975 111.399254,125 109.608177,125 L80.3968824,125 C78.6058059,125 76.9515387,124.041975 76.0601262,122.488483 L61.4279211,96.9884826 C60.543596,95.4473417 60.543596,93.5526583 61.4279211,92.0115174 L76.0601262,66.5115174 C76.9515387,64.958025 78.6058059,64 80.3968824,64 Z M105.987827,70.2765273 C107.779849,70.2765273 109.434839,71.2355558 110.325899,72.7903404 L121.343038,92.0138131 C122.225607,93.5537825 122.225607,95.4462175 121.343038,96.9861869 L110.325899,116.20966 C109.434839,117.764444 107.779849,118.723473 105.987827,118.723473 L84.0172329,118.723473 C82.2252106,118.723473 80.5702207,117.764444 79.6791602,116.20966 L68.6620219,96.9861869 C67.7794521,95.4462175 67.7794521,93.5537825 68.6620219,92.0138131 L79.6791602,72.7903404 C80.5702207,71.2355558 82.2252106,70.2765273 84.0172329,70.2765273 L105.987827,70.2765273 Z M102.080648,77.1414791 L87.9244113,77.1414791 C86.1342282,77.1414791 84.4806439,78.0985567 83.5888998,79.6508285 L83.5888998,79.6508285 L76.4892166,92.0093494 C75.6032319,93.5515958 75.6032319,95.4484042 76.4892166,96.9906506 L76.4892166,96.9906506 L83.5888998,109.349172 C84.4806439,110.901443 86.1342282,111.858521 87.9244113,111.858521 L87.9244113,111.858521 L102.080648,111.858521 C103.870831,111.858521 105.524416,110.901443 106.41616,109.349172 L106.41616,109.349172 L113.515843,96.9906506 C114.401828,95.4484042 114.401828,93.5515958 113.515843,92.0093494 L113.515843,92.0093494 L106.41616,79.6508285 C105.524416,78.0985567 103.870831,77.1414791 102.080648,77.1414791 L102.080648,77.1414791 Z M98.3191856,83.7122186 C100.108028,83.7122186 101.760587,84.6678753 102.652827,86.2183156 L105.983552,92.0060969 C106.87203,93.5500005 106.87203,95.4499995 105.983552,96.9939031 L102.652827,102.781684 C101.760587,104.332125 100.108028,105.287781 98.3191856,105.287781 L91.685874,105.287781 C89.8970316,105.287781 88.2444725,104.332125 87.3522326,102.781684 L84.021508,96.9939031 C83.1330298,95.4499995 83.1330298,93.5500005 84.021508,92.0060969 L87.3522326,86.2183156 C88.2444725,84.6678753 89.8970316,83.7122186 91.685874,83.7122186 L98.3191856,83.7122186 Z M95.0037937,90.1848875 C93.459294,90.1848875 92.0343817,91.0072828 91.2630046,92.3424437 C90.4917325,93.6774232 90.4917325,95.3225768 91.2630046,96.6575563 C92.0343817,97.9927172 93.459294,98.8151125 95.0012659,98.8151125 L95.0012659,98.8151125 C96.5457656,98.8151125 97.9706779,97.9927172 98.7420549,96.6575563 C99.5133271,95.3225768 99.5133271,93.6774232 98.7420549,92.3424437 C97.9706779,91.0072828 96.5457656,90.1848875 95.0037937,90.1848875 L95.0037937,90.1848875 Z M60,94.5009646 L67.7677636,94.5009646" fill="#FFD94C"></path>
|
||||
<path d="M54.8601729,108.357758 C56.1715224,107.608286 57.8360246,108.074601 58.5779424,109.399303 L58.5779424,109.399303 L59.0525843,110.24352 C62.8563392,116.982993 66.8190116,123.380176 70.9406016,129.435068 C75.8078808,136.585427 81.28184,143.82411 87.3624792,151.151115 C88.3168778,152.30114 88.1849437,154.011176 87.065686,154.997937 L87.065686,154.997937 L86.4542085,155.534625 C66.3465389,173.103314 53.2778188,176.612552 47.2480482,166.062341 C41.3500652,155.742717 43.4844915,136.982888 53.6513274,109.782853 C53.876818,109.179582 54.3045861,108.675291 54.8601729,108.357758 Z M140.534177,129.041504 C141.986131,128.785177 143.375496,129.742138 143.65963,131.194242 L143.65963,131.194242 L143.812815,131.986376 C148.782365,157.995459 145.283348,171 133.315764,171 C121.609745,171 106.708724,159.909007 88.6127018,137.727022 C88.2113495,137.235047 87.9945723,136.617371 88,135.981509 C88.013158,134.480686 89.2357854,133.274651 90.730918,133.287756 L90.730918,133.287756 L91.6846544,133.294531 C99.3056979,133.335994 106.714387,133.071591 113.910723,132.501323 C122.409039,131.82788 131.283523,130.674607 140.534177,129.041504 Z M147.408726,73.8119663 C147.932139,72.4026903 149.508386,71.6634537 150.954581,72.149012 L150.954581,72.149012 L151.742552,72.4154854 C177.583763,81.217922 187.402356,90.8916805 181.198332,101.436761 C175.129904,111.751366 157.484347,119.260339 128.26166,123.963678 C127.613529,124.067994 126.948643,123.945969 126.382735,123.618843 C125.047025,122.846729 124.602046,121.158214 125.388848,119.847438 L125.388848,119.847438 L125.889328,119.0105 C129.877183,112.31633 133.481358,105.654262 136.701854,99.0242957 C140.50501,91.1948179 144.073967,82.7907081 147.408726,73.8119663 Z M61.7383398,66.0363218 C62.3864708,65.9320063 63.0513565,66.0540315 63.6172646,66.3811573 C64.9529754,67.153271 65.3979538,68.8417862 64.6111517,70.1525615 L64.6111517,70.1525615 L64.1106718,70.9895001 C60.1228168,77.6836699 56.5186416,84.3457379 53.2981462,90.9757043 C49.49499,98.8051821 45.9260328,107.209292 42.5912744,116.188034 C42.0678608,117.59731 40.4916142,118.336546 39.045419,117.850988 L39.045419,117.850988 L38.2574475,117.584515 C12.4162372,108.782078 2.59764398,99.1083195 8.80166786,88.5632391 C14.8700957,78.2486335 32.515653,70.7396611 61.7383398,66.0363218 Z M103.545792,34.4653746 C123.653461,16.8966864 136.722181,13.3874478 142.751952,23.9376587 C148.649935,34.2572826 146.515508,53.0171122 136.348673,80.2171474 C136.123182,80.8204179 135.695414,81.324709 135.139827,81.6422422 C133.828478,82.3917144 132.163975,81.9253986 131.422058,80.6006966 L131.422058,80.6006966 L130.947416,79.7564798 C127.143661,73.0170065 123.180988,66.6198239 119.059398,60.564932 C114.192119,53.4145727 108.71816,46.1758903 102.637521,38.8488847 C101.683122,37.6988602 101.815056,35.9888243 102.934314,35.0020629 L102.934314,35.0020629 Z M57.6842361,18 C69.3902551,18 84.2912758,29.0909926 102.387298,51.2729777 C102.788651,51.7649527 103.005428,52.3826288 103,53.0184911 C102.986842,54.5193144 101.764215,55.7253489 100.269082,55.7122445 L100.269082,55.7122445 L99.3153456,55.7054689 C91.6943021,55.6640063 84.2856126,55.9284091 77.0892772,56.4986773 C68.5909612,57.17212 59.7164767,58.325393 50.4658235,59.9584962 C49.0138691,60.2148231 47.6245044,59.2578618 47.3403697,57.805758 L47.3403697,57.805758 L47.1871846,57.0136235 C42.2176347,31.0045412 45.7166519,18 57.6842361,18 Z" fill="#FF4154"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
BIN
media/header.png
|
Before Width: | Height: | Size: 30 KiB |
BIN
media/logo-light.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
media/logo.png
|
Before Width: | Height: | Size: 101 KiB |
BIN
media/repo-dark.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
media/repo-header.png
Normal file
|
After Width: | Height: | Size: 267 KiB |
81
nx-old.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"tasksRunnerOptions": {
|
||||
"default": {
|
||||
"runner": "@nrwl/nx-cloud",
|
||||
"options": {
|
||||
"cacheableOperations": [
|
||||
"test:lib",
|
||||
"test:eslint",
|
||||
"test:types",
|
||||
"build:types",
|
||||
"build",
|
||||
"rollup"
|
||||
],
|
||||
"accessToken": "ZDdkNDA4MGEtYjNmYi00MWI4LWE1N2QtYTdlNmYxMGJlZWM2fHJlYWQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultBase": "main",
|
||||
"pluginsConfig": {
|
||||
"@nrwl/js": {
|
||||
"analyzeSourceFiles": false
|
||||
}
|
||||
},
|
||||
"namedInputs": {
|
||||
"globalBuildAffectingConfig": [
|
||||
"{workspaceRoot}/babel.config.js",
|
||||
"{workspaceRoot}/rollup.config.js",
|
||||
"{workspaceRoot}/rollup.config.ts",
|
||||
"{workspaceRoot}/tsconfig.json",
|
||||
"{workspaceRoot}/tsconfig.base.json"
|
||||
],
|
||||
"globalNonBuildAffectingConfig": [
|
||||
"{workspaceRoot}/.eslintrc",
|
||||
"{workspaceRoot}/jest-preset.js"
|
||||
],
|
||||
"default": [
|
||||
"{projectRoot}/**/*",
|
||||
"globalBuildAffectingConfig",
|
||||
"globalNonBuildAffectingConfig",
|
||||
"!{projectRoot}/**/*.md",
|
||||
"!{projectRoot}/**/build/**/*"
|
||||
],
|
||||
"public": [
|
||||
"default",
|
||||
"!{workspaceRoot}/.eslintrc",
|
||||
"!{workspaceRoot}/jest-preset.js",
|
||||
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
|
||||
"!{projectRoot}/.eslintrc",
|
||||
"!{projectRoot}/jest.config.js"
|
||||
]
|
||||
},
|
||||
"targetDefaults": {
|
||||
"test:lib": {
|
||||
"outputs": ["{projectRoot}/coverage"],
|
||||
"inputs": ["default", "^public"]
|
||||
},
|
||||
"test:eslint": {
|
||||
"inputs": ["default", "^public"]
|
||||
},
|
||||
"test:types": {
|
||||
"outputs": [
|
||||
"{projectRoot}/build/**/*.d.ts",
|
||||
"{projectRoot}/build/.tsbuildinfo"
|
||||
],
|
||||
"inputs": ["default", "^public"],
|
||||
"dependsOn": ["^test:types"]
|
||||
},
|
||||
"build:types": {
|
||||
"outputs": [
|
||||
"{projectRoot}/build/**/*.d.ts",
|
||||
"{projectRoot}/build/.tsbuildinfo"
|
||||
],
|
||||
"inputs": ["default", "^public"],
|
||||
"dependsOn": ["^build:types"]
|
||||
},
|
||||
"test:build": {
|
||||
"dependsOn": ["build"],
|
||||
"inputs": ["^public"]
|
||||
}
|
||||
}
|
||||
}
|
||||
165
package.json
@@ -1,59 +1,116 @@
|
||||
{
|
||||
"name": "react-form",
|
||||
"version": "4.0.1",
|
||||
"description": "⚛️ 💼 React hooks for managing form state and lifecycle",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": "tannerlinsley/react-form",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
"name": "form",
|
||||
"repository": "https://github.com/tanstack/form.git",
|
||||
"scripts": {
|
||||
"test": "cross-env CI=1 react-scripts test --env=jsdom",
|
||||
"test:watch": "react-scripts test --env=jsdom",
|
||||
"build": "rollup -c",
|
||||
"start": "rollup -c -w",
|
||||
"prepare": "yarn build",
|
||||
"release": "yarn publish",
|
||||
"releaseNext": "yarn publish --tag next",
|
||||
"format": "prettier {src,src/**}/*.{md,js,jsx,tsx} --write"
|
||||
"clean": "pnpm --filter \"./packages/**\" run clean",
|
||||
"preinstall": "node -e \"if(process.env.CI == 'true') {console.log('Skipping preinstall...'); process.exit(1)}\" || npx -y only-allow pnpm",
|
||||
"install:csb": "pnpm install --frozen-lockfile",
|
||||
"test": "pnpm run test:ci",
|
||||
"test:ci": "nx affected --targets=test:lib,test:types,test:eslint,test:format --parallel=5",
|
||||
"test:react:17": "nx affected --target=test:lib --parallel=5",
|
||||
"test:eslint": "nx affected --target=test:eslint --parallel=5",
|
||||
"test:format": "pnpm run prettier --check",
|
||||
"test:lib": "nx affected --target=test:lib --parallel=5",
|
||||
"test:lib:dev": "pnpm --filter \"./packages/**\" run test:lib:dev",
|
||||
"test:build": "nx run-many --target=test:build --projects=root",
|
||||
"test:types": "nx affected --target=test:types --parallel=5",
|
||||
"build": "nx run-many --target=build --projects=root",
|
||||
"build:types": "nx affected --target=build:types --parallel=5",
|
||||
"watch": "concurrently --kill-others \"rollup --config rollup.config.js -w\" \"pnpm run build:types --watch\"",
|
||||
"dev": "pnpm run watch",
|
||||
"prettier": "prettier --plugin-search-dir . \"{packages,examples}/**/src/**/*.{md,js,jsx,ts,tsx,json,vue,svelte}\"",
|
||||
"prettier:write": "pnpm run prettier --write",
|
||||
"cipublish": "ts-node scripts/publish.ts",
|
||||
"validatePackages": "ts-node scripts/validate-packages.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.5.4",
|
||||
"react": "^16.8.3 || ^17.0.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@svgr/rollup": "^4.3.0",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "9.x",
|
||||
"cross-env": "^5.1.4",
|
||||
"eslint": "5.x",
|
||||
"eslint-config-prettier": "^6.3.0",
|
||||
"eslint-config-react-app": "^5.0.1",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-config-standard-react": "^7.0.2",
|
||||
"eslint-plugin-flowtype": "2.x",
|
||||
"eslint-plugin-import": "2.x",
|
||||
"eslint-plugin-jsx-a11y": "6.x",
|
||||
"eslint-plugin-node": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "1.5.0",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"prettier": "^1.18.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"rollup": "^1.12.4",
|
||||
"rollup-plugin-babel": "^4.3.2",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup-plugin-node-resolve": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"nx": {
|
||||
"includedScripts": [
|
||||
"test:format",
|
||||
"test:build"
|
||||
]
|
||||
},
|
||||
"namespace": "@tanstack",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@commitlint/parse": "^16.2.1",
|
||||
"@faker-js/faker": "^6.3.1",
|
||||
"@nrwl/nx-cloud": "^16.0.2",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-commonjs": "22.0.1",
|
||||
"@rollup/plugin-node-resolve": "^13.2.1",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/react-17": "npm:@testing-library/react@12.1.4",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/luxon": "^2.3.1",
|
||||
"@types/node": "^17.0.25",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/semver": "^7.3.13",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.41.0",
|
||||
"axios": "^0.26.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^27.5.1",
|
||||
"babel-plugin-transform-async-to-promises": "^0.8.18",
|
||||
"babel-preset-solid": "^1.5.4",
|
||||
"bundlewatch": "^0.3.2",
|
||||
"chalk": "^4.1.2",
|
||||
"concurrently": "^7.1.0",
|
||||
"current-git-branch": "^1.1.0",
|
||||
"eslint": "7.x",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-config-react-app": "^5.2.1",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-config-standard-react": "^9.2.0",
|
||||
"eslint-import-resolver-typescript": "^2.7.1",
|
||||
"eslint-plugin-flowtype": "5.x",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "6.x",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"git-log-parser": "^1.2.0",
|
||||
"jest": "^27.5.1",
|
||||
"jsonfile": "^6.1.0",
|
||||
"luxon": "^2.3.2",
|
||||
"nx": "^15.8.6",
|
||||
"prettier": "^2.6.2",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"react": "^18.2.0",
|
||||
"react-17": "npm:react@^17.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom-17": "npm:react-dom@^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup-plugin-preserve-directives": "0.1.0",
|
||||
"rollup-plugin-size": "^0.2.2",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"semver": "^7.3.8",
|
||||
"solid-js": "^1.5.7",
|
||||
"solid-testing-library": "^0.3.0",
|
||||
"stream-to-array": "^2.3.0",
|
||||
"ts-jest": "^27.1.1",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vue": "^3.2.33"
|
||||
},
|
||||
"bundlewatch": {
|
||||
"files": [
|
||||
{
|
||||
"path": "packages/*/build/umd/*.production.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
6
packages/form-core/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
4
packages/form-core/jest.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
displayName: 'form-core',
|
||||
preset: '../../jest-preset.js',
|
||||
}
|
||||
41
packages/form-core/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@tanstack/form-core",
|
||||
"version": "4.29.5",
|
||||
"description": "The framework agnostic core that powers TanStack Form",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": "tanstack/form",
|
||||
"homepage": "https://tanstack.com/form",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"types": "build/lib/index.d.ts",
|
||||
"main": "build/lib/index.js",
|
||||
"module": "build/lib/index.esm.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"import": "./build/lib/index.mjs",
|
||||
"default": "./build/lib/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"build/lib/*",
|
||||
"build/umd/*",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
||||
"test:types": "tsc",
|
||||
"test:lib": "jest --config ./jest.config.ts",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"build:types": "tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/store": "0.0.1-beta.84"
|
||||
}
|
||||
}
|
||||
278
packages/form-core/src/FieldApi.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
//
|
||||
import type { DeepKeys, DeepValue, RequiredByKey, Updater } from './utils'
|
||||
import type { FormApi, ValidationError } from './FormApi'
|
||||
import { Store } from '@tanstack/store'
|
||||
|
||||
export type FieldOptions<TData, TFormData> = {
|
||||
name: unknown extends TFormData ? string : DeepKeys<TFormData>
|
||||
defaultValue?: TData
|
||||
form?: FormApi<TFormData>
|
||||
validate?: (
|
||||
value: TData,
|
||||
fieldApi: FieldApi<TData, TFormData>,
|
||||
) => ValidationError | Promise<ValidationError>
|
||||
validatePristine?: boolean
|
||||
filterValue?: (value: TData) => TData
|
||||
defaultMeta?: Partial<FieldMeta>
|
||||
validateOn?:
|
||||
| 'change'
|
||||
| 'blur'
|
||||
| 'change-blur'
|
||||
| 'change-submit'
|
||||
| 'blur-submit'
|
||||
| 'submit'
|
||||
}
|
||||
|
||||
export type ChangeProps<TData> = {
|
||||
onChange: (updater: Updater<TData>) => void
|
||||
onBlur: (event: any) => void
|
||||
}
|
||||
|
||||
export type InputProps = {
|
||||
onChange: (event: any) => void
|
||||
onBlur: (event: any) => void
|
||||
}
|
||||
|
||||
export type FieldMeta = {
|
||||
isTouched: boolean
|
||||
touchedError?: ValidationError
|
||||
error?: ValidationError
|
||||
isValidating: boolean
|
||||
}
|
||||
|
||||
export type FieldApiOptions<TData, TFormData> = RequiredByKey<
|
||||
FieldOptions<TData, TFormData>,
|
||||
'form'
|
||||
>
|
||||
|
||||
let uid = 0
|
||||
|
||||
export type FieldState<TData> = {
|
||||
value: TData
|
||||
meta: FieldMeta
|
||||
}
|
||||
|
||||
export class FieldApi<TData, TFormData> {
|
||||
uid: number
|
||||
form: FormApi<TFormData>
|
||||
name!: DeepKeys<TFormData>
|
||||
store!: Store<FieldState<TData>>
|
||||
state!: FieldState<TData>
|
||||
options: RequiredByKey<FieldOptions<TData, TFormData>, 'validateOn'> =
|
||||
{} as any
|
||||
|
||||
constructor(opts: FieldApiOptions<TData, TFormData>) {
|
||||
this.form = opts.form
|
||||
this.uid = uid++
|
||||
// Support field prefixing from FieldScope
|
||||
let fieldPrefix = ''
|
||||
if (this.form.fieldName) {
|
||||
fieldPrefix = `${this.form.fieldName}.`
|
||||
}
|
||||
|
||||
this.name = (fieldPrefix + opts.name) as any
|
||||
|
||||
this.store = new Store<FieldState<TData>>(
|
||||
{
|
||||
value: this.getValue(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
meta: this.getMeta() ?? {
|
||||
isValidating: false,
|
||||
isTouched: false,
|
||||
...this.options.defaultMeta,
|
||||
},
|
||||
},
|
||||
{
|
||||
onUpdate: (next) => {
|
||||
next.meta.touchedError = next.meta.isTouched
|
||||
? next.meta.error
|
||||
: undefined
|
||||
|
||||
// Do not validate pristine fields
|
||||
if (!this.options.validatePristine && !next.meta.isTouched) return
|
||||
|
||||
// If validateOn is set to a variation of change, run the validation
|
||||
if (
|
||||
this.options.validateOn === 'change' ||
|
||||
this.options.validateOn.split('-')[0] === 'change'
|
||||
) {
|
||||
try {
|
||||
this.validate()
|
||||
} catch (err) {
|
||||
console.error('An error occurred during validation', err)
|
||||
}
|
||||
}
|
||||
|
||||
this.state = next
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
this.state = this.store.state
|
||||
this.update(opts)
|
||||
}
|
||||
|
||||
mount = () => {
|
||||
const info = this.getInfo()
|
||||
info.instances[this.uid] = this
|
||||
|
||||
const unsubscribe = this.form.store.subscribe(() => {
|
||||
this.updateStore()
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
delete info.instances[this.uid]
|
||||
if (!Object.keys(info.instances).length) {
|
||||
delete this.form.fieldInfo[this.name]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateStore = () => {
|
||||
this.store.batch(() => {
|
||||
const nextValue = this.getValue()
|
||||
const nextMeta = this.getMeta()
|
||||
|
||||
if (nextValue !== this.state.value) {
|
||||
this.store.setState((prev) => ({ ...prev, value: nextValue }))
|
||||
}
|
||||
|
||||
if (nextMeta !== this.state.meta) {
|
||||
this.store.setState((prev) => ({ ...prev, meta: nextMeta }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
update = (opts: FieldApiOptions<TData, TFormData>) => {
|
||||
this.options = { validateOn: 'blur', ...opts }
|
||||
|
||||
// Default Value
|
||||
if (
|
||||
this.state.value === undefined &&
|
||||
this.options.defaultValue !== undefined
|
||||
) {
|
||||
this.setValue(this.options.defaultValue)
|
||||
}
|
||||
|
||||
// Default Meta
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (this.getMeta() === undefined) {
|
||||
this.setMeta(this.state.meta)
|
||||
}
|
||||
}
|
||||
|
||||
getValue = (): TData => this.form.getFieldValue(this.name)
|
||||
setValue = (
|
||||
updater: Updater<TData>,
|
||||
options?: { touch?: boolean; notify?: boolean },
|
||||
) => this.form.setFieldValue(this.name, updater as any, options)
|
||||
|
||||
getMeta = (): FieldMeta => this.form.getFieldMeta(this.name)
|
||||
setMeta = (updater: Updater<FieldMeta>) =>
|
||||
this.form.setFieldMeta(this.name, updater)
|
||||
|
||||
getInfo = () => this.form.getFieldInfo(this.name)
|
||||
|
||||
pushValue = (value: TData) =>
|
||||
this.form.pushFieldValue(this.name, value as any)
|
||||
insertValue = (index: number, value: TData) =>
|
||||
this.form.insertFieldValue(this.name, index, value as any)
|
||||
removeValue = (index: number) => this.form.spliceFieldValue(this.name, index)
|
||||
swapValues = (aIndex: number, bIndex: number) =>
|
||||
this.form.swapFieldValues(this.name, aIndex, bIndex)
|
||||
|
||||
getSubField = <TName extends DeepKeys<TData>>(name: TName) =>
|
||||
new FieldApi<DeepValue<TData, TName>, TFormData>({
|
||||
name: `${this.name}.${name}` as any,
|
||||
form: this.form,
|
||||
})
|
||||
|
||||
validate = async () => {
|
||||
if (!this.options.validate) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setMeta((prev) => ({ ...prev, isValidating: true }))
|
||||
|
||||
// Use the validationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
const validationCount = (this.getInfo().validationCount || 0) + 1
|
||||
|
||||
this.getInfo().validationCount = validationCount
|
||||
|
||||
const checkLatest = () => validationCount === this.getInfo().validationCount
|
||||
|
||||
if (!this.getInfo().validationPromise) {
|
||||
this.getInfo().validationPromise = new Promise((resolve, reject) => {
|
||||
this.getInfo().validationResolve = resolve
|
||||
this.getInfo().validationReject = reject
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const error = await this.options.validate(this.state.value, this)
|
||||
|
||||
if (checkLatest()) {
|
||||
this.setMeta((prev) => ({
|
||||
...prev,
|
||||
isValidating: false,
|
||||
error: error
|
||||
? typeof error === 'string'
|
||||
? error
|
||||
: 'Invalid Form Values'
|
||||
: null,
|
||||
}))
|
||||
this.getInfo().validationResolve?.(error)
|
||||
}
|
||||
} catch (error) {
|
||||
if (checkLatest()) {
|
||||
this.getInfo().validationReject?.(error)
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
if (checkLatest()) {
|
||||
this.setMeta((prev) => ({ ...prev, isValidating: false }))
|
||||
delete this.getInfo().validationPromise
|
||||
}
|
||||
}
|
||||
|
||||
return this.getInfo().validationPromise
|
||||
}
|
||||
|
||||
getChangeProps = <T extends ChangeProps<any>>(
|
||||
props: T = {} as T,
|
||||
): ChangeProps<TData> & Omit<T, keyof ChangeProps<TData>> => {
|
||||
return {
|
||||
...props,
|
||||
onChange: (value) => {
|
||||
this.setValue(value)
|
||||
props.onChange(value)
|
||||
},
|
||||
onBlur: (e) => {
|
||||
this.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||
|
||||
const { validateOn } = this.options
|
||||
|
||||
if (validateOn === 'blur' || validateOn.split('-')[0] === 'blur') {
|
||||
this.validate()
|
||||
}
|
||||
|
||||
props.onBlur(e)
|
||||
},
|
||||
} as ChangeProps<TData> & Omit<T, keyof ChangeProps<TData>>
|
||||
}
|
||||
|
||||
getInputProps = <T extends InputProps>(
|
||||
props: T = {} as T,
|
||||
): InputProps & Omit<T, keyof InputProps> => {
|
||||
return {
|
||||
...props,
|
||||
onChange: (e) => {
|
||||
this.setValue(e.target.value)
|
||||
props.onChange(e.target.value)
|
||||
},
|
||||
onBlur: this.getChangeProps(props).onBlur,
|
||||
}
|
||||
}
|
||||
}
|
||||
424
packages/form-core/src/FormApi.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import { Store } from '@tanstack/store'
|
||||
//
|
||||
import type { DeepKeys, DeepValue, Updater } from './utils'
|
||||
import { functionalUpdate, getBy, setBy } from './utils'
|
||||
import type { FieldApi, FieldMeta } from './FieldApi'
|
||||
|
||||
export type FormOptions<TData> = {
|
||||
defaultValues?: TData
|
||||
defaultState?: Partial<FormState<TData>>
|
||||
onSubmit?: (values: TData, formApi: FormApi<TData>) => Promise<any>
|
||||
onInvalidSubmit?: (values: TData, formApi: FormApi<TData>) => void
|
||||
validate?: (values: TData, formApi: FormApi<TData>) => Promise<any>
|
||||
debugForm?: boolean
|
||||
validatePristine?: boolean
|
||||
}
|
||||
|
||||
export type FieldInfo<TFormData> = {
|
||||
instances: Record<string, FieldApi<any, TFormData>>
|
||||
} & ValidationMeta
|
||||
|
||||
export type ValidationMeta = {
|
||||
validationCount?: number
|
||||
validationPromise?: Promise<ValidationError>
|
||||
validationResolve?: (error: ValidationError) => void
|
||||
validationReject?: (error: unknown) => void
|
||||
}
|
||||
|
||||
export type ValidationError = undefined | false | null | string
|
||||
|
||||
export type FormState<TData> = {
|
||||
values: TData
|
||||
// Form Validation
|
||||
isFormValidating: boolean
|
||||
formValidationCount: number
|
||||
isFormValid: boolean
|
||||
formError?: ValidationError
|
||||
// Fields
|
||||
fieldMeta: Record<DeepKeys<TData>, FieldMeta>
|
||||
isFieldsValidating: boolean
|
||||
isFieldsValid: boolean
|
||||
isSubmitting: boolean
|
||||
// General
|
||||
isTouched: boolean
|
||||
isSubmitted: boolean
|
||||
isValidating: boolean
|
||||
isValid: boolean
|
||||
canSubmit: boolean
|
||||
submissionAttempts: number
|
||||
}
|
||||
|
||||
export function getDefaultFormState<TData>(
|
||||
defaultState: Partial<FormState<TData>>,
|
||||
): FormState<TData> {
|
||||
return {
|
||||
values: {} as any,
|
||||
fieldMeta: {} as any,
|
||||
canSubmit: true,
|
||||
isFieldsValid: false,
|
||||
isFieldsValidating: false,
|
||||
isFormValid: false,
|
||||
isFormValidating: false,
|
||||
isSubmitted: false,
|
||||
isSubmitting: false,
|
||||
isTouched: false,
|
||||
isValid: false,
|
||||
isValidating: false,
|
||||
submissionAttempts: 0,
|
||||
formValidationCount: 0,
|
||||
...defaultState,
|
||||
}
|
||||
}
|
||||
|
||||
export class FormApi<TFormData> {
|
||||
// // This carries the context for nested fields
|
||||
options: FormOptions<TFormData> = {}
|
||||
store!: Store<FormState<TFormData>>
|
||||
// Do not use __state directly, as it is not reactive.
|
||||
// Please use form.useStore() utility to subscribe to state
|
||||
state!: FormState<TFormData>
|
||||
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>> = {} as any
|
||||
fieldName?: string
|
||||
validationMeta: ValidationMeta = {}
|
||||
|
||||
constructor(opts?: FormOptions<TFormData>) {
|
||||
this.store = new Store<FormState<TFormData>>(
|
||||
getDefaultFormState({
|
||||
...opts?.defaultState,
|
||||
values: opts?.defaultValues ?? opts?.defaultState?.values,
|
||||
isFormValid: !opts?.validate,
|
||||
}),
|
||||
{
|
||||
onUpdate: (next) => {
|
||||
// Computed state
|
||||
const fieldMetaValues = Object.values(next.fieldMeta) as (
|
||||
| FieldMeta
|
||||
| undefined
|
||||
)[]
|
||||
|
||||
const isFieldsValidating = fieldMetaValues.some(
|
||||
(field) => field?.isValidating,
|
||||
)
|
||||
|
||||
const isFieldsValid = !fieldMetaValues.some((field) => field?.error)
|
||||
|
||||
const isTouched = fieldMetaValues.some((field) => field?.isTouched)
|
||||
|
||||
const isValidating = isFieldsValidating || next.isFormValidating
|
||||
const isFormValid = !next.formError
|
||||
const isValid = isFieldsValid && isFormValid
|
||||
const canSubmit =
|
||||
next.submissionAttempts === 0 ||
|
||||
(!isValidating && !next.isSubmitting && isValid)
|
||||
|
||||
next = {
|
||||
...next,
|
||||
isFieldsValidating,
|
||||
isFieldsValid,
|
||||
isFormValid,
|
||||
isValid,
|
||||
canSubmit,
|
||||
isTouched,
|
||||
}
|
||||
|
||||
// Create a shortcut for the state
|
||||
// Write it back to the store
|
||||
this.store.state = next
|
||||
this.state = next
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
this.state = this.store.state
|
||||
|
||||
this.update(opts || {})
|
||||
}
|
||||
|
||||
update = (options: FormOptions<TFormData>) => {
|
||||
this.store.batch(() => {
|
||||
if (
|
||||
options.defaultState &&
|
||||
options.defaultState !== this.options.defaultState
|
||||
) {
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
...options.defaultState,
|
||||
}))
|
||||
}
|
||||
|
||||
if (options.defaultValues !== this.options.defaultValues) {
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
values: options.defaultValues as TFormData,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
this.options = options
|
||||
}
|
||||
|
||||
reset = () =>
|
||||
this.store.setState(() => getDefaultFormState(this.options.defaultValues!))
|
||||
|
||||
validateAllFields = async () => {
|
||||
const fieldValidationPromises: Promise<ValidationError>[] = [] as any
|
||||
|
||||
this.store.batch(() => {
|
||||
void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach(
|
||||
(field) => {
|
||||
Object.values(field.instances).forEach((instance) => {
|
||||
// If any fields are not touched
|
||||
if (!instance.state.meta.isTouched) {
|
||||
// Mark them as touched
|
||||
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
||||
// Validate the field
|
||||
if (instance.options.validate) {
|
||||
fieldValidationPromises.push(instance.validate())
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
return Promise.all(fieldValidationPromises)
|
||||
}
|
||||
|
||||
validateForm = async () => {
|
||||
const { validate } = this.options
|
||||
|
||||
if (!validate) {
|
||||
return
|
||||
}
|
||||
|
||||
// Use the formValidationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
isValidating: true,
|
||||
formValidationCount: prev.formValidationCount + 1,
|
||||
}))
|
||||
|
||||
const formValidationCount = this.state.formValidationCount
|
||||
|
||||
const checkLatest = () =>
|
||||
formValidationCount === this.state.formValidationCount
|
||||
|
||||
if (!this.validationMeta.validationPromise) {
|
||||
this.validationMeta.validationPromise = new Promise((resolve, reject) => {
|
||||
this.validationMeta.validationResolve = resolve
|
||||
this.validationMeta.validationReject = reject
|
||||
})
|
||||
}
|
||||
|
||||
const doValidation = async () => {
|
||||
try {
|
||||
const error = await validate(this.state.values, this)
|
||||
|
||||
if (checkLatest()) {
|
||||
this.store.setState((prev) => ({
|
||||
...prev,
|
||||
isValidating: false,
|
||||
error: error
|
||||
? typeof error === 'string'
|
||||
? error
|
||||
: 'Invalid Form Values'
|
||||
: null,
|
||||
}))
|
||||
|
||||
this.validationMeta.validationResolve?.(error)
|
||||
}
|
||||
} catch (err) {
|
||||
if (checkLatest()) {
|
||||
this.validationMeta.validationReject?.(err)
|
||||
}
|
||||
} finally {
|
||||
delete this.validationMeta.validationPromise
|
||||
}
|
||||
}
|
||||
|
||||
doValidation()
|
||||
|
||||
return this.validationMeta.validationPromise
|
||||
}
|
||||
|
||||
handleSubmit = async (e: FormEvent & { __handled?: boolean }) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// Check to see that the form and all fields have been touched
|
||||
// If they have not, touch them all and run validation
|
||||
// Run form validation
|
||||
// Submit the form
|
||||
|
||||
this.store.setState((old) => ({
|
||||
...old,
|
||||
// Submittion attempts mark the form as not submitted
|
||||
isSubmitted: false,
|
||||
// Count submission attempts
|
||||
submissionAttempts: old.submissionAttempts + 1,
|
||||
}))
|
||||
|
||||
// Don't let invalid forms submit
|
||||
if (!this.state.canSubmit) return
|
||||
|
||||
this.store.setState((d) => ({ ...d, isSubmitting: true }))
|
||||
|
||||
const done = () => {
|
||||
this.store.setState((prev) => ({ ...prev, isSubmitting: false }))
|
||||
}
|
||||
|
||||
// Validate all fields
|
||||
await this.validateAllFields()
|
||||
|
||||
// Fields are invalid, do not submit
|
||||
if (!this.state.isFieldsValid) {
|
||||
done()
|
||||
this.options.onInvalidSubmit?.(this.state.values, this)
|
||||
return
|
||||
}
|
||||
|
||||
// Run validation for the form
|
||||
await this.validateForm()
|
||||
|
||||
if (!this.state.isValid) {
|
||||
done()
|
||||
this.options.onInvalidSubmit?.(this.state.values, this)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Run the submit code
|
||||
await this.options.onSubmit?.(this.state.values, this)
|
||||
|
||||
this.store.batch(() => {
|
||||
this.store.setState((prev) => ({ ...prev, isSubmitted: true }))
|
||||
done()
|
||||
})
|
||||
} catch (err) {
|
||||
done()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
getFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
): DeepValue<TFormData, TField> => getBy(this.state.values, field)
|
||||
|
||||
getFieldMeta = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
): FieldMeta => {
|
||||
return this.state.fieldMeta[field]
|
||||
}
|
||||
|
||||
getFieldInfo = <TField extends DeepKeys<TFormData>>(field: TField) => {
|
||||
return (this.fieldInfo[field] ||= {
|
||||
instances: {},
|
||||
})
|
||||
}
|
||||
|
||||
setFieldMeta = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
updater: Updater<FieldMeta>,
|
||||
) => {
|
||||
this.store.setState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
fieldMeta: {
|
||||
...prev.fieldMeta,
|
||||
[field]: functionalUpdate(updater, prev.fieldMeta[field]),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
updater: Updater<DeepValue<TFormData, TField>>,
|
||||
opts?: { touch?: boolean },
|
||||
) => {
|
||||
const touch = opts?.touch ?? true
|
||||
|
||||
this.store.batch(() => {
|
||||
this.store.setState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
values: setBy(prev.values, field, updater),
|
||||
}
|
||||
})
|
||||
|
||||
if (touch) {
|
||||
this.setFieldMeta(field, (prev) => ({
|
||||
...prev,
|
||||
isTouched: true,
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pushFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
value: DeepValue<TFormData, TField>,
|
||||
opts?: { touch?: boolean },
|
||||
) => {
|
||||
return this.setFieldValue(
|
||||
field,
|
||||
(prev) => [...(Array.isArray(prev) ? prev : []), value] as any,
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
insertFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
index: number,
|
||||
value: DeepValue<TFormData, TField>,
|
||||
opts?: { touch?: boolean },
|
||||
) => {
|
||||
this.setFieldValue(
|
||||
field,
|
||||
(prev) => {
|
||||
// invariant( // TODO: bring in invariant
|
||||
// Array.isArray(prev),
|
||||
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
||||
// )
|
||||
return (prev as DeepValue<TFormData, TField>[]).map((d, i) =>
|
||||
i === index ? value : d,
|
||||
) as any
|
||||
},
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
spliceFieldValue = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
index: number,
|
||||
opts?: { touch?: boolean },
|
||||
) => {
|
||||
this.setFieldValue(
|
||||
field,
|
||||
(prev) => {
|
||||
// invariant( // TODO: bring in invariant
|
||||
// Array.isArray(prev),
|
||||
// `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
||||
// )
|
||||
return (prev as DeepValue<TFormData, TField>[]).filter(
|
||||
(_d, i) => i !== index,
|
||||
) as any
|
||||
},
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
swapFieldValues = <TField extends DeepKeys<TFormData>>(
|
||||
field: TField,
|
||||
index1: number,
|
||||
index2: number,
|
||||
) => {
|
||||
this.setFieldValue(field, (prev: any) => {
|
||||
const prev1 = prev[index1]!
|
||||
const prev2 = prev[index2]!
|
||||
return setBy(setBy(prev, [index1], prev2), [index2], prev1)
|
||||
})
|
||||
}
|
||||
}
|
||||
3
packages/form-core/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './FormApi'
|
||||
export * from './FieldApi'
|
||||
export * from './utils'
|
||||
5
packages/form-core/src/tests/test.test.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('tests', () => {
|
||||
it('should test', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
144
packages/form-core/src/utils.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput
|
||||
|
||||
export type Updater<TInput, TOutput = TInput> =
|
||||
| TOutput
|
||||
| UpdaterFn<TInput, TOutput>
|
||||
|
||||
export function functionalUpdate<TInput, TOutput = TInput>(
|
||||
updater: Updater<TInput, TOutput>,
|
||||
input: TInput,
|
||||
): TOutput {
|
||||
return typeof updater === 'function'
|
||||
? (updater as UpdaterFn<TInput, TOutput>)(input)
|
||||
: updater
|
||||
}
|
||||
|
||||
export function getBy(obj: any, path: any) {
|
||||
if (!path) {
|
||||
throw new Error('A path string is required to use getBy')
|
||||
}
|
||||
const pathArray = makePathArray(path)
|
||||
const pathObj = pathArray
|
||||
return pathObj.reduce((current: any, pathPart: any) => {
|
||||
if (typeof current !== 'undefined') {
|
||||
return current[pathPart]
|
||||
}
|
||||
return undefined
|
||||
}, obj)
|
||||
}
|
||||
|
||||
export function setBy(obj: any, _path: any, updater: Updater<any>) {
|
||||
const path = makePathArray(_path)
|
||||
|
||||
function doSet(parent?: any): any {
|
||||
if (!path.length) {
|
||||
return functionalUpdate(updater, parent)
|
||||
}
|
||||
|
||||
const key = path.shift()
|
||||
|
||||
if (typeof key === 'string') {
|
||||
if (typeof parent === 'object') {
|
||||
return {
|
||||
...parent,
|
||||
[key]: doSet(parent[key]),
|
||||
}
|
||||
}
|
||||
return {
|
||||
[key]: doSet(),
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof key === 'number') {
|
||||
if (Array.isArray(parent)) {
|
||||
const prefix = parent.slice(0, key)
|
||||
return [
|
||||
...(prefix.length ? prefix : new Array(key)),
|
||||
doSet(parent[key]),
|
||||
...parent.slice(key + 1),
|
||||
]
|
||||
}
|
||||
return [...new Array(key), doSet()]
|
||||
}
|
||||
|
||||
throw new Error('Uh oh!')
|
||||
}
|
||||
|
||||
return doSet(obj)
|
||||
}
|
||||
|
||||
const reFindNumbers0 = /^(\d*)$/gm
|
||||
const reFindNumbers1 = /\.(\d*)\./gm
|
||||
const reFindNumbers2 = /^(\d*)\./gm
|
||||
const reFindNumbers3 = /\.(\d*$)/gm
|
||||
const reFindMultiplePeriods = /\.{2,}/gm
|
||||
|
||||
function makePathArray(str: string) {
|
||||
return str
|
||||
.replace('[', '.')
|
||||
.replace(']', '')
|
||||
.replace(reFindNumbers0, '__int__$1')
|
||||
.replace(reFindNumbers1, '.__int__$1.')
|
||||
.replace(reFindNumbers2, '__int__$1.')
|
||||
.replace(reFindNumbers3, '.__int__$1')
|
||||
.replace(reFindMultiplePeriods, '.')
|
||||
.split('.')
|
||||
.map((d) => {
|
||||
if (d.indexOf('__int__') === 0) {
|
||||
return parseInt(d.substring('__int__'.length), 10)
|
||||
}
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
|
||||
Required<Pick<T, K>>
|
||||
|
||||
type ComputeRange<
|
||||
N extends number,
|
||||
Result extends Array<unknown> = [],
|
||||
> = Result['length'] extends N
|
||||
? Result
|
||||
: ComputeRange<N, [...Result, Result['length']]>
|
||||
type Index40 = ComputeRange<40>[number]
|
||||
|
||||
// Is this type a tuple?
|
||||
type IsTuple<T> = T extends readonly any[] & { length: infer Length }
|
||||
? Length extends Index40
|
||||
? T
|
||||
: never
|
||||
: never
|
||||
|
||||
// If this type is a tuple, what indices are allowed?
|
||||
type AllowedIndexes<
|
||||
Tuple extends ReadonlyArray<any>,
|
||||
Keys extends number = never,
|
||||
> = Tuple extends readonly []
|
||||
? Keys
|
||||
: Tuple extends readonly [infer _, ...infer Tail]
|
||||
? AllowedIndexes<Tail, Keys | Tail['length']>
|
||||
: Keys
|
||||
|
||||
export type DeepKeys<T> = unknown extends T
|
||||
? keyof T
|
||||
: object extends T
|
||||
? string
|
||||
: T extends readonly any[] & IsTuple<T>
|
||||
? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>>
|
||||
: T extends any[]
|
||||
? never & 'Dynamic length array indexing is not supported'
|
||||
: T extends Date
|
||||
? never
|
||||
: T extends object
|
||||
? (keyof T & string) | DeepKeysPrefix<T, keyof T>
|
||||
: never
|
||||
|
||||
type DeepKeysPrefix<T, TPrefix> = TPrefix extends keyof T & (number | string)
|
||||
? `${TPrefix}.${DeepKeys<T[TPrefix]> & string}`
|
||||
: never
|
||||
|
||||
export type DeepValue<T, TProp> = T extends Record<string | number, any>
|
||||
? TProp extends `${infer TBranch}.${infer TDeepProp}`
|
||||
? DeepValue<T[TBranch], TDeepProp>
|
||||
: T[TProp & string]
|
||||
: never
|
||||
10
packages/form-core/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build/lib",
|
||||
"tsBuildInfoFile": "./build/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
6
packages/react-form/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
35
packages/react-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 React
|
||||
|
||||
<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%22react-form+tests%22">
|
||||
<img src="https://github.com/TanStack/form/workflows/react-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/react-form@latest" target="\_parent">
|
||||
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/react-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 -->
|
||||
6
packages/react-form/jest.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
displayName: 'react-form',
|
||||
preset: '../../jest-preset.js',
|
||||
setupFilesAfterEnv: ['./jest.setup.ts'],
|
||||
testMatch: ['<rootDir>/src/**/*.test.tsx', '<rootDir>/codemods/**/*.test.js'],
|
||||
}
|
||||
66
packages/react-form/package.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "@tanstack/react-form",
|
||||
"version": "4.29.5",
|
||||
"description": "Hooks for managing form state in React",
|
||||
"author": "tannerlinsley",
|
||||
"license": "MIT",
|
||||
"repository": "tanstack/form",
|
||||
"homepage": "https://tanstack.com/form",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"types": "build/lib/index.d.ts",
|
||||
"main": "build/lib/index.js",
|
||||
"module": "build/lib/index.esm.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/lib/index.d.ts",
|
||||
"import": "./build/lib/index.mjs",
|
||||
"default": "./build/lib/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": [
|
||||
"./src/setBatchUpdatesFn.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf ./build",
|
||||
"test:eslint": "eslint --ext .ts,.tsx ./src",
|
||||
"test:types": "tsc",
|
||||
"test:lib": "jest --config ./jest.config.ts",
|
||||
"test:lib:dev": "pnpm run test:lib --watch",
|
||||
"build:types": "tsc --build"
|
||||
},
|
||||
"files": [
|
||||
"build/lib/*",
|
||||
"build/umd/*",
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/jscodeshift": "^0.11.3",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react-dom": "^18.0.5",
|
||||
"@types/use-sync-external-store": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "workspace:*",
|
||||
"@tanstack/react-store": "0.0.1-beta.84"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
38
packages/react-form/src/Field.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react'
|
||||
import type {
|
||||
DeepKeys,
|
||||
DeepValue,
|
||||
FieldApi,
|
||||
FieldOptions,
|
||||
FormApi,
|
||||
} from '@tanstack/form-core'
|
||||
import { useField } from './useField'
|
||||
|
||||
//
|
||||
|
||||
export type FieldComponent<TFormData> = <TField extends DeepKeys<TFormData>>({
|
||||
children,
|
||||
...fieldOptions
|
||||
}: {
|
||||
children: (fieldApi: FieldApi<DeepValue<TFormData, TField>, TFormData>) => any
|
||||
name: TField
|
||||
} & Omit<FieldOptions<DeepValue<TFormData, TField>, TFormData>, 'name'>) => any
|
||||
|
||||
export function createFieldComponent<TFormData>(formApi: FormApi<TFormData>) {
|
||||
const ConnectedField: FieldComponent<TFormData> = (props) => (
|
||||
<Field {...(props as any)} form={formApi} />
|
||||
)
|
||||
return ConnectedField
|
||||
}
|
||||
|
||||
export function Field<TData, TFormData>({
|
||||
children,
|
||||
...fieldOptions
|
||||
}: {
|
||||
children: (fieldApi: FieldApi<TData, TFormData>) => any
|
||||
} & FieldOptions<TData, TFormData>) {
|
||||
const fieldApi = useField(fieldOptions as any)
|
||||
return typeof children === 'function'
|
||||
? React.createElement(children, fieldApi as any)
|
||||
: children
|
||||
}
|
||||
18
packages/react-form/src/formContext.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FormApi } from '@tanstack/form-core'
|
||||
import * as React from 'react'
|
||||
|
||||
export const formContext = React.createContext<FormApi<any>>(null!)
|
||||
|
||||
export function useFormContext(customFormApi?: FormApi<any>) {
|
||||
const formApi = React.useContext(formContext)
|
||||
|
||||
if (customFormApi) {
|
||||
return customFormApi
|
||||
}
|
||||
|
||||
if (!formApi) {
|
||||
throw new Error(`You are trying to use the form API outside of a form!`)
|
||||
}
|
||||
|
||||
return formApi
|
||||
}
|
||||
3
packages/react-form/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './useForm'
|
||||
export * from './Field'
|
||||
export * from './useField'
|
||||
5
packages/react-form/src/tests/test.test.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('tests', () => {
|
||||
it('should test', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
54
packages/react-form/src/useField.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react'
|
||||
//
|
||||
import { useStore } from '@tanstack/react-store'
|
||||
import type {
|
||||
DeepKeys,
|
||||
DeepValue,
|
||||
FieldOptions,
|
||||
FormApi,
|
||||
} from '@tanstack/form-core'
|
||||
import { FieldApi } from '@tanstack/form-core'
|
||||
import { useFormContext } from './formContext'
|
||||
|
||||
export type UseField<TFormData> = <TField extends DeepKeys<TFormData>>(
|
||||
opts?: { name: TField } & FieldOptions<
|
||||
DeepValue<TFormData, TField>,
|
||||
TFormData
|
||||
>,
|
||||
) => FieldApi<DeepValue<TFormData, TField>, TFormData>
|
||||
|
||||
export function createUseField<TFormData>(formApi: FormApi<TFormData>) {
|
||||
const useFormField: UseField<TFormData> = (opts) => {
|
||||
return useField({ ...opts, form: formApi } as any)
|
||||
}
|
||||
|
||||
return useFormField
|
||||
}
|
||||
|
||||
export function useField<TData, TFormData>(
|
||||
opts: FieldOptions<TData, TFormData> & {
|
||||
// selector: (state: FieldApi<TData, TFormData>) => TSelected
|
||||
},
|
||||
): FieldApi<TData, TFormData> {
|
||||
// invariant( // TODO:
|
||||
// opts.name,
|
||||
// `useField: A field is required to use this hook. eg, useField('myField', options)`
|
||||
// )
|
||||
|
||||
// Get the form API either manually or from context
|
||||
const formApi = useFormContext(opts.form)
|
||||
|
||||
const [fieldApi] = React.useState<FieldApi<TData, TFormData>>(
|
||||
() => new FieldApi({ ...opts, form: formApi }),
|
||||
)
|
||||
|
||||
// Keep options up to date as they are rendered
|
||||
fieldApi.update({ ...opts, form: formApi })
|
||||
|
||||
useStore(fieldApi.store)
|
||||
|
||||
// Instantiates field meta and removes it when unrendered
|
||||
React.useEffect(() => fieldApi.mount(), [fieldApi])
|
||||
|
||||
return fieldApi
|
||||
}
|
||||
121
packages/react-form/src/useForm.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { FormState, FormOptions } from '@tanstack/form-core'
|
||||
import { FormApi, functionalUpdate } from '@tanstack/form-core'
|
||||
import type { NoInfer } from '@tanstack/react-store'
|
||||
import { useStore } from '@tanstack/react-store'
|
||||
import React from 'react'
|
||||
import { createFieldComponent, type FieldComponent } from './Field'
|
||||
import { createUseField, type UseField } from './useField'
|
||||
import { formContext } from './formContext'
|
||||
//
|
||||
|
||||
declare module '@tanstack/form-core' {
|
||||
// eslint-disable-next-line no-shadow
|
||||
interface FormApi<TFormData> {
|
||||
Form: FormComponent
|
||||
Field: FieldComponent<TFormData>
|
||||
useField: UseField<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>) => React.ReactNode)
|
||||
| React.ReactNode
|
||||
}) => any
|
||||
}
|
||||
}
|
||||
|
||||
export function useForm<TData>(
|
||||
opts?: FormOptions<TData> & { listen?: (state: FormState<TData>) => any },
|
||||
): FormApi<TData> {
|
||||
// & { listened: TListen }
|
||||
const [formApi] = React.useState(() => {
|
||||
const api = new FormApi<TData>(opts || {})
|
||||
|
||||
api.Form = createFormComponent(api)
|
||||
api.Field = createFieldComponent(api)
|
||||
api.useField = createUseField(api)
|
||||
api.useStore = (selector) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useStore(api.store, selector) as any
|
||||
}
|
||||
api.Subscribe = (props) => {
|
||||
return functionalUpdate(
|
||||
props.children,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useStore(api.store, props.selector),
|
||||
) as any
|
||||
}
|
||||
|
||||
return api
|
||||
})
|
||||
|
||||
// React.useEffect(() => formApi.mount(), [])
|
||||
|
||||
return formApi
|
||||
}
|
||||
|
||||
export type FormProps = React.HTMLProps<HTMLFormElement> & {
|
||||
children: React.ReactNode
|
||||
noFormElement?: boolean
|
||||
}
|
||||
|
||||
export type FormComponent = (props: FormProps) => any
|
||||
|
||||
export function createFormComponent(formApi: FormApi<any>) {
|
||||
const Form: FormComponent = ({ children, noFormElement, ...rest }) => {
|
||||
const isSubmitting = formApi.useStore((state) => state.isSubmitting)
|
||||
|
||||
return (
|
||||
<formContext.Provider value={formApi}>
|
||||
{noFormElement ? (
|
||||
children
|
||||
) : (
|
||||
<form
|
||||
onSubmit={formApi.handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
{...rest}
|
||||
>
|
||||
{formApi.options.debugForm ? (
|
||||
<div
|
||||
style={{
|
||||
margin: '2rem 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 'bolder',
|
||||
}}
|
||||
>
|
||||
Form State
|
||||
</div>
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(formApi, safeStringifyReplace(), 2)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</form>
|
||||
)}
|
||||
</formContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return Form
|
||||
}
|
||||
|
||||
function safeStringifyReplace() {
|
||||
const set = new Set()
|
||||
return (_key: string, value: any) => {
|
||||
if (typeof value === 'object' || Array.isArray(value)) {
|
||||
if (set.has(value)) {
|
||||
return '(circular value)'
|
||||
}
|
||||
set.add(value)
|
||||
}
|
||||
return typeof value === 'function' ? undefined : value
|
||||
}
|
||||
}
|
||||
11
packages/react-form/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./build/lib",
|
||||
"tsBuildInfoFile": "./build/.tsbuildinfo"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "../form-core" }]
|
||||
}
|
||||
10397
pnpm-lock.yaml
generated
Normal file
8
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
packages:
|
||||
- 'packages/**'
|
||||
- 'examples/react/**'
|
||||
- 'examples/solid/**'
|
||||
- 'examples/svelte/**'
|
||||
- 'examples/vue/**'
|
||||
- '!examples/vue/2*'
|
||||
- '!examples/vue/nuxt*'
|
||||
@@ -1,12 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
bracketSpacing: true,
|
||||
jsxBracketSameLine: false,
|
||||
arrowParens: 'avoid',
|
||||
endOfLine: 'auto',
|
||||
}
|
||||
28
project.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "root",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "./",
|
||||
"targets": {
|
||||
"rollup": {
|
||||
"command": "rollup --config rollup.config.js",
|
||||
"outputs": [
|
||||
"{workspaceRoot}/packages/*/build/**/*(.cjs|.mjs|.js)*",
|
||||
"{workspaceRoot}/packages/*/build/stats*",
|
||||
"{workspaceRoot}/packages/*/build/**/*.svelte"
|
||||
],
|
||||
"inputs": ["default"]
|
||||
},
|
||||
"build": {
|
||||
"command": "echo \" @tanstack/form > All packages built! 📦\"",
|
||||
"dependsOn": ["rollup", "^build:types", "^build"]
|
||||
},
|
||||
"test:build": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"commands": ["bundlewatch", "pnpm run validatePackages"],
|
||||
"parallel": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"implicitDependencies": ["@tanstack/form-core", "@tanstack/react-form"]
|
||||
}
|
||||
@@ -1,28 +1,7 @@
|
||||
import babel from "rollup-plugin-babel";
|
||||
import commonjs from "rollup-plugin-commonjs";
|
||||
import external from "rollup-plugin-peer-deps-external";
|
||||
import resolve from "rollup-plugin-node-resolve";
|
||||
|
||||
import pkg from "./package.json";
|
||||
|
||||
export default {
|
||||
input: "src/index.js",
|
||||
output: [
|
||||
{
|
||||
file: pkg.main,
|
||||
format: "cjs",
|
||||
sourcemap: true
|
||||
require("ts-node").register({
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
},
|
||||
{
|
||||
file: pkg.module,
|
||||
format: "es",
|
||||
sourcemap: true
|
||||
}
|
||||
],
|
||||
plugins: [
|
||||
external(),
|
||||
babel(),
|
||||
resolve(),
|
||||
commonjs()
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = require("./rollup.config.ts");
|
||||
|
||||
412
rollup.config.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
import type { OutputOptions, RollupOptions } from 'rollup'
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import size from 'rollup-plugin-size'
|
||||
import visualizer from 'rollup-plugin-visualizer'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import nodeResolve from '@rollup/plugin-node-resolve'
|
||||
import commonJS from '@rollup/plugin-commonjs'
|
||||
import path from 'path'
|
||||
import preserveDirectives from 'rollup-plugin-preserve-directives'
|
||||
|
||||
type Options = {
|
||||
input: string | string[]
|
||||
packageDir: string
|
||||
external: RollupOptions['external']
|
||||
banner: string
|
||||
jsName: string
|
||||
outputFile: string
|
||||
globals: Record<string, string>
|
||||
forceDevEnv: boolean
|
||||
forceBundle: boolean
|
||||
}
|
||||
|
||||
const forceEnvPlugin = (type: 'development' | 'production') =>
|
||||
replace({
|
||||
'process.env.NODE_ENV': `"${type}"`,
|
||||
delimiters: ['', ''],
|
||||
preventAssignment: true,
|
||||
})
|
||||
|
||||
const babelPlugin = babel({
|
||||
babelHelpers: 'bundled',
|
||||
exclude: /node_modules/,
|
||||
extensions: ['.ts', '.tsx', '.native.ts'],
|
||||
})
|
||||
|
||||
export default function rollup(options: RollupOptions): RollupOptions[] {
|
||||
return [
|
||||
...buildConfigs({
|
||||
name: 'form-core',
|
||||
packageDir: 'packages/form-core',
|
||||
jsName: 'FormCore',
|
||||
outputFile: 'index',
|
||||
entryFile: ['src/index.ts'],
|
||||
globals: {},
|
||||
}),
|
||||
...buildConfigs({
|
||||
name: 'react-form',
|
||||
packageDir: 'packages/react-form',
|
||||
jsName: 'ReactForm',
|
||||
outputFile: 'index',
|
||||
entryFile: ['src/index.ts'],
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
'@tanstack/form-core': 'FormCore',
|
||||
'use-sync-external-store/shim/index.js': 'UseSyncExternalStore',
|
||||
'use-sync-external-store/shim/index.native.js':
|
||||
'UseSyncExternalStoreNative',
|
||||
},
|
||||
bundleUMDGlobals: [
|
||||
'@tanstack/form-core',
|
||||
'use-sync-external-store/shim/index.js',
|
||||
],
|
||||
}),
|
||||
// ...buildConfigs({
|
||||
// name: 'react-form-devtools',
|
||||
// packageDir: 'packages/react-form-devtools',
|
||||
// jsName: 'ReactFormDevtools',
|
||||
// outputFile: 'index',
|
||||
// entryFile: 'src/index.ts',
|
||||
// globals: {
|
||||
// react: 'React',
|
||||
// 'react-dom': 'ReactDOM',
|
||||
// '@tanstack/react-form': 'ReactForm',
|
||||
// '@tanstack/match-sorter-utils': 'MatchSorterUtils',
|
||||
// 'use-sync-external-store/shim/index.js': 'UseSyncExternalStore',
|
||||
// superjson: 'SuperJson',
|
||||
// },
|
||||
// bundleUMDGlobals: [
|
||||
// '@tanstack/match-sorter-utils',
|
||||
// 'use-sync-external-store/shim/index.js',
|
||||
// 'superjson',
|
||||
// ],
|
||||
// }),
|
||||
// ...buildConfigs({
|
||||
// name: 'react-form-devtools-prod',
|
||||
// packageDir: 'packages/react-form-devtools',
|
||||
// jsName: 'ReactFormDevtools',
|
||||
// outputFile: 'index.prod',
|
||||
// entryFile: 'src/index.ts',
|
||||
// globals: {
|
||||
// react: 'React',
|
||||
// 'react-dom': 'ReactDOM',
|
||||
// '@tanstack/react-form': 'ReactForm',
|
||||
// '@tanstack/match-sorter-utils': 'MatchSorterUtils',
|
||||
// 'use-sync-external-store/shim/index.js': 'UseSyncExternalStore',
|
||||
// superjson: 'SuperJson',
|
||||
// },
|
||||
// forceDevEnv: true,
|
||||
// forceBundle: true,
|
||||
// skipUmdBuild: true,
|
||||
// }),
|
||||
// ...buildConfigs({
|
||||
// name: 'solid-form',
|
||||
// packageDir: 'packages/solid-form',
|
||||
// jsName: 'SolidForm',
|
||||
// outputFile: 'index',
|
||||
// entryFile: 'src/index.ts',
|
||||
// globals: {
|
||||
// 'solid-js/store': 'SolidStore',
|
||||
// 'solid-js': 'Solid',
|
||||
// '@tanstack/form-core': 'FormCore',
|
||||
// },
|
||||
// bundleUMDGlobals: ['@tanstack/form-core'],
|
||||
// }),
|
||||
// ...buildConfigs({
|
||||
// name: 'vue-form',
|
||||
// packageDir: 'packages/vue-form',
|
||||
// jsName: 'VueForm',
|
||||
// outputFile: 'index',
|
||||
// entryFile: 'src/index.ts',
|
||||
// globals: {
|
||||
// '@tanstack/form-core': 'FormCore',
|
||||
// vue: 'Vue',
|
||||
// 'vue-demi': 'Vue',
|
||||
// '@tanstack/match-sorter-utils': 'MatchSorter',
|
||||
// '@vue/devtools-api': 'DevtoolsApi',
|
||||
// },
|
||||
// bundleUMDGlobals: [
|
||||
// '@tanstack/form-core',
|
||||
// '@tanstack/match-sorter-utils',
|
||||
// '@vue/devtools-api',
|
||||
// ],
|
||||
// }),
|
||||
]
|
||||
}
|
||||
|
||||
function buildConfigs(opts: {
|
||||
packageDir: string
|
||||
name: string
|
||||
jsName: string
|
||||
outputFile: string
|
||||
entryFile: string | string[]
|
||||
globals: Record<string, string>
|
||||
// This option allows to bundle specified dependencies for umd build
|
||||
bundleUMDGlobals?: string[]
|
||||
// Force prod env build
|
||||
forceDevEnv?: boolean
|
||||
forceBundle?: boolean
|
||||
skipUmdBuild?: boolean
|
||||
}): RollupOptions[] {
|
||||
const firstEntry = path.resolve(
|
||||
opts.packageDir,
|
||||
Array.isArray(opts.entryFile) ? opts.entryFile[0] : opts.entryFile,
|
||||
)
|
||||
const entries = Array.isArray(opts.entryFile)
|
||||
? opts.entryFile
|
||||
: [opts.entryFile]
|
||||
const input = entries.map((entry) => path.resolve(opts.packageDir, entry))
|
||||
const externalDeps = Object.keys(opts.globals)
|
||||
|
||||
const bundleUMDGlobals = opts.bundleUMDGlobals || []
|
||||
const umdExternal = externalDeps.filter(
|
||||
(external) => !bundleUMDGlobals.includes(external),
|
||||
)
|
||||
|
||||
const external = (moduleName) => externalDeps.includes(moduleName)
|
||||
const banner = '' //createBanner(opts.name)
|
||||
|
||||
const options: Options = {
|
||||
input,
|
||||
jsName: opts.jsName,
|
||||
outputFile: opts.outputFile,
|
||||
packageDir: opts.packageDir,
|
||||
external,
|
||||
banner,
|
||||
globals: opts.globals,
|
||||
forceDevEnv: opts.forceDevEnv || false,
|
||||
forceBundle: opts.forceBundle || false,
|
||||
}
|
||||
|
||||
let builds = [mjs(options), esm(options), cjs(options)]
|
||||
|
||||
if (!opts.skipUmdBuild) {
|
||||
builds = builds.concat([
|
||||
umdDev({ ...options, external: umdExternal, input: firstEntry }),
|
||||
umdProd({ ...options, external: umdExternal, input: firstEntry }),
|
||||
])
|
||||
}
|
||||
|
||||
return builds
|
||||
}
|
||||
|
||||
function mjs({
|
||||
input,
|
||||
packageDir,
|
||||
external,
|
||||
banner,
|
||||
outputFile,
|
||||
forceDevEnv,
|
||||
forceBundle,
|
||||
}: Options): RollupOptions {
|
||||
const bundleOutput: OutputOptions = {
|
||||
format: 'esm',
|
||||
file: `${packageDir}/build/lib/${outputFile}.mjs`,
|
||||
sourcemap: true,
|
||||
banner,
|
||||
}
|
||||
|
||||
const normalOutput: OutputOptions = {
|
||||
format: 'esm',
|
||||
dir: `${packageDir}/build/lib`,
|
||||
sourcemap: true,
|
||||
banner,
|
||||
preserveModules: true,
|
||||
entryFileNames: '[name].mjs',
|
||||
}
|
||||
|
||||
return {
|
||||
// ESM
|
||||
external,
|
||||
input,
|
||||
output: forceBundle ? bundleOutput : normalOutput,
|
||||
plugins: [
|
||||
babelPlugin,
|
||||
commonJS(),
|
||||
nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }),
|
||||
forceDevEnv ? forceEnvPlugin('development') : undefined,
|
||||
preserveDirectives(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function esm({
|
||||
input,
|
||||
packageDir,
|
||||
external,
|
||||
banner,
|
||||
outputFile,
|
||||
forceDevEnv,
|
||||
forceBundle,
|
||||
}: Options): RollupOptions {
|
||||
const bundleOutput: OutputOptions = {
|
||||
format: 'esm',
|
||||
file: `${packageDir}/build/lib/${outputFile}.esm.js`,
|
||||
sourcemap: true,
|
||||
banner,
|
||||
}
|
||||
|
||||
const normalOutput: OutputOptions = {
|
||||
format: 'esm',
|
||||
dir: `${packageDir}/build/lib`,
|
||||
sourcemap: true,
|
||||
banner,
|
||||
preserveModules: true,
|
||||
entryFileNames: '[name].esm.js',
|
||||
}
|
||||
|
||||
return {
|
||||
// ESM
|
||||
external,
|
||||
input,
|
||||
output: forceBundle ? bundleOutput : normalOutput,
|
||||
plugins: [
|
||||
babelPlugin,
|
||||
commonJS(),
|
||||
nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }),
|
||||
forceDevEnv ? forceEnvPlugin('development') : undefined,
|
||||
preserveDirectives(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function cjs({
|
||||
input,
|
||||
external,
|
||||
packageDir,
|
||||
banner,
|
||||
outputFile,
|
||||
forceDevEnv,
|
||||
forceBundle,
|
||||
}: Options): RollupOptions {
|
||||
const bundleOutput: OutputOptions = {
|
||||
format: 'cjs',
|
||||
file: `${packageDir}/build/lib/${outputFile}.js`,
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
banner,
|
||||
}
|
||||
|
||||
const normalOutput: OutputOptions = {
|
||||
format: 'cjs',
|
||||
dir: `${packageDir}/build/lib`,
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
banner,
|
||||
preserveModules: true,
|
||||
entryFileNames: '[name].js',
|
||||
}
|
||||
|
||||
return {
|
||||
// CJS
|
||||
external,
|
||||
input,
|
||||
output: forceBundle ? bundleOutput : normalOutput,
|
||||
plugins: [
|
||||
babelPlugin,
|
||||
commonJS(),
|
||||
nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }),
|
||||
forceDevEnv ? forceEnvPlugin('development') : undefined,
|
||||
replace({
|
||||
// TODO: figure out a better way to produce extensionless cjs imports
|
||||
"require('./logger.js')": "require('./logger')",
|
||||
"require('./reactBatchedUpdates.js')":
|
||||
"require('./reactBatchedUpdates')",
|
||||
"require('./useSyncExternalStore.js')":
|
||||
"require('./useSyncExternalStore')",
|
||||
preventAssignment: true,
|
||||
delimiters: ['', ''],
|
||||
}),
|
||||
preserveDirectives(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function umdDev({
|
||||
input,
|
||||
external,
|
||||
packageDir,
|
||||
outputFile,
|
||||
globals,
|
||||
banner,
|
||||
jsName,
|
||||
}: Options): RollupOptions {
|
||||
return {
|
||||
// UMD (Dev)
|
||||
external,
|
||||
input,
|
||||
output: {
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
file: `${packageDir}/build/umd/${outputFile}.development.js`,
|
||||
name: jsName,
|
||||
globals,
|
||||
banner,
|
||||
},
|
||||
plugins: [
|
||||
commonJS(),
|
||||
babelPlugin,
|
||||
nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }),
|
||||
forceEnvPlugin('development'),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function umdProd({
|
||||
input,
|
||||
external,
|
||||
packageDir,
|
||||
outputFile,
|
||||
globals,
|
||||
banner,
|
||||
jsName,
|
||||
}: Options): RollupOptions {
|
||||
return {
|
||||
// UMD (Prod)
|
||||
external,
|
||||
input,
|
||||
output: {
|
||||
format: 'umd',
|
||||
sourcemap: true,
|
||||
file: `${packageDir}/build/umd/${outputFile}.production.js`,
|
||||
name: jsName,
|
||||
globals,
|
||||
banner,
|
||||
},
|
||||
plugins: [
|
||||
commonJS(),
|
||||
babelPlugin,
|
||||
nodeResolve({ extensions: ['.ts', '.tsx', '.native.ts'] }),
|
||||
forceEnvPlugin('production'),
|
||||
terser({
|
||||
mangle: true,
|
||||
compress: true,
|
||||
}),
|
||||
size({}),
|
||||
visualizer({
|
||||
filename: `${packageDir}/build/stats-html.html`,
|
||||
gzipSize: true,
|
||||
}),
|
||||
visualizer({
|
||||
filename: `${packageDir}/build/stats.json`,
|
||||
json: true,
|
||||
gzipSize: true,
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function createBanner(libraryName: string) {
|
||||
return `/**
|
||||
* ${libraryName}
|
||||
*
|
||||
* Copyright (c) TanStack
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/`
|
||||
}
|
||||
56
scripts/config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import path from 'path'
|
||||
import type { BranchConfig, Package } from './types'
|
||||
|
||||
// TODO: List your npm packages here. The first package will be used as the versioner.
|
||||
export const packages: Package[] = [
|
||||
{ name: '@tanstack/form-core', packageDir: 'form-core', srcDir: 'src' },
|
||||
{ name: '@tanstack/react-form', packageDir: 'react-form', srcDir: 'src' },
|
||||
// {
|
||||
// name: '@tanstack/react-form-devtools',
|
||||
// packageDir: 'react-form-devtools',
|
||||
// srcDir: 'src',
|
||||
// },
|
||||
// {
|
||||
// name: '@tanstack/react-form-persist-client',
|
||||
// packageDir: 'react-form-persist-client',
|
||||
// srcDir: 'src',
|
||||
// },
|
||||
// {
|
||||
// name: '@tanstack/solid-form',
|
||||
// packageDir: 'solid-form',
|
||||
// srcDir: 'src',
|
||||
// },
|
||||
// {
|
||||
// name: '@tanstack/svelte-form',
|
||||
// packageDir: 'svelte-form',
|
||||
// srcDir: 'src',
|
||||
// },
|
||||
// {
|
||||
// name: '@tanstack/vue-form',
|
||||
// packageDir: 'vue-form',
|
||||
// srcDir: 'src',
|
||||
// },
|
||||
]
|
||||
|
||||
export const latestBranch = 'main'
|
||||
|
||||
export const branchConfigs: Record<string, BranchConfig> = {
|
||||
main: {
|
||||
prerelease: false,
|
||||
ghRelease: true,
|
||||
},
|
||||
next: {
|
||||
prerelease: true,
|
||||
ghRelease: true,
|
||||
},
|
||||
beta: {
|
||||
prerelease: true,
|
||||
ghRelease: true,
|
||||
},
|
||||
alpha: {
|
||||
prerelease: true,
|
||||
ghRelease: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const rootDir = path.resolve(__dirname, '..')
|
||||
8
scripts/project.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "scripts",
|
||||
"targets": {
|
||||
"test:eslint": { "command": "eslint ./scripts" }
|
||||
}
|
||||
}
|
||||
476
scripts/publish.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
import { branchConfigs, latestBranch, packages, rootDir } from './config'
|
||||
import type { BranchConfig, Commit, Package } from './types'
|
||||
|
||||
// Originally ported to TS from https://github.com/remix-run/react-router/tree/main/scripts/{version,publish}.js
|
||||
import path from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
import chalk from 'chalk'
|
||||
import jsonfile from 'jsonfile'
|
||||
import semver from 'semver'
|
||||
import currentGitBranch from 'current-git-branch'
|
||||
import parseCommit from '@commitlint/parse'
|
||||
import log from 'git-log-parser'
|
||||
import streamToArray from 'stream-to-array'
|
||||
import axios from 'axios'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
import type { PackageJson } from 'type-fest'
|
||||
|
||||
const releaseCommitMsg = (version: string) => `release: v${version}`
|
||||
|
||||
async function run() {
|
||||
const branchName: string =
|
||||
process.env.BRANCH ??
|
||||
// (process.env.PR_NUMBER ? `pr-${process.env.PR_NUMBER}` : currentGitBranch())
|
||||
currentGitBranch()
|
||||
|
||||
const branchConfig: BranchConfig | undefined = branchConfigs[branchName]
|
||||
|
||||
if (!branchConfig) {
|
||||
console.log(`No publish config found for branch: ${branchName}`)
|
||||
console.log('Exiting...')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const isLatestBranch = branchName === latestBranch
|
||||
const npmTag = isLatestBranch ? 'latest' : branchName
|
||||
|
||||
// Get tags
|
||||
let tags: string[] = execSync('git tag').toString().split('\n')
|
||||
|
||||
// Filter tags to our branch/pre-release combo
|
||||
tags = tags
|
||||
.filter((tag) => semver.valid(tag))
|
||||
.filter((tag) => {
|
||||
if (isLatestBranch) {
|
||||
return semver.prerelease(tag) == null
|
||||
}
|
||||
|
||||
return tag.includes(`-${branchName}`)
|
||||
})
|
||||
// sort by latest
|
||||
.sort(semver.compare)
|
||||
|
||||
// Get the latest tag
|
||||
let latestTag = [...tags].pop()
|
||||
|
||||
let range = `${latestTag}..HEAD`
|
||||
// let range = ``;
|
||||
|
||||
// If RELEASE_ALL is set via a commit subject or body, all packages will be
|
||||
// released regardless if they have changed files matching the package srcDir.
|
||||
let RELEASE_ALL = false
|
||||
|
||||
if (!latestTag || process.env.TAG) {
|
||||
if (process.env.TAG) {
|
||||
if (!process.env.TAG.startsWith('v')) {
|
||||
throw new Error(
|
||||
`process.env.TAG must start with "v", eg. v0.0.0. You supplied ${process.env.TAG}`,
|
||||
)
|
||||
}
|
||||
console.info(
|
||||
chalk.yellow(
|
||||
`Tag is set to ${process.env.TAG}. This will force release all packages. Publishing...`,
|
||||
),
|
||||
)
|
||||
RELEASE_ALL = true
|
||||
|
||||
// Is it a major version?
|
||||
if (!semver.patch(process.env.TAG) && !semver.minor(process.env.TAG)) {
|
||||
range = `beta..HEAD`
|
||||
latestTag = process.env.TAG
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'Could not find latest tag! To make a release tag of v0.0.1, run with TAG=v0.0.1',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`Git Range: ${range}`)
|
||||
|
||||
// Get the commits since the latest tag
|
||||
const commitsSinceLatestTag = (
|
||||
await new Promise<Commit[]>((resolve, reject) => {
|
||||
const strm = log.parse({
|
||||
_: range,
|
||||
})
|
||||
|
||||
streamToArray(strm, function (err: any, arr: any[]) {
|
||||
if (err) return reject(err)
|
||||
|
||||
Promise.all(
|
||||
arr.map(async (d) => {
|
||||
const parsed = await parseCommit(d.subject)
|
||||
|
||||
return { ...d, parsed }
|
||||
}),
|
||||
).then((res) => resolve(res.filter(Boolean)))
|
||||
})
|
||||
})
|
||||
).filter((commit: Commit) => {
|
||||
const exclude = [
|
||||
commit.subject.startsWith('Merge branch '), // No merge commits
|
||||
commit.subject.startsWith(releaseCommitMsg('')), // No example update commits
|
||||
].some(Boolean)
|
||||
|
||||
return !exclude
|
||||
})
|
||||
|
||||
console.info(
|
||||
`Parsing ${commitsSinceLatestTag.length} commits since ${latestTag}...`,
|
||||
)
|
||||
|
||||
// Pares the commit messsages, log them, and determine the type of release needed
|
||||
let recommendedReleaseLevel: number = commitsSinceLatestTag.reduce(
|
||||
(releaseLevel, commit) => {
|
||||
if (['fix', 'refactor', 'perf'].includes(commit.parsed.type!)) {
|
||||
releaseLevel = Math.max(releaseLevel, 0)
|
||||
}
|
||||
if (['feat'].includes(commit.parsed.type!)) {
|
||||
releaseLevel = Math.max(releaseLevel, 1)
|
||||
}
|
||||
if (commit.body.includes('BREAKING CHANGE')) {
|
||||
releaseLevel = Math.max(releaseLevel, 2)
|
||||
}
|
||||
if (
|
||||
commit.subject.includes('RELEASE_ALL') ||
|
||||
commit.body.includes('RELEASE_ALL')
|
||||
) {
|
||||
RELEASE_ALL = true
|
||||
}
|
||||
|
||||
return releaseLevel
|
||||
},
|
||||
-1,
|
||||
)
|
||||
|
||||
const changedFiles: string[] = process.env.TAG
|
||||
? []
|
||||
: execSync(`git diff ${latestTag} --name-only`)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
|
||||
const changedPackages = RELEASE_ALL
|
||||
? packages
|
||||
: changedFiles.reduce((acc, file) => {
|
||||
const pkg = packages.find((p) =>
|
||||
file.startsWith(path.join('packages', p.packageDir, p.srcDir)),
|
||||
)
|
||||
if (pkg && !acc.find((d) => d.name === pkg.name)) {
|
||||
acc.push(pkg)
|
||||
}
|
||||
return acc
|
||||
}, [] as Package[])
|
||||
|
||||
// If a package has a dependency that has been updated, we need to update the
|
||||
// package that depends on it as well.
|
||||
// run this multiple times so that dependencies of dependencies are also included
|
||||
for (let runs = 0; runs < 3; runs++) {
|
||||
for (const pkg of packages) {
|
||||
const packageJson = await readPackageJson(
|
||||
path.resolve(rootDir, 'packages', pkg.packageDir, 'package.json'),
|
||||
)
|
||||
const allDependencies = Object.keys(
|
||||
Object.assign(
|
||||
{},
|
||||
packageJson.dependencies ?? {},
|
||||
packageJson.peerDependencies ?? {},
|
||||
),
|
||||
)
|
||||
|
||||
if (
|
||||
allDependencies.find((dep) =>
|
||||
changedPackages.find((d) => d.name === dep),
|
||||
) &&
|
||||
!changedPackages.find((d) => d.name === pkg.name)
|
||||
) {
|
||||
console.info(
|
||||
'adding package dependency',
|
||||
pkg.name,
|
||||
'to changed packages',
|
||||
)
|
||||
changedPackages.push(pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!process.env.TAG) {
|
||||
if (recommendedReleaseLevel === 2) {
|
||||
console.info(
|
||||
`Major versions releases must be tagged and released manually.`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (recommendedReleaseLevel === -1) {
|
||||
console.info(
|
||||
`There have been no changes since the release of ${latestTag} that require a new version. You're good!`,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function getSorterFn<TItem>(sorters: ((d: TItem) => any)[]) {
|
||||
return (a: TItem, b: TItem) => {
|
||||
let i = 0
|
||||
|
||||
sorters.some((sorter) => {
|
||||
const sortedA = sorter(a)
|
||||
const sortedB = sorter(b)
|
||||
if (sortedA > sortedB) {
|
||||
i = 1
|
||||
return true
|
||||
}
|
||||
if (sortedA < sortedB) {
|
||||
i = -1
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
const changelogCommitsMd = process.env.TAG
|
||||
? `Manual Release: ${process.env.TAG}`
|
||||
: await Promise.all(
|
||||
Object.entries(
|
||||
commitsSinceLatestTag.reduce((acc, next) => {
|
||||
const type = next.parsed.type?.toLowerCase() ?? 'other'
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[type]: [...(acc[type] || []), next],
|
||||
}
|
||||
}, {} as Record<string, Commit[]>),
|
||||
)
|
||||
.sort(
|
||||
getSorterFn([
|
||||
([d]) =>
|
||||
[
|
||||
'other',
|
||||
'examples',
|
||||
'docs',
|
||||
'chore',
|
||||
'refactor',
|
||||
'perf',
|
||||
'fix',
|
||||
'feat',
|
||||
].indexOf(d),
|
||||
]),
|
||||
)
|
||||
.reverse()
|
||||
.map(async ([type, commits]) => {
|
||||
return Promise.all(
|
||||
commits.map(async (commit) => {
|
||||
let username = ''
|
||||
|
||||
if (process.env.GH_TOKEN) {
|
||||
const query = `${
|
||||
commit.author.email || commit.committer.email
|
||||
}`
|
||||
|
||||
const res = await axios.get(
|
||||
'https://api.github.com/search/users',
|
||||
{
|
||||
params: {
|
||||
q: query,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `token ${process.env.GH_TOKEN}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
username = res.data.items[0]?.login
|
||||
}
|
||||
|
||||
const scope = commit.parsed.scope
|
||||
? `${commit.parsed.scope}: `
|
||||
: ''
|
||||
const subject = commit.parsed.subject || commit.subject
|
||||
|
||||
return `- ${scope}${subject} (${commit.commit.short}) ${
|
||||
username
|
||||
? `by @${username}`
|
||||
: `by ${commit.author.name || commit.author.email}`
|
||||
}`
|
||||
}),
|
||||
).then((c) => [type, c] as const)
|
||||
}),
|
||||
).then((groups) => {
|
||||
return groups
|
||||
.map(([type, commits]) => {
|
||||
return [`### ${capitalize(type)}`, commits.join('\n')].join('\n\n')
|
||||
})
|
||||
.join('\n\n')
|
||||
})
|
||||
|
||||
if (process.env.TAG && recommendedReleaseLevel === -1) {
|
||||
recommendedReleaseLevel = 0
|
||||
}
|
||||
|
||||
const releaseType = branchConfig.prerelease
|
||||
? 'prerelease'
|
||||
: ({ 0: 'patch', 1: 'minor', 2: 'major' } as const)[recommendedReleaseLevel]
|
||||
|
||||
if (!releaseType) {
|
||||
throw new Error(`Invalid release level: ${recommendedReleaseLevel}`)
|
||||
}
|
||||
|
||||
const version = process.env.TAG
|
||||
? semver.parse(process.env.TAG)?.version
|
||||
: semver.inc(latestTag!, releaseType, npmTag)
|
||||
|
||||
if (!version) {
|
||||
throw new Error(
|
||||
`Invalid version increment from semver.inc(${[
|
||||
latestTag,
|
||||
recommendedReleaseLevel,
|
||||
branchConfig.prerelease,
|
||||
].join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
const changelogMd = [
|
||||
`Version ${version} - ${DateTime.now().toLocaleString(
|
||||
DateTime.DATETIME_SHORT,
|
||||
)}`,
|
||||
`## Changes`,
|
||||
changelogCommitsMd,
|
||||
`## Packages`,
|
||||
changedPackages.map((d) => `- ${d.name}@${version}`).join('\n'),
|
||||
].join('\n\n')
|
||||
|
||||
console.info('Generating changelog...')
|
||||
console.info()
|
||||
console.info(changelogMd)
|
||||
console.info()
|
||||
|
||||
if (changedPackages.length === 0) {
|
||||
console.info('No packages have been affected.')
|
||||
return
|
||||
}
|
||||
|
||||
console.info('Building packages...')
|
||||
execSync(`pnpm run build --skip-nx-cache`, {
|
||||
encoding: 'utf8',
|
||||
stdio: 'inherit',
|
||||
})
|
||||
console.info('')
|
||||
|
||||
console.info('Validating packages...')
|
||||
execSync(`pnpm run validatePackages`, { encoding: 'utf8', stdio: 'inherit' })
|
||||
|
||||
console.info(`Updating all changed packages to version ${version}...`)
|
||||
// Update each package to the new version
|
||||
for (const pkg of changedPackages) {
|
||||
console.info(` Updating ${pkg.name} version to ${version}...`)
|
||||
|
||||
await updatePackageJson(
|
||||
path.resolve(rootDir, 'packages', pkg.packageDir, 'package.json'),
|
||||
(config) => {
|
||||
config.version = version
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (!process.env.CI) {
|
||||
console.warn(
|
||||
`This is a dry run for version ${version}. Push to CI to publish for real or set CI=true to override!`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Tag and commit
|
||||
console.info(`Creating new git tag v${version}`)
|
||||
execSync(`git tag -a -m "v${version}" v${version}`)
|
||||
|
||||
const taggedVersion = getTaggedVersion()
|
||||
if (!taggedVersion) {
|
||||
throw new Error(
|
||||
'Missing the tagged release version. Something weird is afoot!',
|
||||
)
|
||||
}
|
||||
|
||||
console.info()
|
||||
console.info(`Publishing all packages to npm with tag "${npmTag}"`)
|
||||
|
||||
// Publish each package
|
||||
changedPackages.forEach((pkg) => {
|
||||
const packageDir = path.join(rootDir, 'packages', pkg.packageDir)
|
||||
const cmd = `cd ${packageDir} && pnpm publish --tag ${npmTag} --access=public --no-git-checks`
|
||||
console.info(
|
||||
` Publishing ${pkg.name}@${version} to npm with tag "${npmTag}"...`,
|
||||
)
|
||||
execSync(cmd, {
|
||||
stdio: [process.stdin, process.stdout, process.stderr],
|
||||
})
|
||||
})
|
||||
|
||||
console.info()
|
||||
|
||||
console.info(`Pushing new tags to branch.`)
|
||||
execSync(`git push --tags`)
|
||||
console.info(` Pushed tags to branch.`)
|
||||
|
||||
if (branchConfig.ghRelease) {
|
||||
console.info(`Creating github release...`)
|
||||
// Stringify the markdown to excape any quotes
|
||||
execSync(
|
||||
`gh release create v${version} ${
|
||||
!isLatestBranch ? '--prerelease' : ''
|
||||
} --notes '${changelogMd.replace(/'/g, '"')}'`,
|
||||
)
|
||||
console.info(` Github release created.`)
|
||||
|
||||
console.info(`Committing changes...`)
|
||||
execSync(`git add -A && git commit -m "${releaseCommitMsg(version)}"`)
|
||||
console.info()
|
||||
console.info(` Committed Changes.`)
|
||||
console.info(`Pushing changes...`)
|
||||
execSync(`git push`)
|
||||
console.info()
|
||||
console.info(` Changes pushed.`)
|
||||
} else {
|
||||
console.info(`Skipping github release and change commit.`)
|
||||
}
|
||||
|
||||
console.info(`Pushing tags...`)
|
||||
execSync(`git push --tags`)
|
||||
console.info()
|
||||
console.info(` Tags pushed.`)
|
||||
console.info(`All done!`)
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.info(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
function capitalize(str: string) {
|
||||
return str.slice(0, 1).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
async function readPackageJson(pathName: string) {
|
||||
return (await jsonfile.readFile(pathName)) as PackageJson
|
||||
}
|
||||
|
||||
async function updatePackageJson(
|
||||
pathName: string,
|
||||
transform: (json: PackageJson) => Promise<void> | void,
|
||||
) {
|
||||
const json = await readPackageJson(pathName)
|
||||
await transform(json)
|
||||
await jsonfile.writeFile(pathName, json, {
|
||||
spaces: 2,
|
||||
})
|
||||
}
|
||||
|
||||
function getTaggedVersion() {
|
||||
const output = execSync('git tag --list --points-at HEAD').toString()
|
||||
return output.replace(/^v|\n+$/g, '')
|
||||
}
|
||||
20
scripts/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"checkJs": true
|
||||
},
|
||||
"ts-node": {
|
||||
"transpileOnly": true,
|
||||
"files": true,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"inlineSources": true
|
||||
}
|
||||
}
|
||||
}
|
||||
46
scripts/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export type Commit = {
|
||||
commit: CommitOrTree
|
||||
tree: CommitOrTree
|
||||
author: AuthorOrCommitter
|
||||
committer: AuthorOrCommitter
|
||||
subject: string
|
||||
body: string
|
||||
parsed: Parsed
|
||||
}
|
||||
|
||||
export type CommitOrTree = {
|
||||
long: string
|
||||
short: string
|
||||
}
|
||||
|
||||
export type AuthorOrCommitter = {
|
||||
name: string
|
||||
email: string
|
||||
date: string
|
||||
}
|
||||
|
||||
export type Parsed = {
|
||||
type: string | null
|
||||
scope?: string | null
|
||||
subject: string
|
||||
merge?: null
|
||||
header: string
|
||||
body?: null
|
||||
footer?: null
|
||||
notes?: null[] | null
|
||||
references?: null[] | null
|
||||
mentions?: null[] | null
|
||||
revert?: null
|
||||
raw: string
|
||||
}
|
||||
|
||||
export type Package = {
|
||||
name: string
|
||||
packageDir: string
|
||||
srcDir: string
|
||||
}
|
||||
|
||||
export type BranchConfig = {
|
||||
prerelease: boolean
|
||||
ghRelease: boolean
|
||||
}
|
||||
64
scripts/validate-packages.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { packages, rootDir } from './config'
|
||||
import path from 'path'
|
||||
import fsp from 'fs/promises'
|
||||
import jsonfile from 'jsonfile'
|
||||
|
||||
import type { PackageJson } from 'type-fest'
|
||||
|
||||
async function run() {
|
||||
console.info('Validating packages...')
|
||||
const failedValidations: string[] = []
|
||||
|
||||
await Promise.all(
|
||||
packages.map(async (pkg) => {
|
||||
const pkgJson = await readPackageJson(
|
||||
path.resolve(rootDir, 'packages', pkg.packageDir, 'package.json'),
|
||||
)
|
||||
|
||||
const entries =
|
||||
pkg.name === '@tanstack/svelte-form'
|
||||
? (['types', 'module'] as const)
|
||||
: (['main', 'types', 'module'] as const)
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async (entryKey) => {
|
||||
const entry = pkgJson[entryKey] as string
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`Missing entry for "${entryKey}" in ${pkg.packageDir}/package.json!`,
|
||||
)
|
||||
}
|
||||
|
||||
const filePath = path.resolve(
|
||||
rootDir,
|
||||
'packages',
|
||||
pkg.packageDir,
|
||||
entry,
|
||||
)
|
||||
|
||||
try {
|
||||
await fsp.access(filePath)
|
||||
} catch (err) {
|
||||
failedValidations.push(`Missing build file: ${filePath}`)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
console.info('')
|
||||
if (failedValidations.length > 0) {
|
||||
throw new Error(
|
||||
'Some packages failed validation:\n\n' + failedValidations.join('\n'),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.info(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
async function readPackageJson(pathName: string) {
|
||||
return (await jsonfile.readFile(pathName)) as PackageJson
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function useAsyncDebounce(defaultFn, defaultWait = 0) {
|
||||
const debounceRef = React.useRef({})
|
||||
debounceRef.current.defaultFn = defaultFn
|
||||
debounceRef.current.defaultWait = defaultWait
|
||||
|
||||
const debounce = React.useCallback(
|
||||
async (
|
||||
fn = debounceRef.current.defaultFn,
|
||||
wait = debounceRef.current.defaultWait
|
||||
) => {
|
||||
if (!debounceRef.current.promise) {
|
||||
debounceRef.current.promise = new Promise((resolve, reject) => {
|
||||
debounceRef.current.resolve = resolve
|
||||
debounceRef.current.reject = reject
|
||||
})
|
||||
}
|
||||
|
||||
if (debounceRef.current.timeout) {
|
||||
clearTimeout(debounceRef.current.timeout)
|
||||
}
|
||||
|
||||
debounceRef.current.timeout = setTimeout(async () => {
|
||||
delete debounceRef.current.timeout
|
||||
try {
|
||||
debounceRef.current.resolve(await fn())
|
||||
} catch (err) {
|
||||
debounceRef.current.reject(err)
|
||||
} finally {
|
||||
delete debounceRef.current.promise
|
||||
delete debounceRef.current.resolve
|
||||
delete debounceRef.current.reject
|
||||
}
|
||||
}, wait)
|
||||
|
||||
return debounceRef.current.promise
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return debounce
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
import React from 'react'
|
||||
//
|
||||
import useAsyncDebounce from './useAsyncDebounce'
|
||||
import useFormContext from './useFormContext'
|
||||
import useFieldScope from './useFieldScope'
|
||||
import { getFieldID } from '../utils'
|
||||
|
||||
let uid = 0
|
||||
|
||||
const methodMap = [
|
||||
'setFieldValue',
|
||||
'setFieldMeta',
|
||||
'pushFieldValue',
|
||||
'insertFieldValue',
|
||||
'removeFieldValue',
|
||||
'swapFieldValues',
|
||||
]
|
||||
|
||||
const defaultDefaultMeta = {
|
||||
error: null,
|
||||
isTouched: false,
|
||||
isValidating: false,
|
||||
}
|
||||
|
||||
export default function useField(
|
||||
fieldName,
|
||||
{
|
||||
defaultValue,
|
||||
defaultIsTouched = false,
|
||||
defaultError = null,
|
||||
defaultMeta = defaultDefaultMeta,
|
||||
validatePristine,
|
||||
validate,
|
||||
filterValue,
|
||||
formContext: manualFormContext,
|
||||
} = {}
|
||||
) {
|
||||
if (!fieldName) {
|
||||
throw new Error(
|
||||
`useField: A field is required to use this hook. eg, useField('myField', options)`
|
||||
)
|
||||
}
|
||||
|
||||
const formApiRef = React.useRef()
|
||||
const fieldApiRef = React.useRef({})
|
||||
|
||||
let formApi = useFormContext(manualFormContext)
|
||||
|
||||
const instanceIDRef = React.useRef(uid++)
|
||||
const instanceID = instanceIDRef.current
|
||||
|
||||
// Support field prefixing from FieldScope
|
||||
let fieldPrefix = ''
|
||||
if (formApi.fieldName) {
|
||||
fieldPrefix = `${formApi.fieldName}.`
|
||||
formApi = formApi.form
|
||||
}
|
||||
fieldName = fieldPrefix + fieldName
|
||||
|
||||
// Create a debounce for this field hook instance (not all instances)
|
||||
const debounce = useAsyncDebounce()
|
||||
|
||||
// An escape hatch for accessing latest formAPI
|
||||
formApiRef.current = formApi
|
||||
|
||||
// Get the field value, meta, and metaRef
|
||||
const preValue = formApi.getFieldValue(fieldName)
|
||||
const preMeta = formApi.getFieldMeta(fieldName)
|
||||
const __metaRef = formApi.__getFieldMetaRef(fieldName)
|
||||
|
||||
// Handle default value
|
||||
const value = React.useMemo(
|
||||
() =>
|
||||
typeof preValue === 'undefined' && typeof defaultValue !== 'undefined'
|
||||
? defaultValue
|
||||
: preValue,
|
||||
[defaultValue, preValue]
|
||||
)
|
||||
|
||||
// Handle default meta
|
||||
const meta = React.useMemo(
|
||||
() =>
|
||||
typeof preMeta === 'undefined'
|
||||
? {
|
||||
...defaultMeta,
|
||||
error: defaultError,
|
||||
isTouched: defaultIsTouched,
|
||||
}
|
||||
: preMeta,
|
||||
[defaultError, defaultMeta, defaultIsTouched, preMeta]
|
||||
)
|
||||
|
||||
// Create the fieldApi
|
||||
const fieldApi = React.useMemo(
|
||||
() => ({
|
||||
value,
|
||||
meta,
|
||||
form: formApi,
|
||||
fieldName,
|
||||
}),
|
||||
[fieldName, formApi, meta, value]
|
||||
)
|
||||
|
||||
// Keep the fieldApiRef up to date
|
||||
fieldApiRef.current = fieldApi
|
||||
fieldApiRef.current.__filterValue = filterValue
|
||||
fieldApiRef.current.__validate = validate
|
||||
|
||||
// Let's scope some field-level methods for convenience
|
||||
const [
|
||||
setValue,
|
||||
setMeta,
|
||||
pushValue,
|
||||
insertValue,
|
||||
removeValue,
|
||||
swapValues,
|
||||
] = methodMap.map(d => {
|
||||
// Since this array is stable and always the same, we can disable
|
||||
// the react-hooks linter here:
|
||||
|
||||
// eslint-disable-next-line
|
||||
return React.useCallback(
|
||||
(...args) => formApiRef.current[d](fieldName, ...args),
|
||||
// eslint-disable-next-line
|
||||
[fieldName]
|
||||
)
|
||||
})
|
||||
|
||||
// Let's scope some field-level methods for convenience
|
||||
const [
|
||||
setFieldValue,
|
||||
setFieldMeta,
|
||||
pushFieldValue,
|
||||
insertFieldValue,
|
||||
removeFieldValue,
|
||||
swapFieldValues,
|
||||
] = methodMap.map(d => {
|
||||
// Since this array is stable and always the same, we can disable
|
||||
// the react-hooks linter here:
|
||||
|
||||
// eslint-disable-next-line
|
||||
return React.useCallback(
|
||||
(subField, ...args) =>
|
||||
formApiRef.current[d](`${fieldName}.${subField}`, ...args),
|
||||
// eslint-disable-next-line
|
||||
[fieldName]
|
||||
)
|
||||
})
|
||||
|
||||
const runValidation = React.useCallback(async () => {
|
||||
if (!fieldApiRef.current.__validate) {
|
||||
return
|
||||
}
|
||||
setMeta({ isValidating: true })
|
||||
|
||||
// Use the validationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
const id = (__metaRef.current.validationCount || 0) + 1
|
||||
__metaRef.current.validationCount = id
|
||||
|
||||
const checkLatest = () => id === __metaRef.current.validationCount
|
||||
|
||||
if (!__metaRef.current.validationPromise) {
|
||||
__metaRef.current.validationPromise = new Promise((resolve, reject) => {
|
||||
__metaRef.current.validationResolve = resolve
|
||||
__metaRef.current.validationReject = reject
|
||||
})
|
||||
}
|
||||
|
||||
const doValidate = async () => {
|
||||
try {
|
||||
const error = await fieldApiRef.current.__validate(
|
||||
fieldApiRef.current.value,
|
||||
fieldApiRef.current
|
||||
)
|
||||
|
||||
if (checkLatest()) {
|
||||
setMeta({ isValidating: false })
|
||||
if (typeof error !== 'undefined') {
|
||||
if (error) {
|
||||
if (typeof error === 'string') {
|
||||
setMeta({ error })
|
||||
}
|
||||
} else {
|
||||
setMeta({ error: null })
|
||||
}
|
||||
}
|
||||
__metaRef.current.validationResolve()
|
||||
}
|
||||
} catch (error) {
|
||||
if (checkLatest()) {
|
||||
__metaRef.current.validationReject(error)
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
if (checkLatest()) {
|
||||
setMeta({ isValidating: false })
|
||||
delete __metaRef.current.validationPromise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doValidate()
|
||||
|
||||
return __metaRef.current.validationPromise
|
||||
}, [__metaRef, setMeta])
|
||||
|
||||
const getInputProps = React.useCallback(
|
||||
({ onChange, onBlur, ...rest } = {}) => {
|
||||
return {
|
||||
value,
|
||||
onChange: e => {
|
||||
setValue(e.target.value)
|
||||
if (onChange) {
|
||||
onChange(e)
|
||||
}
|
||||
},
|
||||
onBlur: e => {
|
||||
setMeta({ isTouched: true })
|
||||
if (onBlur) {
|
||||
onBlur(e)
|
||||
}
|
||||
},
|
||||
...rest,
|
||||
}
|
||||
},
|
||||
[setMeta, setValue, value]
|
||||
)
|
||||
|
||||
const FieldScope = useFieldScope(fieldApi)
|
||||
|
||||
// Fill in the rest of the fieldApi
|
||||
Object.assign(fieldApi, {
|
||||
__metaRef,
|
||||
setValue,
|
||||
setMeta,
|
||||
pushValue,
|
||||
insertValue,
|
||||
removeValue,
|
||||
swapValues,
|
||||
setFieldValue,
|
||||
setFieldMeta,
|
||||
pushFieldValue,
|
||||
insertFieldValue,
|
||||
removeFieldValue,
|
||||
swapFieldValues,
|
||||
debounce,
|
||||
runValidation,
|
||||
getInputProps,
|
||||
FieldScope,
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
const { current: meta } = formApiRef.current.__getFieldMetaRef(fieldName)
|
||||
|
||||
meta.instanceRefs = meta.instanceRefs || {}
|
||||
meta.instanceRefs[instanceID] = fieldApiRef
|
||||
|
||||
const fieldID = getFieldID(fieldName)
|
||||
|
||||
return () => {
|
||||
delete meta.instanceRefs[instanceID]
|
||||
if (!Object.keys(meta.instanceRefs).length) {
|
||||
fieldApiRef.current.setMeta(() => undefined)
|
||||
delete formApiRef.current.__fieldMetaRefs[fieldID]
|
||||
}
|
||||
}
|
||||
}, [fieldName, instanceID])
|
||||
|
||||
// The default value effect handler
|
||||
React.useEffect(() => {
|
||||
if (typeof preValue === 'undefined' && typeof value !== 'undefined') {
|
||||
setValue(value, { isTouched: false })
|
||||
}
|
||||
}, [preValue, setValue, value])
|
||||
|
||||
// The default meta effect handler
|
||||
React.useEffect(() => {
|
||||
if (typeof preMeta === 'undefined' && typeof meta !== 'undefined') {
|
||||
setMeta(meta)
|
||||
}
|
||||
}, [fieldName, meta, preMeta, setMeta, setValue, value])
|
||||
|
||||
// When the form gets dirty and when the value changes, run the validation
|
||||
React.useEffect(() => {
|
||||
if (!validatePristine && !meta.isTouched) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
runValidation(value)
|
||||
} catch (err) {
|
||||
console.error('An error occurred during validation', err)
|
||||
}
|
||||
}, [meta.isTouched, runValidation, validatePristine, value])
|
||||
|
||||
return fieldApiRef.current
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react'
|
||||
//
|
||||
|
||||
import { FormContextProvider } from './useFormContext'
|
||||
|
||||
export default function useFieldScope(contextValue) {
|
||||
const FieldScopeRef = React.useRef()
|
||||
const FieldScopeApiRef = React.useRef()
|
||||
|
||||
FieldScopeApiRef.current = contextValue
|
||||
|
||||
// Create a new form element
|
||||
if (!FieldScopeRef.current) {
|
||||
FieldScopeRef.current = function Field({ children }) {
|
||||
return (
|
||||
<FormContextProvider value={FieldScopeApiRef.current}>
|
||||
{children}
|
||||
</FormContextProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return FieldScopeRef.current
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
import React from 'react'
|
||||
//
|
||||
import useAsyncDebounce from './useAsyncDebounce'
|
||||
import useFormElement from './useFormElement'
|
||||
import { someObject, getBy, setBy, getFieldID } from '../utils'
|
||||
|
||||
const defaultDefaultValue = {}
|
||||
|
||||
function makeState(decor) {
|
||||
return {
|
||||
meta: {
|
||||
isSubmitting: false,
|
||||
isTouched: false,
|
||||
isSubmitted: false,
|
||||
submissionAttempts: 0,
|
||||
},
|
||||
__fieldMeta: {},
|
||||
...decor,
|
||||
}
|
||||
}
|
||||
|
||||
export default function useForm({
|
||||
onSubmit,
|
||||
defaultValues = defaultDefaultValue,
|
||||
validate,
|
||||
validatePristine,
|
||||
debugForm,
|
||||
} = {}) {
|
||||
let [{ values, meta, __fieldMeta }, setState] = React.useState(() =>
|
||||
makeState({
|
||||
values: defaultValues,
|
||||
})
|
||||
)
|
||||
|
||||
const [shouldResubmit, setShouldResubmit] = React.useState(false)
|
||||
const apiRef = React.useRef()
|
||||
const metaRef = React.useRef({})
|
||||
const __fieldMetaRefsRef = React.useRef({})
|
||||
|
||||
// Keep validate up to date with the latest version
|
||||
metaRef.current.validate = validate
|
||||
|
||||
const fieldsAreValidating = someObject(
|
||||
__fieldMeta,
|
||||
field => field && field.isValidating
|
||||
)
|
||||
const fieldsAreValid = !someObject(__fieldMeta, field => field && field.error)
|
||||
|
||||
// Can we submit this form?
|
||||
const isValid = !fieldsAreValidating && fieldsAreValid && !meta.error
|
||||
|
||||
const canSubmit = isValid && !meta.isValidating && !meta.isSubmitting
|
||||
|
||||
// Decorate form meta
|
||||
meta = React.useMemo(
|
||||
() => ({
|
||||
...meta,
|
||||
fieldsAreValidating,
|
||||
fieldsAreValid,
|
||||
isValid,
|
||||
canSubmit,
|
||||
}),
|
||||
[meta, fieldsAreValidating, fieldsAreValid, isValid, canSubmit]
|
||||
)
|
||||
|
||||
// We want the apiRef to change every time state updates
|
||||
const api = React.useMemo(
|
||||
() => ({
|
||||
values,
|
||||
meta,
|
||||
__fieldMeta,
|
||||
debugForm,
|
||||
}),
|
||||
[debugForm, __fieldMeta, meta, values]
|
||||
)
|
||||
// Keep the apiRef up to date with the latest version of the api
|
||||
apiRef.current = api
|
||||
|
||||
const reset = React.useCallback(() => {
|
||||
setState(() =>
|
||||
makeState({
|
||||
values: defaultValues,
|
||||
})
|
||||
)
|
||||
}, [defaultValues, setState])
|
||||
|
||||
// On submit
|
||||
const handleSubmit = React.useCallback(async (e = {}) => {
|
||||
if (e.persist) e.persist()
|
||||
if (e.preventDefault) e.preventDefault()
|
||||
|
||||
// This lets sub-forms with form elements (despite them being invalid HTML)
|
||||
// handle submissions without triggering parent forms
|
||||
if (e.__handled) {
|
||||
return
|
||||
}
|
||||
e.__handled = true
|
||||
|
||||
// Don't let invalid forms submit
|
||||
if (!apiRef.current.meta.isValid) {
|
||||
// If the form can't submit, let's trigger all of the fields
|
||||
// to be touched. Thus, their validations will run
|
||||
apiRef.current.setMeta({ isSubmitting: false })
|
||||
return
|
||||
}
|
||||
|
||||
apiRef.current.setMeta({ isSubmitting: true })
|
||||
|
||||
let needsResubmit = false
|
||||
|
||||
const fieldValidationPromises = []
|
||||
|
||||
Object.keys(apiRef.current.__fieldMetaRefs).forEach(key => {
|
||||
const { current: fieldMeta } = apiRef.current.__fieldMetaRefs[key]
|
||||
Object.keys(fieldMeta.instanceRefs).forEach(key => {
|
||||
const { current: fieldInstance } = fieldMeta.instanceRefs[key]
|
||||
// If any fields are not touched
|
||||
if (!fieldInstance.meta.isTouched) {
|
||||
// Mark them as touched
|
||||
fieldInstance.setMeta({ isTouched: true })
|
||||
// Likewise, if they need validation
|
||||
if (fieldInstance.__validate) {
|
||||
// Run their validation and keep track of the
|
||||
// promise
|
||||
fieldValidationPromises.push(fieldInstance.runValidation())
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// If any validation needed to be run
|
||||
if (fieldValidationPromises.length) {
|
||||
// Mark for resubmission
|
||||
needsResubmit = true
|
||||
}
|
||||
|
||||
if (!apiRef.current.meta.isTouched) {
|
||||
// Mark for resubmission
|
||||
needsResubmit = true
|
||||
|
||||
// Mark the form as touched
|
||||
apiRef.current.setMeta(old => ({
|
||||
...old,
|
||||
isTouched: true,
|
||||
}))
|
||||
}
|
||||
|
||||
if (needsResubmit) {
|
||||
// Wait for any field validations to complete
|
||||
await Promise.all(fieldValidationPromises)
|
||||
// Be sure to run validation for the form
|
||||
// and wait for it to complete
|
||||
await apiRef.current.runValidation()
|
||||
// Then rerun the submission attempt
|
||||
e.__handled = false
|
||||
setShouldResubmit(e || true)
|
||||
// Do not continue
|
||||
return
|
||||
}
|
||||
|
||||
apiRef.current.setMeta(old => ({
|
||||
...old,
|
||||
// Submittion attempts mark the form as not submitted
|
||||
isSubmitted: false,
|
||||
// Count submission attempts
|
||||
submissionAttempts: old.submissionAttempts + 1,
|
||||
}))
|
||||
|
||||
try {
|
||||
// Run the submit code
|
||||
await apiRef.current.onSubmit(apiRef.current.values, apiRef.current)
|
||||
|
||||
apiRef.current.setMeta({ isSubmitted: true })
|
||||
} catch (err) {
|
||||
throw err
|
||||
} finally {
|
||||
apiRef.current.setMeta({ isSubmitting: false })
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Create a debounce for this field hook instance (not all instances)
|
||||
const debounce = useAsyncDebounce()
|
||||
|
||||
const setMeta = React.useCallback(
|
||||
updater => {
|
||||
setState(old => ({
|
||||
...old,
|
||||
meta:
|
||||
typeof updater === 'function'
|
||||
? updater(old.meta)
|
||||
: { ...old.meta, ...updater },
|
||||
}))
|
||||
},
|
||||
[setState]
|
||||
)
|
||||
|
||||
const runValidation = React.useCallback(() => {
|
||||
if (!metaRef.current.validate) {
|
||||
return
|
||||
}
|
||||
apiRef.current.setMeta({ isValidating: true })
|
||||
|
||||
// Use the validationCount for all field instances to
|
||||
// track freshness of the validation
|
||||
const id = (metaRef.current.validationCount || 0) + 1
|
||||
metaRef.current.validationCount = id
|
||||
|
||||
const checkLatest = () => id === metaRef.current.validationCount
|
||||
|
||||
if (!metaRef.current.validationPromise) {
|
||||
metaRef.current.validationPromise = new Promise((resolve, reject) => {
|
||||
metaRef.current.validationResolve = resolve
|
||||
metaRef.current.validationReject = reject
|
||||
})
|
||||
}
|
||||
|
||||
const doValidation = async () => {
|
||||
try {
|
||||
const error = await metaRef.current.validate(
|
||||
apiRef.current.values,
|
||||
apiRef.current
|
||||
)
|
||||
|
||||
if (checkLatest()) {
|
||||
apiRef.current.setMeta({ isValidating: false })
|
||||
if (typeof error !== 'undefined') {
|
||||
if (error) {
|
||||
if (typeof error === 'string') {
|
||||
apiRef.current.setMeta({ error })
|
||||
}
|
||||
} else {
|
||||
apiRef.current.setMeta({ error: null })
|
||||
}
|
||||
}
|
||||
metaRef.current.validationResolve()
|
||||
}
|
||||
} catch (err) {
|
||||
if (checkLatest()) {
|
||||
metaRef.current.validationReject(err)
|
||||
}
|
||||
} finally {
|
||||
delete metaRef.current.validationPromise
|
||||
}
|
||||
}
|
||||
|
||||
doValidation()
|
||||
|
||||
return metaRef.current.validationPromise
|
||||
}, [])
|
||||
|
||||
const getFieldValue = React.useCallback(
|
||||
field => getBy(apiRef.current.values, field),
|
||||
[]
|
||||
)
|
||||
|
||||
const getFieldMeta = React.useCallback(field => {
|
||||
const fieldID = getFieldID(field)
|
||||
return apiRef.current.__fieldMeta[fieldID]
|
||||
}, [])
|
||||
|
||||
const __getFieldMetaRef = React.useCallback(field => {
|
||||
const fieldID = getFieldID(field)
|
||||
if (!apiRef.current.__fieldMetaRefs[fieldID]) {
|
||||
apiRef.current.__fieldMetaRefs[fieldID] = {
|
||||
current: {
|
||||
instanceRefs: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
return apiRef.current.__fieldMetaRefs[fieldID]
|
||||
}, [])
|
||||
|
||||
const setFieldMeta = React.useCallback(
|
||||
(field, updater) => {
|
||||
const fieldID = getFieldID(field)
|
||||
setState(old => {
|
||||
const newFieldMeta =
|
||||
typeof updater === 'function'
|
||||
? updater(old.__fieldMeta[fieldID])
|
||||
: { ...old.__fieldMeta[fieldID], ...updater }
|
||||
|
||||
return {
|
||||
...old,
|
||||
// Any errors in fields should visually stop
|
||||
// form.isSubmitting
|
||||
meta:
|
||||
newFieldMeta && newFieldMeta.error
|
||||
? {
|
||||
...old.meta,
|
||||
isSubmitting: false,
|
||||
}
|
||||
: old.meta,
|
||||
__fieldMeta: {
|
||||
...old.__fieldMeta,
|
||||
[fieldID]: newFieldMeta,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
[setState]
|
||||
)
|
||||
|
||||
const setFieldValue = React.useCallback(
|
||||
(field, updater, { isTouched = true } = {}) => {
|
||||
const fieldInstances = apiRef.current.__getFieldInstances(field)
|
||||
|
||||
setState(old => {
|
||||
let newValue =
|
||||
typeof updater === 'function'
|
||||
? updater(getBy(old.values, field))
|
||||
: updater
|
||||
|
||||
fieldInstances.forEach(instance => {
|
||||
if (instance.current.__filterValue) {
|
||||
newValue = instance.current.__filterValue(newValue, apiRef.current)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...old,
|
||||
values: setBy(old.values, field, newValue),
|
||||
}
|
||||
})
|
||||
if (isTouched) {
|
||||
apiRef.current.setFieldMeta(field, {
|
||||
isTouched: true,
|
||||
})
|
||||
apiRef.current.setMeta({ isTouched: true })
|
||||
}
|
||||
},
|
||||
[setState]
|
||||
)
|
||||
|
||||
const __getFieldInstances = React.useCallback(field => {
|
||||
const __metaRef = apiRef.current.__getFieldMetaRef(field)
|
||||
return Object.keys(__metaRef.current.instanceRefs).map(
|
||||
key => __metaRef.current.instanceRefs[key]
|
||||
)
|
||||
}, [])
|
||||
|
||||
const pushFieldValue = React.useCallback((field, value, options) => {
|
||||
apiRef.current.setFieldValue(
|
||||
field,
|
||||
old => {
|
||||
return [...(Array.isArray(old) ? old : []), value]
|
||||
},
|
||||
options
|
||||
)
|
||||
}, [])
|
||||
|
||||
const insertFieldValue = React.useCallback((field, index, value, options) => {
|
||||
apiRef.current.setFieldValue(
|
||||
field,
|
||||
old => {
|
||||
if (Array.isArray(old)) {
|
||||
return old.map((d, i) => (i === index ? value : d))
|
||||
} else {
|
||||
throw Error(
|
||||
`Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
|
||||
)
|
||||
}
|
||||
},
|
||||
options
|
||||
)
|
||||
}, [])
|
||||
|
||||
const removeFieldValue = React.useCallback((field, index, options) => {
|
||||
apiRef.current.setFieldValue(
|
||||
field,
|
||||
old => {
|
||||
if (Array.isArray(old)) {
|
||||
return old.filter((d, i) => i !== index)
|
||||
} else {
|
||||
throw Error(
|
||||
`Cannot remove a field value from a non-array field. Check that this field's existing value is an array: ${field}.`
|
||||
)
|
||||
}
|
||||
},
|
||||
options
|
||||
)
|
||||
}, [])
|
||||
|
||||
const swapFieldValues = React.useCallback(
|
||||
(path, index1, index2) => {
|
||||
setState(old => {
|
||||
const old1 = getBy(old.values, [path, index1])
|
||||
const old2 = getBy(old.values, [path, index2])
|
||||
|
||||
let values = setBy(old.values, [path, index1], old2)
|
||||
values = setBy(values, [path, index2], old1)
|
||||
|
||||
return {
|
||||
...old,
|
||||
values,
|
||||
}
|
||||
})
|
||||
},
|
||||
[setState]
|
||||
)
|
||||
|
||||
const setValues = React.useCallback(
|
||||
values => {
|
||||
setState(old => ({
|
||||
...old,
|
||||
values: values,
|
||||
}))
|
||||
},
|
||||
[setState]
|
||||
)
|
||||
|
||||
// Create the Form element if necessary
|
||||
const Form = useFormElement(api)
|
||||
|
||||
Object.assign(api, {
|
||||
__fieldMetaRefs: __fieldMetaRefsRef.current,
|
||||
onSubmit,
|
||||
reset,
|
||||
handleSubmit,
|
||||
debounce,
|
||||
setMeta,
|
||||
runValidation,
|
||||
getFieldValue,
|
||||
getFieldMeta,
|
||||
__getFieldMetaRef,
|
||||
setFieldMeta,
|
||||
setFieldValue,
|
||||
__getFieldInstances,
|
||||
pushFieldValue,
|
||||
insertFieldValue,
|
||||
removeFieldValue,
|
||||
swapFieldValues,
|
||||
setValues,
|
||||
Form,
|
||||
formContext: api,
|
||||
})
|
||||
|
||||
// If shouldResubmit is true, do yo thang
|
||||
React.useEffect(() => {
|
||||
if (shouldResubmit) {
|
||||
handleSubmit(shouldResubmit)
|
||||
setShouldResubmit(false)
|
||||
}
|
||||
}, [handleSubmit, shouldResubmit])
|
||||
|
||||
// When the form gets dirty and when the value changes
|
||||
// validate
|
||||
React.useEffect(() => {
|
||||
if (!validatePristine && !meta.isTouched) {
|
||||
return
|
||||
}
|
||||
|
||||
apiRef.current.runValidation(values)
|
||||
}, [meta.isTouched, validatePristine, values])
|
||||
|
||||
// When defaultValues update, set them
|
||||
React.useEffect(() => {
|
||||
if (defaultValues !== apiRef.current.values) {
|
||||
setState(old => ({
|
||||
...old,
|
||||
values: defaultValues,
|
||||
}))
|
||||
}
|
||||
}, [defaultValues, setState])
|
||||
|
||||
// Return the root form and the Form component to the hook user
|
||||
return apiRef.current
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react'
|
||||
//
|
||||
|
||||
const formContext = React.createContext()
|
||||
|
||||
export function FormContextProvider({ value, children }) {
|
||||
return <formContext.Provider value={value}>{children}</formContext.Provider>
|
||||
}
|
||||
|
||||
export default function useFormContext(manualFormContext) {
|
||||
let formApi = React.useContext(formContext)
|
||||
|
||||
if (manualFormContext) {
|
||||
return manualFormContext
|
||||
}
|
||||
|
||||
if (!formApi) {
|
||||
throw new Error(`You are trying to use the form API outside of a form!`)
|
||||
}
|
||||
|
||||
return formApi
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import React from 'react'
|
||||
//
|
||||
|
||||
import { FormContextProvider } from './useFormContext'
|
||||
|
||||
export default function useFormElement(contextValue) {
|
||||
const FormRef = React.useRef()
|
||||
const FormApiRef = React.useRef()
|
||||
|
||||
FormApiRef.current = contextValue
|
||||
|
||||
// Create a new form element
|
||||
if (!FormRef.current) {
|
||||
FormRef.current = function Form({ children, noFormElement, ...rest }) {
|
||||
const {
|
||||
handleSubmit,
|
||||
meta: { isSubmitting },
|
||||
debugForm,
|
||||
} = FormApiRef.current
|
||||
|
||||
return (
|
||||
<FormContextProvider value={FormApiRef.current}>
|
||||
{noFormElement ? (
|
||||
children
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} disabled={isSubmitting} {...rest}>
|
||||
{children}
|
||||
{debugForm ? (
|
||||
<div
|
||||
style={{
|
||||
margin: '2rem 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 'bolder',
|
||||
}}
|
||||
>
|
||||
Form State
|
||||
</div>
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
{ ...FormApiRef.current, formContext: undefined },
|
||||
safeStringifyReplace(new Set()),
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
)}
|
||||
</FormContextProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the form element
|
||||
return FormRef.current
|
||||
}
|
||||
|
||||
function safeStringifyReplace(set) {
|
||||
return (key, value) => {
|
||||
if (typeof value === 'object' || Array.isArray(value)) {
|
||||
if (set.has(value)) {
|
||||
return '(circular value)'
|
||||
}
|
||||
set.add(value)
|
||||
}
|
||||
return typeof value === 'function' ? undefined : value
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import useForm from './hooks/useForm'
|
||||
import useField from './hooks/useField'
|
||||
import useFormContext from './hooks/useFormContext'
|
||||
import { splitFormProps } from './utils'
|
||||
|
||||
export { useForm, useField, useFormContext, splitFormProps }
|
||||
136
src/utils.js
@@ -1,136 +0,0 @@
|
||||
export function splitFormProps({
|
||||
field,
|
||||
defaultValue,
|
||||
defaultIsTouched,
|
||||
defaultError,
|
||||
defaultMeta,
|
||||
validatePristine,
|
||||
validate,
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
filterValue,
|
||||
debugForm,
|
||||
...rest
|
||||
}) {
|
||||
return [
|
||||
field,
|
||||
{
|
||||
defaultValue,
|
||||
defaultIsTouched,
|
||||
defaultError,
|
||||
defaultMeta,
|
||||
validatePristine,
|
||||
validate,
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
filterValue,
|
||||
debugForm,
|
||||
},
|
||||
rest,
|
||||
]
|
||||
}
|
||||
|
||||
// Utils
|
||||
|
||||
export function getBy(obj, path) {
|
||||
if (!path) {
|
||||
throw new Error('A path string is required to use getBy')
|
||||
}
|
||||
const pathArray = makePathArray(path)
|
||||
const pathObj = pathArray
|
||||
return pathObj.reduce((current, pathPart) => {
|
||||
if (typeof current !== 'undefined') {
|
||||
return current[pathPart]
|
||||
}
|
||||
return undefined
|
||||
}, obj)
|
||||
}
|
||||
|
||||
export function setBy(obj, path, updater) {
|
||||
path = makePathArray(path)
|
||||
|
||||
function doSet(parent) {
|
||||
if (!path.length) {
|
||||
return typeof updater === 'function' ? updater(parent) : updater
|
||||
}
|
||||
|
||||
const key = path.shift()
|
||||
|
||||
if (typeof key === 'string') {
|
||||
if (typeof parent === 'object') {
|
||||
return {
|
||||
...parent,
|
||||
[key]: doSet(parent[key]),
|
||||
}
|
||||
}
|
||||
return {
|
||||
[key]: doSet(),
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof key === 'number') {
|
||||
if (Array.isArray(parent)) {
|
||||
const prefix = parent.slice(0, key)
|
||||
return [
|
||||
...(prefix.length ? prefix : new Array(key)),
|
||||
doSet(parent[key]),
|
||||
...parent.slice(key + 1),
|
||||
]
|
||||
}
|
||||
return [...new Array(key), doSet()]
|
||||
}
|
||||
|
||||
throw new Error('Uh oh!')
|
||||
}
|
||||
|
||||
return doSet(obj)
|
||||
}
|
||||
|
||||
export function getFieldID(str) {
|
||||
return makePathArray(str).join('_')
|
||||
}
|
||||
|
||||
const reFindNumbers0 = /^(\d*)$/gm
|
||||
const reFindNumbers1 = /\.(\d*)\./gm
|
||||
const reFindNumbers2 = /^(\d*)\./gm
|
||||
const reFindNumbers3 = /\.(\d*$)/gm
|
||||
const reFindMultiplePeriods = /\.{2,}/gm
|
||||
|
||||
function makePathArray(str) {
|
||||
return str
|
||||
.replace('[', '.')
|
||||
.replace(']', '')
|
||||
.replace(reFindNumbers0, '__int__$1')
|
||||
.replace(reFindNumbers1, '.__int__$1.')
|
||||
.replace(reFindNumbers2, '__int__$1.')
|
||||
.replace(reFindNumbers3, '.__int__$1')
|
||||
.replace(reFindMultiplePeriods, '.')
|
||||
.split('.')
|
||||
.map(d => {
|
||||
if (d.indexOf('__int__') === 0) {
|
||||
return parseInt(d.substring('__int__'.length), 10)
|
||||
}
|
||||
return d
|
||||
})
|
||||
}
|
||||
|
||||
function loopObject(obj, fn, callback) {
|
||||
Object.keys(obj).forEach(key => {
|
||||
callback(fn(obj[key], key), key)
|
||||
})
|
||||
}
|
||||
|
||||
export function someObject(obj, fn) {
|
||||
let found = false
|
||||
|
||||
loopObject(obj, fn, (result, key) => {
|
||||
if (found) {
|
||||
return
|
||||
}
|
||||
if (result) {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
|
||||
return found
|
||||
}
|
||||
31
tsconfig.base.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"strictNullChecks": true,
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@tanstack/form-core": ["packages/form-core/src"],
|
||||
"@tanstack/react-form": ["packages/react-form/src"]
|
||||
// "@tanstack/react-form-devtools": ["packages/react-form-devtools/src"],
|
||||
// "@tanstack/solid-form": ["packages/solid-form/src"],
|
||||
// "@tanstack/vue-form": ["packages/vue-form/src"],
|
||||
}
|
||||
}
|
||||
}
|
||||
10
tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "packages/form-core" },
|
||||
{ "path": "packages/react-form" }
|
||||
// { "path": "packages/react-form-devtools" }
|
||||
// { "path": "packages/solid-form" },
|
||||
// { "path": "packages/vue-form" },
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
* https://github.com/tannerlinsley/react-form
|
||||