From b84ac46ebeeb57fcaeab20e6c780e833aaba71b2 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 15 Feb 2026 16:39:18 +0100 Subject: [PATCH 1/2] Add preact integration for tanstack form --- packages/preact-form/CHANGELOG.md | 220 +++ packages/preact-form/README.md | 35 + packages/preact-form/eslint.config.js | 17 + packages/preact-form/package.json | 66 + packages/preact-form/src/createFormHook.tsx | 601 +++++++ packages/preact-form/src/index.ts | 10 + packages/preact-form/src/types.ts | 138 ++ packages/preact-form/src/useField.tsx | 781 ++++++++++ packages/preact-form/src/useFieldGroup.tsx | 265 ++++ packages/preact-form/src/useForm.tsx | 282 ++++ packages/preact-form/src/useFormId.ts | 3 + .../src/useIsomorphicLayoutEffect.ts | 4 + packages/preact-form/src/useUUID.ts | 7 + .../tests/createFormHook.test-d.tsx | 897 +++++++++++ .../preact-form/tests/createFormHook.test.tsx | 706 +++++++++ packages/preact-form/tests/test-setup.ts | 6 + .../preact-form/tests/useField.test-d.tsx | 109 ++ packages/preact-form/tests/useField.test.tsx | 1380 +++++++++++++++++ packages/preact-form/tests/useForm.test-d.tsx | 157 ++ packages/preact-form/tests/useForm.test.tsx | 1083 +++++++++++++ packages/preact-form/tests/utils.ts | 5 + packages/preact-form/tsconfig.docs.json | 9 + packages/preact-form/tsconfig.json | 12 + packages/preact-form/vite.config.ts | 25 + pnpm-lock.yaml | 792 +++++++++- 25 files changed, 7550 insertions(+), 60 deletions(-) create mode 100644 packages/preact-form/CHANGELOG.md create mode 100644 packages/preact-form/README.md create mode 100644 packages/preact-form/eslint.config.js create mode 100644 packages/preact-form/package.json create mode 100644 packages/preact-form/src/createFormHook.tsx create mode 100644 packages/preact-form/src/index.ts create mode 100644 packages/preact-form/src/types.ts create mode 100644 packages/preact-form/src/useField.tsx create mode 100644 packages/preact-form/src/useFieldGroup.tsx create mode 100644 packages/preact-form/src/useForm.tsx create mode 100644 packages/preact-form/src/useFormId.ts create mode 100644 packages/preact-form/src/useIsomorphicLayoutEffect.ts create mode 100644 packages/preact-form/src/useUUID.ts create mode 100644 packages/preact-form/tests/createFormHook.test-d.tsx create mode 100644 packages/preact-form/tests/createFormHook.test.tsx create mode 100644 packages/preact-form/tests/test-setup.ts create mode 100644 packages/preact-form/tests/useField.test-d.tsx create mode 100644 packages/preact-form/tests/useField.test.tsx create mode 100644 packages/preact-form/tests/useForm.test-d.tsx create mode 100644 packages/preact-form/tests/useForm.test.tsx create mode 100644 packages/preact-form/tests/utils.ts create mode 100644 packages/preact-form/tsconfig.docs.json create mode 100644 packages/preact-form/tsconfig.json create mode 100644 packages/preact-form/vite.config.ts diff --git a/packages/preact-form/CHANGELOG.md b/packages/preact-form/CHANGELOG.md new file mode 100644 index 000000000..7042c8fe7 --- /dev/null +++ b/packages/preact-form/CHANGELOG.md @@ -0,0 +1,220 @@ +# @tanstack/preact-form + +## 1.28.3 + +### Patch Changes + +- form arrays now work again ([#2041](https://github.com/TanStack/form/pull/2041)) + +- Updated dependencies [[`0b3952d`](https://github.com/TanStack/form/commit/0b3952d9805b4f1756829faa012e4112c14859a7)]: + - @tanstack/form-core@1.28.3 + +## 1.28.2 + +### Patch Changes + +- Updated dependencies [[`a07862d`](https://github.com/TanStack/form/commit/a07862de23ea008c7dd3821edd880b6ebc569016)]: + - @tanstack/form-core@1.28.2 + +## 1.28.1 + +### Patch Changes + +- Fix compile error with webpack when using react v17 ([#1982](https://github.com/TanStack/form/pull/1982)) + +- Fix various issues with SSR. Things should now work as-expected in many many more scenarios than before ([#1890](https://github.com/TanStack/form/pull/1890)) + +- Updated dependencies [[`72d970a`](https://github.com/TanStack/form/commit/72d970add6ab682d733e35a95e5e1f44efb695d2)]: + - @tanstack/form-core@1.28.1 + +## 1.28.0 + +### Minor Changes + +- add `useTypedAppFormContext` ([#1826](https://github.com/TanStack/form/pull/1826)) + +### Patch Changes + +- fix: flatten errors consistently when validating before field mount ([#2003](https://github.com/TanStack/form/pull/2003)) + + Fixed an issue where `field.errors` was incorrectly nested as `[[error]]` instead of `[error]` when `form.validate()` was called manually before a field was mounted. The `flat(1)` operation is now applied by default unless `disableErrorFlat` is explicitly set to true, ensuring consistent error structure regardless of when validation occurs. + +- Updated dependencies [[`41faffe`](https://github.com/TanStack/form/commit/41faffee657e753b37132275c2255d29fdd3f325), [`7f2453b`](https://github.com/TanStack/form/commit/7f2453baf8c852adfab2475fa3f110f597b24c52)]: + - @tanstack/form-core@1.28.0 + +## 1.27.7 + +### Patch Changes + +- Updated dependencies [[`3519cce`](https://github.com/TanStack/form/commit/3519cce63072e87989bfa1b83b845e8d645d2725)]: + - @tanstack/form-core@1.27.7 + +## 1.27.6 + +### Patch Changes + +- Updated dependencies [[`c526378`](https://github.com/TanStack/form/commit/c5263786ed8b12144837ddb87f43c87fa4efc2d4)]: + - @tanstack/form-core@1.27.6 + +## 1.27.5 + +### Patch Changes + +- Updated dependencies [[`36fa503`](https://github.com/TanStack/form/commit/36fa503f21c59e68138a21de7038bf941a579b55), [`01b24a9`](https://github.com/TanStack/form/commit/01b24a9aa54f7d908830af352cacd51fddf65bbe)]: + - @tanstack/form-core@1.27.5 + +## 1.27.4 + +### Patch Changes + +- fix(preact-form): prevent array field re-render when child property changes ([#1930](https://github.com/TanStack/form/pull/1930)) + + Array fields with `mode="array"` were incorrectly re-rendering when a property on any array element was mutated. This was a regression introduced in v1.27.0 by the React Compiler compatibility changes. + + The fix ensures that `mode="array"` fields only re-render when the array length changes (items added/removed), not when individual item properties are modified. + + Fixes #1925 + +- Updated dependencies [[`c753d5e`](https://github.com/TanStack/form/commit/c753d5eca5021c231bcdfd5f0a337156958fcde1)]: + - @tanstack/form-core@1.27.4 + +## 1.27.3 + +### Patch Changes + +- Updated dependencies [[`c2ecf5d`](https://github.com/TanStack/form/commit/c2ecf5d6df0034d2db982f9b55aed963d94a76a3)]: + - @tanstack/form-core@1.27.3 + +## 1.27.2 + +### Patch Changes + +- use React 18's useId hook by default for formId generation, only calling Math.random() as a fallback if no formId is provided. ([#1913](https://github.com/TanStack/form/pull/1913)) + +- fix(preact-form): ensure `FormApi.handleSubmit` returns a promise again ([#1924](https://github.com/TanStack/form/pull/1924)) + +- Updated dependencies []: + - @tanstack/form-core@1.27.2 + +## 1.27.1 + +### Patch Changes + +- Fix issues with methods not being present in React adapter ([#1903](https://github.com/TanStack/form/pull/1903)) + +- Updated dependencies [[`3b080ec`](https://github.com/TanStack/form/commit/3b080ec1faefa9894c0f73880dbff680888e6a9a)]: + - @tanstack/form-core@1.27.1 + +## 1.27.0 + +### Patch Changes + +- Minorly improve performance and fix issues with Start ([#1882](https://github.com/TanStack/form/pull/1882)) + +- Fixed issues with React Compiler ([#1893](https://github.com/TanStack/form/pull/1893)) + +- Remove useId for react 17 user compatibility, replaced with uuid ([#1850](https://github.com/TanStack/form/pull/1850)) + +- Updated dependencies [[`8afbfc3`](https://github.com/TanStack/form/commit/8afbfc39d7373ec2b516f7c8ff5585ca44098cc1), [`4e92a91`](https://github.com/TanStack/form/commit/4e92a913e109f54463be572cdc3f09232e9d2701)]: + - @tanstack/form-core@1.27.0 + +## 1.26.0 + +### Patch Changes + +- Updated dependencies [[`74f40e7`](https://github.com/TanStack/form/commit/74f40e7d0a862dcb4dbda3481b3a23482883a0a2)]: + - @tanstack/form-core@1.26.0 + +## 1.25.0 + +### Minor Changes + +- Update Start to Release Candidate version. Extracted start, remix and nextJs adapters to the respective libraries @tanstack/preact-form-start, @tanstack/preact-form-remix, and @tanstack/preact-form-nextjs, ([#1771](https://github.com/TanStack/form/pull/1771)) + +### Patch Changes + +- Updated dependencies [[`004835f`](https://github.com/TanStack/form/commit/004835fbc113f36ac32fc5691ad27bc00813f389)]: + - @tanstack/form-core@1.25.0 + +## 1.23.9 + +### Patch Changes + +- Updated dependencies [[`8ede6d0`](https://github.com/TanStack/form/commit/8ede6d0bb5615a105f54c13d3160d0243ea6c041)]: + - @tanstack/form-core@1.24.5 + +## 1.23.8 + +### Patch Changes + +- Allow interfaces to be assigned to `withFieldGroup`'s `props`. ([#1816](https://github.com/TanStack/form/pull/1816)) + +- Allow returning all other `ReactNode`s not just `JSX.Element` in the `render` function of `withForm` and `withFieldGroup`. ([#1817](https://github.com/TanStack/form/pull/1817)) + +- form-core: Optimise event client emissions and minor layout tweaks ([#1758](https://github.com/TanStack/form/pull/1758)) + +- Updated dependencies [[`94631cb`](https://github.com/TanStack/form/commit/94631cb97dea611de69a900c89b7e8dfe0eeee37)]: + - @tanstack/form-core@1.24.4 + +## 1.23.7 + +### Patch Changes + +- Updated dependencies [[`33cce81`](https://github.com/TanStack/form/commit/33cce812cbfeb42aa7457bab220a807ff5c4ba7f)]: + - @tanstack/form-core@1.24.3: respect dontValidate option in formApi array modifiers ([#1775](https://github.com/TanStack/form/pull/1775)) + +## 1.23.6 + +### Patch Changes + +- Updated dependencies [[`74af33e`](https://github.com/TanStack/form/commit/74af33eb80218b8cec8642b64ce7e69a62a65248)]: + - @tanstack/form-core@1.24.2: prevent runtime errors when using `deleteField` ([#1706](https://github.com/TanStack/form/pull/1706)) + +## 1.23.5 + +### Patch Changes + +- Updated dependencies [[`2cfe44c`](https://github.com/TanStack/form/commit/2cfe44ce1e35235ae37ee260dc943a94c9feb71d)]: + - @tanstack/form-core@1.24.1 + +## 1.23.4 + +### Patch Changes + +- Updated dependencies [[`c978946`](https://github.com/TanStack/form/commit/c97894688c6f5f1953a87c26890e156ecb0bcaab)]: + - @tanstack/form-core@1.24.0 + +## 1.23.3 + +### Patch Changes + +- Updated dependencies [[`f608267`](https://github.com/TanStack/form/commit/f6082674290a2ec5bc1d3ae33f193539ac7fc4b6)]: + - @tanstack/form-core@1.23.3 + +## 1.23.2 + +### Patch Changes + +- Updated dependencies [[`7cf3728`](https://github.com/TanStack/form/commit/7cf3728a7b75e077802b427db2a387e36b23682a)]: + - @tanstack/form-core@1.23.2 + +## 1.23.1 + +### Patch Changes + +- Updated dependencies [[`db96886`](https://github.com/TanStack/form/commit/db96886a8bf9d3d944bf09fc050b4c2c4b514851)]: + - @tanstack/form-core@1.23.1 + +## 1.23.0 + +### Patch Changes + +- Updated dependencies [[`773c1b8`](https://github.com/TanStack/form/commit/773c1b8d9e1b82b5403633691de22f1a1e188d4f), [`1e36222`](https://github.com/TanStack/form/commit/1e362224d3086f67d8a49839d196edd7aa78c04d)]: + - @tanstack/form-core@1.23.0 + +## 1.21.1 + +### Patch Changes + +- Updated dependencies [[`d2b6063`](https://github.com/TanStack/form/commit/d2b6063c0fc5406235f8be5462c19497717dfd0d)]: + - @tanstack/form-core@1.22.0 diff --git a/packages/preact-form/README.md b/packages/preact-form/README.md new file mode 100644 index 000000000..b20213713 --- /dev/null +++ b/packages/preact-form/README.md @@ -0,0 +1,35 @@ + + +![TanStack Form Header](https://github.com/TanStack/form/raw/main/media/repo-header.png) + +Hooks for managing form state in Preact + + + #TanStack + + + + + + + + + + semantic-release + + Join the discussion on Github +Best of JS + + + + + Gitpod Ready-to-Code + + +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/) + + diff --git a/packages/preact-form/eslint.config.js b/packages/preact-form/eslint.config.js new file mode 100644 index 000000000..6f18a0590 --- /dev/null +++ b/packages/preact-form/eslint.config.js @@ -0,0 +1,17 @@ +// @ts-check + +import pluginReact from '@eslint-react/eslint-plugin' +import rootConfig from '../../eslint.config.js' + +export default [ + ...rootConfig, + { + files: ['**/*.{ts,tsx}'], + ...pluginReact.configs.recommended, + }, + { + rules: { + '@eslint-react/no-use-context': 'off', + } + } +] diff --git a/packages/preact-form/package.json b/packages/preact-form/package.json new file mode 100644 index 000000000..7367159ff --- /dev/null +++ b/packages/preact-form/package.json @@ -0,0 +1,66 @@ +{ + "name": "@tanstack/preact-form", + "version": "1.28.3", + "description": "Powerful, type-safe forms for Preact.", + "author": "tannerlinsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/form.git", + "directory": "packages/preact-form" + }, + "homepage": "https://tanstack.com/form", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "scripts": { + "clean": "premove ./dist ./coverage", + "test:eslint": "eslint ./src ./tests", + "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js", + "test:types:ts58": "tsc", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict", + "build": "vite build" + }, + "type": "module", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "dependencies": { + "@tanstack/form-core": "workspace:*", + "@tanstack/preact-store": "^0.10.2" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "@testing-library/preact": "^3.2.4", + "preact": "^10.11.1", + "vite": "^7.2.2" + }, + "peerDependencies": { + "preact": ">10.11.0" + } +} diff --git a/packages/preact-form/src/createFormHook.tsx b/packages/preact-form/src/createFormHook.tsx new file mode 100644 index 000000000..720e17605 --- /dev/null +++ b/packages/preact-form/src/createFormHook.tsx @@ -0,0 +1,601 @@ +/* eslint-disable @eslint-react/no-context-provider */ +import { createContext, useContext, useMemo } from 'preact/compat' +import { useForm } from './useForm' +import { useFieldGroup } from './useFieldGroup' +import type { + AnyFieldApi, + AnyFormApi, + BaseFormOptions, + DeepKeysOfType, + FieldApi, + FieldsMap, + FormAsyncValidateOrFn, + FormOptions, + FormValidateOrFn, +} from '@tanstack/form-core' +import type { + ComponentType, + Context, + FunctionComponent, + PropsWithChildren, +} from 'preact/compat' +import type { FieldComponent } from './useField' +import type { ReactFormExtendedApi } from './useForm' +import type { AppFieldExtendedReactFieldGroupApi } from './useFieldGroup' + +// We should never hit the `null` case here +const FieldContext = createContext(null as never) +const FormContext = createContext(null as never) + +/** + * TypeScript inferencing is weird. + * + * If you have: + * + * @example + * + * interface Args { + * arg?: T + * } + * + * function test(arg?: Partial>): T { + * return 0 as any; + * } + * + * const a = test({}); + * + * Then `T` will default to `unknown`. + * + * However, if we change `test` to be: + * + * @example + * + * function test(arg?: Partial>): T; + * + * Then `T` becomes `undefined`. + * + * Here, we are checking if the passed type `T` extends `DefaultT` and **only** + * `DefaultT`, as if that's the case we assume that inferencing has not occurred. + */ +type UnwrapOrAny = [unknown] extends [T] ? any : T +type UnwrapDefaultOrAny = [DefaultT] extends [T] + ? [T] extends [DefaultT] + ? any + : T + : T + +function useFormContext() { + const form = useContext(FormContext) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!form) { + throw new Error( + '`formContext` only works when within a `formComponent` passed to `createFormHook`', + ) + } + + return form as ReactFormExtendedApi< + // If you need access to the form data, you need to use `withForm` instead + Record, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any + > +} + +export function createFormHookContexts() { + function useFieldContext() { + const field = useContext(FieldContext) + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!field) { + throw new Error( + '`fieldContext` only works when within a `fieldComponent` passed to `createFormHook`', + ) + } + + return field as FieldApi< + any, + string, + TData, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any + > + } + + return { fieldContext: FieldContext, useFieldContext, useFormContext, formContext: FormContext } +} + +interface CreateFormHookProps< + TFieldComponents extends Record>, + TFormComponents extends Record>, +> { + fieldComponents: TFieldComponents + fieldContext: Context + formComponents: TFormComponents + formContext: Context +} + +/** + * @private + */ +export type AppFieldExtendedReactFormApi< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + TFieldComponents extends Record>, + TFormComponents extends Record>, +> = ReactFormExtendedApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +> & + NoInfer & { + AppField: FieldComponent< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + NoInfer + > + AppForm: ComponentType< + // PropsWithChildren

is not optional in React 17 + PropsWithChildren<{}> + > + } + +export interface WithFormProps< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + TFieldComponents extends Record>, + TFormComponents extends Record>, + TRenderProps extends object = Record, +> extends FormOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +> { + // Optional, but adds props to the `render` function outside of `form` + props?: TRenderProps + render: FunctionComponent< + PropsWithChildren< + NoInfer & { + form: AppFieldExtendedReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TFieldComponents, + TFormComponents + > + } + > + > +} + +export interface WithFieldGroupProps< + TFieldGroupData, + TFieldComponents extends Record>, + TFormComponents extends Record>, + TSubmitMeta, + TRenderProps extends object = Record, +> extends BaseFormOptions { + // Optional, but adds props to the `render` function outside of `form` + props?: TRenderProps + render: FunctionComponent< + PropsWithChildren< + NoInfer & { + group: AppFieldExtendedReactFieldGroupApi< + unknown, + TFieldGroupData, + string | FieldsMap, + undefined | FormValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormAsyncValidateOrFn, + // this types it as 'never' in the render prop. It should prevent any + // untyped meta passed to the handleSubmit by accident. + unknown extends TSubmitMeta ? never : TSubmitMeta, + TFieldComponents, + TFormComponents + > + } + > + > +} + +export function createFormHook< + const TComponents extends Record>, + const TFormComponents extends Record>, +>({ + fieldComponents, + fieldContext, + formContext, + formComponents, +}: CreateFormHookProps) { + function useAppForm< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + >( + props: FormOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, + ): AppFieldExtendedReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents + > { + const form = useForm(props) + + // PropsWithChildren

is not optional in React 17 + const AppForm = useMemo>>(() => { + return ({ children }) => { + return ( + {children} + ) + } + }, [form]) + + const AppField = useMemo(() => { + const AppFieldComponent = (({ children, ...restProps }) => { + return ( + + {(field) => ( + + {children(Object.assign(field, fieldComponents))} + + )} + + ) + }) as FieldComponent< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents + > + return AppFieldComponent + }, [form]) + + const extendedForm = useMemo(() => { + return Object.assign(form, { + AppField, + AppForm, + ...formComponents, + }) + }, [form, AppField, AppForm]) + + return extendedForm + } + + function withForm< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + TRenderProps extends object = {}, + >({ + render, + props, + }: WithFormProps< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents, + TRenderProps + >): WithFormProps< + UnwrapOrAny, + UnwrapDefaultOrAny, TOnMount>, + UnwrapDefaultOrAny, TOnChange>, + UnwrapDefaultOrAny, TOnChangeAsync>, + UnwrapDefaultOrAny, TOnBlur>, + UnwrapDefaultOrAny, TOnBlurAsync>, + UnwrapDefaultOrAny, TOnSubmit>, + UnwrapDefaultOrAny, TOnSubmitAsync>, + UnwrapDefaultOrAny, TOnDynamic>, + UnwrapDefaultOrAny< + undefined | FormValidateOrFn, + TOnDynamicAsync + >, + UnwrapDefaultOrAny, TOnServer>, + UnwrapOrAny, + UnwrapOrAny, + UnwrapOrAny, + UnwrapOrAny + >['render'] { + return (innerProps) => render({ ...props, ...innerProps }) + } + + function withFieldGroup< + TFieldGroupData, + TSubmitMeta, + TRenderProps extends object = {}, + >({ + render, + props, + defaultValues, + }: WithFieldGroupProps< + TFieldGroupData, + TComponents, + TFormComponents, + TSubmitMeta, + TRenderProps + >): < + TFormData, + TFields extends + | DeepKeysOfType + | FieldsMap, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TFormSubmitMeta, + >( + params: PropsWithChildren< + NoInfer & { + form: + | AppFieldExtendedReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, + TComponents, + TFormComponents + > + | AppFieldExtendedReactFieldGroupApi< + // Since this only occurs if you nest it within other field groups, it can be more + // lenient with the types. + unknown, + TFormData, + string | FieldsMap, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + unknown extends TSubmitMeta ? TFormSubmitMeta : TSubmitMeta, + TComponents, + TFormComponents + > + fields: TFields + } + >, + ) => ReturnType { + return function Render(innerProps) { + const fieldGroupProps = useMemo(() => { + return { + form: innerProps.form, + fields: innerProps.fields, + defaultValues, + formComponents, + } + }, [innerProps.form, innerProps.fields]) + const fieldGroupApi = useFieldGroup(fieldGroupProps as any) + + return render({ ...props, ...innerProps, group: fieldGroupApi as any }) + } + } + + /** + * ⚠️ **Use withForm whenever possible.** + * + * Gets a typed form from the `` context. + */ + function useTypedAppFormContext< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + >( + _props: FormOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, + ): AppFieldExtendedReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents + > { + const form = useFormContext() + + return form as never + } + + return { + useAppForm, + withForm, + withFieldGroup, + useTypedAppFormContext, + } +} diff --git a/packages/preact-form/src/index.ts b/packages/preact-form/src/index.ts new file mode 100644 index 000000000..194917a29 --- /dev/null +++ b/packages/preact-form/src/index.ts @@ -0,0 +1,10 @@ +export * from '@tanstack/form-core' + +export { useStore } from '@tanstack/preact-store' + +export * from './createFormHook' +export * from './types' +export * from './useField' +export * from './useFieldGroup' +export * from './useForm' +export * from './useIsomorphicLayoutEffect' diff --git a/packages/preact-form/src/types.ts b/packages/preact-form/src/types.ts new file mode 100644 index 000000000..971a5692a --- /dev/null +++ b/packages/preact-form/src/types.ts @@ -0,0 +1,138 @@ +import type { + DeepKeys, + DeepValue, + FieldApiOptions, + FieldAsyncValidateOrFn, + FieldOptions, + FieldValidateOrFn, + FormAsyncValidateOrFn, + FormState, + FormValidateOrFn, +} from '@tanstack/form-core' + +interface FieldOptionsMode { + mode?: 'value' | 'array' +} + +/** + * The field options. + */ +export interface UseFieldOptions< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnDynamic extends undefined | FormValidateOrFn, + TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, +> + extends + FieldApiOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TSubmitMeta + >, + FieldOptionsMode {} + +export interface UseFieldOptionsBound< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, +> + extends + FieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync + >, + FieldOptionsMode {} + +export type ServerFormState< + TFormData, + TOnServer extends undefined | FormAsyncValidateOrFn, +> = Pick< + FormState< + TFormData, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + TOnServer + >, + 'values' | 'errors' | 'errorMap' +> diff --git a/packages/preact-form/src/useField.tsx b/packages/preact-form/src/useField.tsx new file mode 100644 index 000000000..eb2252637 --- /dev/null +++ b/packages/preact-form/src/useField.tsx @@ -0,0 +1,781 @@ +'use client' + +import { useMemo, useRef, useState } from 'preact/compat' +import { useStore } from '@tanstack/preact-store' +import { FieldApi, functionalUpdate } from '@tanstack/form-core' +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' +import type { + AnyFieldApi, + AnyFieldMeta, + DeepKeys, + DeepValue, + FieldAsyncValidateOrFn, + FieldValidateOrFn, + FieldValidators, + FormAsyncValidateOrFn, + FormValidateOrFn, +} from '@tanstack/form-core' +import type { FunctionComponent, ReactElement, ReactNode } from 'preact/compat' +import type { UseFieldOptions, UseFieldOptionsBound } from './types' + +interface ReactFieldApi< + TParentData, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnDynamic extends undefined | FormValidateOrFn, + TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TPatentSubmitMeta, +> { + /** + * A pre-bound and type-safe sub-field component using this field as a root. + */ + Field: FieldComponent< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta + > +} + +/** + * A type representing a hook for using a field in a form with the given form data type. + * + * A function that takes an optional object with a `name` property and field options, and returns a `FieldApi` instance for the specified field. + */ +export type UseField< + TParentData, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnDynamic extends undefined | FormValidateOrFn, + TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TPatentSubmitMeta, +> = < + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, +>( + opts: UseFieldOptionsBound< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync + >, +) => FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta +> + +/** + * A hook for managing a field in a form. + * @param opts An object with field options. + * + * @returns The `FieldApi` instance for the specified field. + */ +export function useField< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnDynamic extends undefined | FormValidateOrFn, + TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TPatentSubmitMeta, +>( + opts: UseFieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta + >, +) { + // Keep a snapshot of options so that React Compiler doesn't + // wrongly optimize fieldApi. + const [prevOptions, setPrevOptions] = useState(() => ({ + form: opts.form, + name: opts.name, + })) + + const [fieldApi, setFieldApi] = useState(() => { + return new FieldApi({ + ...opts, + }) + }) + + // We only want to + // update on name changes since those are at risk of becoming stale. The field + // state must be up to date for the internal JSX render. + // The other options can freely be in `fieldApi.update` + if (prevOptions.form !== opts.form || prevOptions.name !== opts.name) { + setFieldApi( + new FieldApi({ + ...opts, + }), + ) + setPrevOptions({ form: opts.form, name: opts.name }) + } + + // For array mode, only track length changes to avoid re-renders when child properties change + // See: https://github.com/TanStack/form/issues/1925 + const reactiveStateValue = useStore( + fieldApi.store, + (opts.mode === 'array' + ? (state) => Object.keys((state.value as unknown) ?? []).length + : (state) => state.value) as ( + state: typeof fieldApi.state, + ) => TData | number, + ) + const reactiveMetaIsTouched = useStore( + fieldApi.store, + (state) => state.meta.isTouched, + ) + const reactiveMetaIsBlurred = useStore( + fieldApi.store, + (state) => state.meta.isBlurred, + ) + const reactiveMetaIsDirty = useStore( + fieldApi.store, + (state) => state.meta.isDirty, + ) + const reactiveMetaErrorMap = useStore( + fieldApi.store, + (state) => state.meta.errorMap, + ) + const reactiveMetaErrorSourceMap = useStore( + fieldApi.store, + (state) => state.meta.errorSourceMap, + ) + const reactiveMetaIsValidating = useStore( + fieldApi.store, + (state) => state.meta.isValidating, + ) + + // This makes me sad, but if I understand correctly, this is what we have to do for reactivity to work properly with React compiler. + const extendedFieldApi = useMemo(() => { + const reactiveFieldApi = { + ...fieldApi, + get state() { + return { + // For array mode, reactiveStateValue is the length (for reactivity tracking), + // so we need to get the actual value from fieldApi + value: + opts.mode === 'array' ? fieldApi.state.value : reactiveStateValue, + get meta() { + return { + ...fieldApi.state.meta, + isTouched: reactiveMetaIsTouched, + isBlurred: reactiveMetaIsBlurred, + isDirty: reactiveMetaIsDirty, + errorMap: reactiveMetaErrorMap, + errorSourceMap: reactiveMetaErrorSourceMap, + isValidating: reactiveMetaIsValidating, + } satisfies AnyFieldMeta + }, + } satisfies AnyFieldApi['state'] + }, + } + + const extendedApi: FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta + > & + ReactFieldApi< + TParentData, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta + > = reactiveFieldApi as never + + extendedApi.Field = Field as never + + return extendedApi + }, [ + fieldApi, + opts.mode, + reactiveStateValue, + reactiveMetaIsTouched, + reactiveMetaIsBlurred, + reactiveMetaIsDirty, + reactiveMetaErrorMap, + reactiveMetaErrorSourceMap, + reactiveMetaIsValidating, + ]) + + useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]) + + /** + * fieldApi.update should not have any side effects. Think of it like a `useRef` + * that we need to keep updated every render with the most up-to-date information. + */ + useIsomorphicLayoutEffect(() => { + fieldApi.update(opts) + }) + + return extendedFieldApi +} + +/** + * @param children A render function that takes a field API instance and returns a React element. + */ +interface FieldComponentProps< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnDynamic extends undefined | FormValidateOrFn, + TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TPatentSubmitMeta, + ExtendedApi = {}, +> extends UseFieldOptions< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta +> { + children: ( + fieldApi: FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta + > & + ExtendedApi, + ) => ReactNode +} + +interface FieldComponentBoundProps< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnDynamic extends undefined | FormValidateOrFn, + TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TPatentSubmitMeta, + ExtendedApi = {}, +> extends UseFieldOptionsBound< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync +> { + children: ( + fieldApi: FieldApi< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta + > & + ExtendedApi, + ) => ReactNode +} + +/** + * A type alias representing a field component for a specific form data type. + */ +export type FieldComponent< + in out TParentData, + in out TFormOnMount extends undefined | FormValidateOrFn, + in out TFormOnChange extends undefined | FormValidateOrFn, + in out TFormOnChangeAsync extends + | undefined + | FormAsyncValidateOrFn, + in out TFormOnBlur extends undefined | FormValidateOrFn, + in out TFormOnBlurAsync extends + | undefined + | FormAsyncValidateOrFn, + in out TFormOnSubmit extends undefined | FormValidateOrFn, + in out TFormOnSubmitAsync extends + | undefined + | FormAsyncValidateOrFn, + in out TFormOnDynamic extends undefined | FormValidateOrFn, + in out TFormOnDynamicAsync extends + | undefined + | FormAsyncValidateOrFn, + in out TFormOnServer extends undefined | FormAsyncValidateOrFn, + in out TPatentSubmitMeta, + in out ExtendedApi = {}, +> = < + const TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, +>({ + children, + ...fieldOptions +}: FieldComponentBoundProps< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta, + ExtendedApi +>) => ReturnType + +/** + * A type alias representing a field component for a form lens data type. + */ +export type LensFieldComponent< + in out TLensData, + in out TParentSubmitMeta, + in out ExtendedApi = {}, +> = < + const TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, +>({ + children, + ...fieldOptions +}: Omit< + FieldComponentBoundProps< + unknown, + string, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + undefined | FormValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormValidateOrFn, + undefined | FormAsyncValidateOrFn, + undefined | FormAsyncValidateOrFn, + TParentSubmitMeta, + ExtendedApi + >, + 'name' | 'validators' +> & { + name: TName + validators?: Omit< + FieldValidators< + unknown, + string, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync + >, + 'onChangeListenTo' | 'onBlurListenTo' + > & { + /** + * An optional list of field names that should trigger this field's `onChange` and `onChangeAsync` events when its value changes + */ + onChangeListenTo?: DeepKeys[] + /** + * An optional list of field names that should trigger this field's `onBlur` and `onBlurAsync` events when its value changes + */ + onBlurListenTo?: DeepKeys[] + } +}) => ReturnType + +/** + * A function component that takes field options and a render function as children and returns a React component. + * + * The `Field` component uses the `useField` hook internally to manage the field instance. + */ +export const Field = (< + TParentData, + TName extends DeepKeys, + TData extends DeepValue, + TOnMount extends undefined | FieldValidateOrFn, + TOnChange extends undefined | FieldValidateOrFn, + TOnChangeAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnBlur extends undefined | FieldValidateOrFn, + TOnBlurAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnSubmit extends undefined | FieldValidateOrFn, + TOnSubmitAsync extends + | undefined + | FieldAsyncValidateOrFn, + TOnDynamic extends undefined | FieldValidateOrFn, + TOnDynamicAsync extends + | undefined + | FieldAsyncValidateOrFn, + TFormOnMount extends undefined | FormValidateOrFn, + TFormOnChange extends undefined | FormValidateOrFn, + TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TFormOnBlur extends undefined | FormValidateOrFn, + TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TFormOnSubmit extends undefined | FormValidateOrFn, + TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TFormOnDynamic extends undefined | FormValidateOrFn, + TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TFormOnServer extends undefined | FormAsyncValidateOrFn, + TPatentSubmitMeta, +>({ + children, + ...fieldOptions +}: FieldComponentProps< + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync, + TFormOnServer, + TPatentSubmitMeta +>): ReturnType => { + const fieldApi = useField(fieldOptions as any) + + const jsxToDisplay = useMemo( + () => functionalUpdate(children, fieldApi as any), + [children, fieldApi], + ) + return (<>{jsxToDisplay}) as never +}) satisfies FunctionComponent< + FieldComponentProps< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any + > +> diff --git a/packages/preact-form/src/useFieldGroup.tsx b/packages/preact-form/src/useFieldGroup.tsx new file mode 100644 index 000000000..520413f4e --- /dev/null +++ b/packages/preact-form/src/useFieldGroup.tsx @@ -0,0 +1,265 @@ +'use client' + +import { useState } from 'preact/compat' +import { useStore } from '@tanstack/preact-store' +import { FieldGroupApi, functionalUpdate } from '@tanstack/form-core' +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' +import type { + AnyFieldGroupApi, + DeepKeysOfType, + FieldGroupState, + FieldsMap, + FormAsyncValidateOrFn, + FormValidateOrFn, +} from '@tanstack/form-core' +import type { AppFieldExtendedReactFormApi } from './createFormHook' +import type { + ComponentType, + FunctionComponent, + PropsWithChildren, + ReactNode, +} from 'preact/compat' +import type { LensFieldComponent } from './useField' + +function LocalSubscribe({ + lens, + selector, + children, +}: PropsWithChildren<{ + lens: AnyFieldGroupApi + selector: (state: FieldGroupState) => FieldGroupState +}>): ReturnType { + const data = useStore(lens.store, selector) + + return <>{functionalUpdate(children, data)} +} + +/** + * @private + */ +export type AppFieldExtendedReactFieldGroupApi< + TFormData, + TFieldGroupData, + TFields extends + | DeepKeysOfType + | FieldsMap, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + TFieldComponents extends Record>, + TFormComponents extends Record>, +> = FieldGroupApi< + TFormData, + TFieldGroupData, + TFields, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +> & + NoInfer & { + AppField: LensFieldComponent< + TFieldGroupData, + TSubmitMeta, + NoInfer + > + AppForm: ComponentType> + /** + * A React component to render form fields. With this, you can render and manage individual form fields. + */ + Field: LensFieldComponent + + /** + * A `Subscribe` function that allows you to listen and react to changes in the form's state. It's especially useful when you need to execute side effects or render specific components in response to state updates. + */ + Subscribe: >>(props: { + selector?: (state: NoInfer>) => TSelected + children: ((state: NoInfer) => ReactNode) | ReactNode + }) => ReturnType + } + +export function useFieldGroup< + TFormData, + TFieldGroupData, + TFields extends + | DeepKeysOfType + | FieldsMap, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TComponents extends Record>, + TFormComponents extends Record>, + TSubmitMeta = never, +>(opts: { + form: + | AppFieldExtendedReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents + > + | AppFieldExtendedReactFieldGroupApi< + // Since this only occurs if you nest it within other form lenses, it can be more + // lenient with the types. + unknown, + TFormData, + string | FieldsMap, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + TSubmitMeta, + TComponents, + TFormComponents + > + fields: TFields + defaultValues?: TFieldGroupData + onSubmitMeta?: TSubmitMeta + formComponents: TFormComponents +}): AppFieldExtendedReactFieldGroupApi< + TFormData, + TFieldGroupData, + TFields, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents +> { + const [formLensApi] = useState(() => { + const api = new FieldGroupApi(opts) + const form = + opts.form instanceof FieldGroupApi + ? (opts.form.form as AppFieldExtendedReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents + >) + : opts.form + + const extendedApi: AppFieldExtendedReactFieldGroupApi< + TFormData, + TFieldGroupData, + TFields, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents + > = api as never + + extendedApi.AppForm = function AppForm(appFormProps) { + return + } + + extendedApi.AppField = function AppField(props) { + return ( + + ) + } + + extendedApi.Field = function Field(props) { + return + } + + extendedApi.Subscribe = function Subscribe(props: any) { + return ( + + ) + } + + return Object.assign(extendedApi, { + ...opts.formComponents, + }) as AppFieldExtendedReactFieldGroupApi< + TFormData, + TFieldGroupData, + TFields, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta, + TComponents, + TFormComponents + > + }) + + useIsomorphicLayoutEffect(formLensApi.mount, [formLensApi]) + + return formLensApi +} diff --git a/packages/preact-form/src/useForm.tsx b/packages/preact-form/src/useForm.tsx new file mode 100644 index 000000000..633460f08 --- /dev/null +++ b/packages/preact-form/src/useForm.tsx @@ -0,0 +1,282 @@ +'use client' + +import { FormApi, functionalUpdate, mergeAndUpdate } from '@tanstack/form-core' +import { useStore } from '@tanstack/preact-store' +import { useMemo, useRef, useState } from 'preact/compat' +import { Field } from './useField' +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' +import { useFormId } from './useFormId' +import type { + AnyFormApi, + AnyFormState, + FormAsyncValidateOrFn, + FormOptions, + FormState, + FormValidateOrFn, +} from '@tanstack/form-core' +import type { FunctionComponent, PropsWithChildren, ReactNode } from 'preact/compat' +import type { FieldComponent } from './useField' +import type { NoInfer } from '@tanstack/preact-store' + +/** + * Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm` + */ +export interface ReactFormApi< + in out TFormData, + in out TOnMount extends undefined | FormValidateOrFn, + in out TOnChange extends undefined | FormValidateOrFn, + in out TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + in out TOnBlur extends undefined | FormValidateOrFn, + in out TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + in out TOnSubmit extends undefined | FormValidateOrFn, + in out TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + in out TOnDynamic extends undefined | FormValidateOrFn, + in out TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + in out TOnServer extends undefined | FormAsyncValidateOrFn, + in out TSubmitMeta, +> { + /** + * A React component to render form fields. With this, you can render and manage individual form fields. + */ + Field: FieldComponent< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + /** + * A `Subscribe` function that allows you to listen and react to changes in the form's state. It's especially useful when you need to execute side effects or render specific components in response to state updates. + */ + Subscribe: < + TSelected = NoInfer< + FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + >, + >(props: { + selector?: ( + state: NoInfer< + FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + >, + ) => TSelected + children: ((state: NoInfer) => ReactNode) | ReactNode + }) => ReturnType +} + +/** + * An extended version of the `FormApi` class that includes React-specific functionalities from `ReactFormApi` + */ +export type ReactFormExtendedApi< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, +> = FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +> & + ReactFormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + +function LocalSubscribe({ + form, + selector, + children, +}: PropsWithChildren<{ + form: AnyFormApi + selector: (state: AnyFormState) => AnyFormState +}>): ReturnType { + const data = useStore(form.store, selector) + + return <>{functionalUpdate(children, data)} +} + +/** + * A custom React Hook that returns an extended instance of the `FormApi` class. + * + * This API encapsulates all the necessary functionalities related to the form. It allows you to manage form state, handle submissions, and interact with form fields + */ +export function useForm< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, +>( + opts?: FormOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, +) { + const fallbackFormId = useFormId() + const [prevFormId, setPrevFormId] = useState(opts?.formId as never) + + const [formApi, setFormApi] = useState(() => { + return new FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >({ ...opts, formId: opts?.formId ?? fallbackFormId }) + }) + + if (prevFormId !== opts?.formId) { + const formId = opts?.formId ?? fallbackFormId + setFormApi(new FormApi({ ...opts, formId })) + setPrevFormId(formId) + } + + const extendedFormApi = useMemo(() => { + const extendedApi: ReactFormExtendedApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > = { + ...formApi, + handleSubmit: ((...props: never[]) => { + return formApi._handleSubmit(...props) + }) as typeof formApi.handleSubmit, + // We must add all `get`ters from `core`'s `FormApi` here, as otherwise the spread operator won't catch those + get formId(): string { + return formApi._formId + }, + get state() { + return formApi.store.state + }, + } as never + + extendedApi.Field = function APIField(props) { + return + } + + extendedApi.Subscribe = function Subscribe(props: any) { + return ( + + ) + } + + return extendedApi + }, [formApi]) + + useIsomorphicLayoutEffect(formApi.mount, []) + + /** + * formApi.update should not have any side effects. Think of it like a `useRef` + * that we need to keep updated every render with the most up-to-date information. + */ + useIsomorphicLayoutEffect(() => { + formApi.update(opts) + }) + + const hasRan = useRef(false) + + useIsomorphicLayoutEffect(() => { + if (!hasRan.current) return + if (!opts?.transform) return + mergeAndUpdate(formApi, opts.transform as never) + }, [formApi, opts?.transform]) + + useIsomorphicLayoutEffect(() => { + hasRan.current = true + }) + + return extendedFormApi +} diff --git a/packages/preact-form/src/useFormId.ts b/packages/preact-form/src/useFormId.ts new file mode 100644 index 000000000..50049b2ff --- /dev/null +++ b/packages/preact-form/src/useFormId.ts @@ -0,0 +1,3 @@ +import { useId } from 'preact/hooks' + +export const useFormId = useId diff --git a/packages/preact-form/src/useIsomorphicLayoutEffect.ts b/packages/preact-form/src/useIsomorphicLayoutEffect.ts new file mode 100644 index 000000000..f2e2920c0 --- /dev/null +++ b/packages/preact-form/src/useIsomorphicLayoutEffect.ts @@ -0,0 +1,4 @@ +import { useEffect, useLayoutEffect } from 'preact/compat' + +export const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect diff --git a/packages/preact-form/src/useUUID.ts b/packages/preact-form/src/useUUID.ts new file mode 100644 index 000000000..c58a4f728 --- /dev/null +++ b/packages/preact-form/src/useUUID.ts @@ -0,0 +1,7 @@ +import { useState } from 'preact/compat' +import { uuid } from '@tanstack/form-core' + +/** Generates a random UUID. and returns a stable reference to it. */ +export function useUUID() { + return useState(() => uuid())[0] +} diff --git a/packages/preact-form/tests/createFormHook.test-d.tsx b/packages/preact-form/tests/createFormHook.test-d.tsx new file mode 100644 index 000000000..4f8da4740 --- /dev/null +++ b/packages/preact-form/tests/createFormHook.test-d.tsx @@ -0,0 +1,897 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { formOptions } from '@tanstack/form-core' +import { createFormHook, createFormHookContexts } from '../src' +import type { ComponentChildren } from 'preact' + +const { fieldContext, useFieldContext, formContext, useFormContext } = + createFormHookContexts() + +function Test() { + return null +} + +const { useAppForm, withForm, withFieldGroup } = createFormHook({ + fieldComponents: { + Test, + }, + formComponents: { + Test, + }, + fieldContext, + formContext, +}) + +describe('createFormHook', () => { + it('should not break with an infinite type on large schemas', () => { + const ActivityKind0_Names = ['Work', 'Rest', 'OnCall'] as const + type ActivityKind0 = (typeof ActivityKind0_Names)[number] + + enum DayOfWeek { + Monday = 1, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, + } + + interface Branding { + __type?: Brand + } + type Branded = T & Branding + type ActivityId = Branded + interface ActivitySelectorFormData { + includeAll: boolean + includeActivityIds: ActivityId[] + includeActivityKinds: Set + excludeActivityIds: ActivityId[] + } + + const GeneratedTypes0Visibility_Names = [ + 'Normal', + 'Advanced', + 'Hidden', + ] as const + type GeneratedTypes0Visibility = + (typeof GeneratedTypes0Visibility_Names)[number] + interface FormValuesBase { + key: string + visibility: GeneratedTypes0Visibility + } + + interface ActivityCountFormValues extends FormValuesBase { + _type: 'ActivityCount' + activitySelector: ActivitySelectorFormData + daysOfWeek: DayOfWeek[] + label: string + } + + interface PlanningTimesFormValues extends FormValuesBase { + _type: 'PlanningTimes' + showTarget: boolean + showPlanned: boolean + showDiff: boolean + } + + type EditorValues = ActivityCountFormValues | PlanningTimesFormValues + interface EditorFormValues { + editors: Record + ordering: string[] + } + + const ExampleUsage = withForm({ + props: { + initialValues: '' as keyof EditorFormValues['editors'], + }, + defaultValues: {} as EditorFormValues, + render: ({ form, initialValues }) => { + return ( +

+ + {(field) => { + expectTypeOf(field.state.value).toExtend() + return null + }} + + + + +
+ ) + }, + }) + + const ExampleUsage2 = withFieldGroup({ + defaultValues: {} as EditorValues, + render: ({ group }) => { + const test = group.state.values.key + return ( +
+ + {(field) => { + expectTypeOf(field.state.value).toExtend() + return null + }} + + + + +
+ ) + }, + }) + }) + + it('types should be properly inferred when using formOptions', () => { + type Person = { + firstName: string + lastName: string + } + + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + const WithFormComponent = withForm({ + ...formOpts, + render: ({ form }) => { + expectTypeOf(form.state.values).toEqualTypeOf() + return + }, + }) + }) + + it('types should be properly inferred when passing args alongside formOptions', () => { + type Person = { + firstName: string + lastName: string + } + + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + const WithFormComponent = withForm({ + ...formOpts, + onSubmitMeta: { + test: 'test', + }, + render: ({ form }) => { + expectTypeOf(form.handleSubmit).toEqualTypeOf<{ + (): Promise + (submitMeta: { test: string }): Promise + }> + return + }, + }) + }) + + it('types should be properly inferred when formOptions are being overridden', () => { + type Person = { + firstName: string + lastName: string + } + + type PersonWithAge = Person & { + age: number + } + + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + const WithFormComponent = withForm({ + ...formOpts, + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + age: 10, + }, + render: ({ form }) => { + expectTypeOf(form.state.values).toExtend() + return + }, + }) + }) + + it('withForm props should be properly inferred', () => { + const WithFormComponent = withForm({ + props: { + prop1: 'test', + prop2: 10, + }, + render: ({ form, ...props }) => { + expectTypeOf(props).toMatchTypeOf<{ + prop1: string + prop2: number + children?: ComponentChildren + }>() + + return + }, + }) + }) + + it('component made from withForm should have its props properly typed', () => { + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + }, + }) + + const appForm = useAppForm(formOpts) + + const WithFormComponent = withForm({ + ...formOpts, + props: { + prop1: 'test', + prop2: 10, + }, + render: ({ form, children, ...props }) => { + expectTypeOf(props).toMatchTypeOf<{ + prop1: string + prop2: number + }>() + return + }, + }) + + const CorrectComponent = ( + + ) + + // @ts-expect-error Missing required props prop1 and prop2 + const MissingPropsComponent = + + const incorrectFormOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + firstNameWrong: 'FirstName', + lastNameWrong: 'LastName', + }, + }) + + const incorrectAppForm = useAppForm(incorrectFormOpts) + + const IncorrectFormOptsComponent = ( + // @ts-expect-error Incorrect form opts + + ) + }) + + it('should infer subset values and props when calling withFieldGroup', () => { + type Person = { + firstName: string + lastName: string + } + type ComponentProps = { + prop1: string + prop2: number + } + + const defaultValues: Person = { + firstName: 'FirstName', + lastName: 'LastName', + } + + const FormGroupComponent = withFieldGroup({ + defaultValues, + render: function Render({ group, children, ...props }) { + // Existing types may be inferred + expectTypeOf(group.state.values.firstName).toEqualTypeOf() + expectTypeOf(group.state.values.lastName).toEqualTypeOf() + + expectTypeOf(group.state.values).toEqualTypeOf() + expectTypeOf(children).toEqualTypeOf() + expectTypeOf(props).toMatchTypeOf<{}>() + return + }, + }) + + const FormGroupComponentWithProps = withFieldGroup({ + ...defaultValues, + props: {} as ComponentProps, + render: ({ group, children, ...props }) => { + expectTypeOf(props).toMatchTypeOf<{ + prop1: string + prop2: number + }>() + return + }, + }) + }) + + it('should allow spreading formOptions when calling withFieldGroup', () => { + type Person = { + firstName: string + lastName: string + } + + const defaultValues: Person = { + firstName: '', + lastName: '', + } + const formOpts = formOptions({ + defaultValues, + validators: { + onChange: () => 'Error', + }, + listeners: { + onBlur: () => 'Something', + }, + asyncAlways: true, + asyncDebounceMs: 500, + }) + + // validators and listeners are ignored, only defaultValues is acknowledged + const FormGroupComponent = withFieldGroup({ + ...formOpts, + render: function Render({ group }) { + // Existing types may be inferred + expectTypeOf(group.state.values.firstName).toEqualTypeOf() + expectTypeOf(group.state.values.lastName).toEqualTypeOf() + return + }, + }) + + const noDefaultValuesFormOpts = formOptions({ + onSubmitMeta: { foo: '' }, + }) + + const UnknownFormGroupComponent = withFieldGroup({ + ...noDefaultValuesFormOpts, + render: function Render({ group }) { + // group.state.values can be anything. + // note that T extends unknown !== unknown extends T. + expectTypeOf().toExtend() + + // either no submit meta or of the type in formOptions + expectTypeOf(group.handleSubmit).parameters.toEqualTypeOf< + [] | [{ foo: string }] + >() + return + }, + }) + }) + + it('should allow passing compatible forms to withFieldGroup', () => { + type Person = { + firstName: string + lastName: string + } + type ComponentProps = { + prop1: string + prop2: number + } + + const defaultValues: Person = { + firstName: 'FirstName', + lastName: 'LastName', + } + + const FormGroup = withFieldGroup({ + defaultValues, + props: {} as ComponentProps, + render: () => { + return <> + }, + }) + + const equalAppForm = useAppForm({ + defaultValues, + }) + + // ----------------- + // Assert that an equal form is not compatible as you have no name to pass + const NoSubfield = ( + + ) + + // ----------------- + // Assert that a form extending Person in a property is allowed + + const extendedAppForm = useAppForm({ + defaultValues: { person: { ...defaultValues, address: '' }, address: '' }, + }) + // While it has other properties, it satisfies defaultValues + const CorrectComponent1 = ( + + ) + + const MissingProps = ( + // @ts-expect-error because prop1 and prop2 are not added + + ) + + // ----------------- + // Assert that a form not satisfying Person errors + const incompatibleAppForm = useAppForm({ + defaultValues: { person: { ...defaultValues, lastName: 0 } }, + }) + const IncompatibleComponent = ( + + ) + }) + + it('should require strict equal submitMeta if it is set in withFieldGroup', () => { + type Person = { + firstName: string + lastName: string + } + type SubmitMeta = { + correct: string + } + + const defaultValues = { + person: { firstName: 'FirstName', lastName: 'LastName' } as Person, + } + const onSubmitMeta: SubmitMeta = { + correct: 'Prop', + } + + const FormLensNoMeta = withFieldGroup({ + defaultValues: {} as Person, + render: function Render({ group }) { + // Since handleSubmit always allows to submit without meta, this is okay + group.handleSubmit() + + // To prevent unwanted meta behaviour, handleSubmit's meta should be never if not set. + expectTypeOf(group.handleSubmit).parameters.toEqualTypeOf< + [] | [submitMeta: never] + >() + + return + }, + }) + + const FormGroupWithMeta = withFieldGroup({ + defaultValues: {} as Person, + onSubmitMeta, + render: function Render({ group }) { + // Since handleSubmit always allows to submit without meta, this is okay + group.handleSubmit() + + // This matches the value + group.handleSubmit({ correct: '' }) + + // This does not. + // @ts-expect-error + group.handleSubmit({ wrong: 'Meta' }) + + return + }, + }) + + const noMetaForm = useAppForm({ + defaultValues, + }) + + const CorrectComponent1 = ( + + ) + + const WrongComponent1 = ( + + ) + + const metaForm = useAppForm({ + defaultValues, + onSubmitMeta, + }) + + const CorrectComponent2 = + const CorrectComponent3 = ( + + ) + + const diffMetaForm = useAppForm({ + defaultValues, + onSubmitMeta: { ...onSubmitMeta, something: 'else' }, + }) + + const CorrectComponent4 = ( + + ) + const WrongComponent2 = ( + + ) + }) + + it('should accept any validators for withFieldGroup', () => { + type Person = { + firstName: string + lastName: string + } + + const defaultValues = { + person: { firstName: 'FirstName', lastName: 'LastName' } satisfies Person, + } + + const formA = useAppForm({ + defaultValues, + validators: { + onChange: () => 'A', + }, + listeners: { + onChange: () => 'A', + }, + }) + const formB = useAppForm({ + defaultValues, + validators: { + onChange: () => 'B', + }, + listeners: { + onChange: () => 'B', + }, + }) + + const FormGroup = withFieldGroup({ + defaultValues: defaultValues.person, + render: function Render({ group }) { + return + }, + }) + + const CorrectComponent1 = + const CorrectComponent2 = + }) + + it('should allow nesting withFieldGroup in other withFieldGroups', () => { + type Nested = { + firstName: string + } + type Wrapper = { + field: Nested + } + type FormValues = { + form: Wrapper + unrelated: { something: { lastName: string } } + } + + const defaultValues: FormValues = { + form: { + field: { + firstName: 'Test', + }, + }, + unrelated: { + something: { + lastName: '', + }, + }, + } + + const form = useAppForm({ + defaultValues, + }) + const LensNested = withFieldGroup({ + defaultValues: defaultValues.form.field, + render: function Render() { + return <> + }, + }) + const LensWrapper = withFieldGroup({ + defaultValues: defaultValues.form, + render: function Render({ group }) { + return ( +
+ +
+ ) + }, + }) + + const Component = + }) + + it('should not allow withFieldGroups with different metas to be nested', () => { + type Nested = { + firstName: string + } + type Wrapper = { + field: Nested + } + type FormValues = { + form: Wrapper + unrelated: { something: { lastName: string } } + } + + const defaultValues: FormValues = { + form: { + field: { + firstName: 'Test', + }, + }, + unrelated: { + something: { + lastName: '', + }, + }, + } + + const LensNestedNoMeta = withFieldGroup({ + defaultValues: defaultValues.form.field, + render: function Render() { + return <> + }, + }) + const LensNestedWithMeta = withFieldGroup({ + defaultValues: defaultValues.form.field, + onSubmitMeta: { meta: '' }, + render: function Render() { + return <> + }, + }) + const LensWrapper = withFieldGroup({ + defaultValues: defaultValues.form, + render: function Render({ group }) { + return ( +
+ + +
+ ) + }, + }) + + it('should allow mapping withFieldGroup to different fields', () => { + const defaultValues = { + firstName: '', + lastName: '', + age: 0, + relatives: [{ firstName: '', lastName: '', age: 0 }], + } + const defaultFields = { + first: '', + last: '', + } + + const form = useAppForm({ + defaultValues, + }) + + const FieldGroup = withFieldGroup({ + defaultValues: defaultFields, + render: function Render() { + return <> + }, + }) + + const Component1 = ( + + ) + + const Component2 = ( + + ) + }) + + it('should not allow fields mapping if the top level is an array', () => { + const defaultValues = { + firstName: '', + lastName: '', + age: 0, + relatives: [{ firstName: '', lastName: '', age: 0 }], + relativesRecord: { + something: { firstName: '', lastName: '', age: 0 }, + } as Record, + } + const defaultFields = { + firstName: '', + lastName: '', + } + + const form = useAppForm({ + defaultValues, + }) + + const FieldGroupRecord = withFieldGroup({ + defaultValues: { anything: defaultFields } as Record< + string, + typeof defaultFields + >, + render: function Render() { + return <> + }, + }) + const FieldGroupArray = withFieldGroup({ + defaultValues: [defaultFields], + render: function Render() { + return <> + }, + }) + + const CorrectComponent1 = ( + + ) + const WrongComponent1 = ( + + ) + const CorrectComponent3 = ( + + ) + const WrongComponent2 = ( + + ) + }) + }) + + it('should allow mapping field groups to optional fields', () => { + const groupFields = { + name: '', + } + + type WrapperValues = { + namespace: { name: string } | undefined + namespace2: { name: string } | null + namespace3: { name: string } | null | undefined + nope: null | undefined + nope2: { lastName: string } | null | undefined + } + + const defaultValues: WrapperValues = { + namespace: undefined, + namespace2: null, + namespace3: null, + nope: null, + nope2: null, + } + + const FieldGroup = withFieldGroup({ + defaultValues: groupFields, + render: function Render() { + return <> + }, + }) + + const form = useAppForm({ + defaultValues, + }) + + const Component = + const Component2 = + const Component3 = + // @ts-expect-error because it doesn't ever evaluate to the expected values + const Component4 = + // @ts-expect-error because the types don't match properly + const Component5 = + }) + + it('should allow interfaces without index signatures to be assigned to `props` in withForm and withFormGroup', () => { + interface TestNoSignature { + title: string + } + + interface TestWithSignature { + title: string + [key: string]: unknown + } + + const WithFormComponent1 = withForm({ + defaultValues: { name: '' }, + props: {} as TestNoSignature, + render: () => <>, + }) + + const WithFormComponent2 = withForm({ + defaultValues: { name: '' }, + props: {} as TestWithSignature, + render: () => <>, + }) + + const WithFieldGroupComponent1 = withFieldGroup({ + defaultValues: { name: '' }, + props: {} as TestNoSignature, + render: () => <>, + }) + + const WithFieldGroupComponent2 = withFieldGroup({ + defaultValues: { name: '' }, + props: {} as TestWithSignature, + render: () => <>, + }) + + const appForm = useAppForm({ defaultValues: { name: '' } }) + + const Component1 = + const Component2 = ( + + ) + + const FieldGroupComponent1 = ( + + ) + const FieldGroupComponent2 = ( + + ) + }) + + it('should not allow null as prop in withForm and withFormGroup', () => { + const WithFormComponent = withForm({ + defaultValues: { name: '' }, + // @ts-expect-error + props: null, + render: () => <>, + }) + }) + + const WithFieldGroupComponent = withFieldGroup({ + defaultValues: { name: '' }, + // @ts-expect-error + props: null, + render: () => <>, + }) +}) diff --git a/packages/preact-form/tests/createFormHook.test.tsx b/packages/preact-form/tests/createFormHook.test.tsx new file mode 100644 index 000000000..85642e4c7 --- /dev/null +++ b/packages/preact-form/tests/createFormHook.test.tsx @@ -0,0 +1,706 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { render } from '@testing-library/preact' +import { formOptions } from '@tanstack/form-core' +import userEvent from '@testing-library/user-event' +import { createFormHook, createFormHookContexts, useStore } from '../src' + +let user: ReturnType + +beforeEach(() => { + user = userEvent.setup() +}) + +const { fieldContext, useFieldContext, formContext, useFormContext } = + createFormHookContexts() + +function TextField({ label }: { label: string }) { + const field = useFieldContext() + return ( + + ) +} + +function SubscribeButton({ label }: { label: string }) { + const form = useFormContext() + return ( + state.isSubmitting}> + {(isSubmitting) => } + + ) +} + +const { useAppForm, withForm, withFieldGroup, useTypedAppFormContext } = + createFormHook({ + fieldComponents: { + TextField, + }, + formComponents: { + SubscribeButton, + }, + fieldContext, + formContext, + }) + +describe('createFormHook', () => { + it('should allow to set default value', () => { + type Person = { + firstName: string + lastName: string + } + + function Comp() { + const form = useAppForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return ( + <> + } + /> + + ) + } + + const { getByLabelText } = render() + const input = getByLabelText('Testing') + expect(input).toHaveValue('FirstName') + }) + + it('should handle withForm types properly', () => { + const formOpts = formOptions({ + defaultValues: { + firstName: 'John', + lastName: 'Doe', + }, + }) + + const ChildForm = withForm({ + ...formOpts, + // Optional, but adds props to the `render` function outside of `form` + props: { + title: 'Child Form', + }, + render: ({ form, title }) => { + return ( +
+

{title}

+ } + /> + + + +
+ ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + ...formOpts, + }) + + return + } + + const { getByLabelText, getByText } = render() + const input = getByLabelText('First Name') + expect(input).toHaveValue('John') + expect(getByText('Testing')).toBeInTheDocument() + }) + + it('should handle withFieldGroup types properly', () => { + const formOpts = formOptions({ + defaultValues: { + person: { + firstName: 'John', + lastName: 'Doe', + }, + }, + }) + + const ChildForm = withFieldGroup({ + defaultValues: formOpts.defaultValues.person, + // Optional, but adds props to the `render` function outside of `form` + props: { + title: 'Child Form', + }, + render: ({ group, title }) => { + return ( +
+

{title}

+ } + /> + + + +
+ ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + ...formOpts, + }) + + return + } + + const { getByLabelText, getByText } = render() + const input = getByLabelText('First Name') + expect(input).toHaveValue('John') + expect(getByText('Testing')).toBeInTheDocument() + }) + + it('should use the correct field name in Field with withFieldGroup', () => { + const formOpts = formOptions({ + defaultValues: { + person: { + firstName: 'John', + lastName: 'Doe', + }, + people: [ + { + firstName: 'Jane', + lastName: 'Doe', + }, + { + firstName: 'Robert', + lastName: 'Doe', + }, + ], + }, + }) + + const ChildFormAsField = withFieldGroup({ + defaultValues: formOpts.defaultValues.person, + render: ({ group }) => { + return ( +
+ } + /> + + + +
+ ) + }, + }) + const ChildFormAsArray = withFieldGroup({ + defaultValues: [formOpts.defaultValues.person], + props: { + title: '', + }, + render: ({ group, title }) => { + return ( +
+

{title}

+ } + /> + + + +
+ ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + ...formOpts, + }) + + return ( + <> + + + + + ) + } + + const { getByLabelText, getByText } = render() + const inputField1 = getByLabelText('person.firstName') + const inputArray = getByLabelText('people[0].firstName') + const inputField2 = getByLabelText('people[1].firstName') + expect(inputField1).toHaveValue('John') + expect(inputArray).toHaveValue('Jane') + expect(inputField2).toHaveValue('Robert') + expect(getByText('Testing')).toBeInTheDocument() + }) + + it('should forward Field and Subscribe to the form', () => { + const formOpts = formOptions({ + defaultValues: { + person: { + firstName: 'John', + lastName: 'Doe', + }, + }, + }) + + const ChildFormAsField = withFieldGroup({ + defaultValues: formOpts.defaultValues.person, + render: ({ group }) => { + return ( +
+ ( + + )} + /> + state.values.lastName}> + {(lastName) =>

{lastName}

} +
+
+ ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + ...formOpts, + }) + return + } + + const { getByLabelText, getByText } = render() + const input = getByLabelText('person.firstName') + expect(input).toHaveValue('John') + expect(getByText('Doe')).toBeInTheDocument() + }) + + it('should not lose focus on update with withFieldGroup', async () => { + const formOpts = formOptions({ + defaultValues: { + person: { + firstName: 'John', + lastName: 'Doe', + }, + }, + }) + + const ChildForm = withFieldGroup({ + defaultValues: formOpts.defaultValues.person, + render: function Render({ group }) { + const firstName = useStore( + group.store, + (state) => state.values.firstName, + ) + return ( +
+

{firstName}

+ ( + + )} + /> + state.values.lastName}> + {(lastName) =>

{lastName}

} +
+
+ ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + ...formOpts, + }) + return + } + + const { getByLabelText } = render() + + const input = getByLabelText('person.firstName') + input.focus() + expect(input).toHaveFocus() + + await user.clear(input) + await user.type(input, 'Something') + + expect(input).toHaveFocus() + }) + + it('should allow nesting withFieldGroup in other withFieldGroups', () => { + type Nested = { + firstName: string + } + type Wrapper = { + field: Nested + } + type FormValues = { + form: Wrapper + unrelated: { something: { lastName: string } } + } + + const defaultValues: FormValues = { + form: { + field: { + firstName: 'Test', + }, + }, + unrelated: { + something: { + lastName: '', + }, + }, + } + + const LensNested = withFieldGroup({ + defaultValues: defaultValues.form.field, + render: function Render({ group }) { + return ( + + {(field) =>

{field.name}

} +
+ ) + }, + }) + const LensWrapper = withFieldGroup({ + defaultValues: defaultValues.form, + render: function Render({ group }) { + return ( +
+ +
+ ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + defaultValues, + }) + return + } + + const { getByText } = render() + + expect(getByText('form.field.firstName')).toBeInTheDocument() + }) + + it('should allow mapping withFieldGroup to different values', () => { + const formOpts = formOptions({ + defaultValues: { + unrelated: 'John', + values: '', + }, + }) + + const ChildFormAsField = withFieldGroup({ + defaultValues: { firstName: '', lastName: '' }, + render: ({ group }) => { + return ( +
+ } + /> +
+ ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + ...formOpts, + }) + + return ( + + ) + } + + const { getByLabelText } = render() + const inputField1 = getByLabelText('unrelated') + expect(inputField1).toHaveValue('John') + }) + + it('should remap FieldGroupApi.Field validators to the correct names', () => { + const FieldGroupString = withFieldGroup({ + defaultValues: { password: '', confirmPassword: '' }, + render: function Render({ group }) { + return ( + null, + onChangeListenTo: ['password'], + onBlur: () => null, + onBlurListenTo: ['confirmPassword'], + }} + > + {(field) => { + expect(field.options.validators?.onChangeListenTo).toStrictEqual([ + 'account.password', + ]) + expect(field.options.validators?.onBlurListenTo).toStrictEqual([ + 'account.confirmPassword', + ]) + return <> + }} + + ) + }, + }) + + const FieldGroupObject = withFieldGroup({ + defaultValues: { password: '', confirmPassword: '' }, + render: function Render({ group }) { + return ( + null, + onChangeListenTo: ['password'], + onBlur: () => null, + onBlurListenTo: ['confirmPassword'], + }} + > + {(field) => { + expect(field.options.validators?.onChangeListenTo).toStrictEqual([ + 'userPassword', + ]) + expect(field.options.validators?.onBlurListenTo).toStrictEqual([ + 'userConfirmPassword', + ]) + return <> + }} + + ) + }, + }) + + const Parent = () => { + const form = useAppForm({ + defaultValues: { + account: { + password: '', + confirmPassword: '', + }, + userPassword: '', + userConfirmPassword: '', + }, + }) + + return ( + <> + + + + ) + } + + render() + }) + + it('should accept formId and return it', async () => { + function Submit() { + const form = useFormContext() + + return ( + + ) + } + + function Comp() { + const form = useAppForm({ + formId: 'test', + }) + + return ( + +
{ + e.preventDefault() + form.handleSubmit() + }} + >
+ + state.submissionAttempts} + children={(submissionAttempts) => ( + {submissionAttempts} + )} + /> + + +
+ ) + } + + const { getByTestId } = render() + const target = getByTestId('formId-target') + const result = getByTestId('formId-result') + + await user.click(target) + expect(result).toHaveTextContent('1') + }) + + it('should allow using typed app form', () => { + type Person = { + firstName: string + lastName: string + } + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + function Child() { + const form = useTypedAppFormContext(formOpts) + + return ( + } + /> + ) + } + + function Parent() { + const form = useAppForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return ( + + + + ) + } + + const { getByLabelText } = render() + const input = getByLabelText('Testing') + expect(input).toHaveValue('FirstName') + }) + + it('should throw if `useTypedAppFormContext` is used without AppForm', () => { + type Person = { + firstName: string + lastName: string + } + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + function Child() { + const form = useTypedAppFormContext(formOpts) + + return ( + } + /> + ) + } + + function Parent() { + const form = useAppForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return + } + + expect(() => render()).toThrow() + }) + + it('should allow using typed app form with form components', () => { + type Person = { + firstName: string + lastName: string + } + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + function Child() { + const form = useTypedAppFormContext(formOpts) + + return + } + + function Parent() { + const form = useAppForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return ( + + + + ) + } + + const { getByText } = render() + const button = getByText('Testing') + expect(button).toBeInTheDocument() + }) +}) diff --git a/packages/preact-form/tests/test-setup.ts b/packages/preact-form/tests/test-setup.ts new file mode 100644 index 000000000..188abd7e3 --- /dev/null +++ b/packages/preact-form/tests/test-setup.ts @@ -0,0 +1,6 @@ +import '@testing-library/jest-dom/vitest' +import { cleanup } from '@testing-library/preact' +import { afterEach } from 'vitest' + +// https://testing-library.com/docs/react-testing-library/api#cleanup +afterEach(() => cleanup()) diff --git a/packages/preact-form/tests/useField.test-d.tsx b/packages/preact-form/tests/useField.test-d.tsx new file mode 100644 index 000000000..33a943d0c --- /dev/null +++ b/packages/preact-form/tests/useField.test-d.tsx @@ -0,0 +1,109 @@ +import { expectTypeOf, it } from 'vitest' +import { useForm } from '../src/index' + +it('should type state.value properly', () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: 'test', + age: 84, + }, + } as const) + + return ( + <> + { + expectTypeOf(field.state.value).toEqualTypeOf<'test'>() + return null + }} + /> + { + expectTypeOf(field.state.value).toEqualTypeOf<84>() + return null + }} + /> + + ) + } +}) + +it('should type onChange properly', () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: 'test', + age: 84, + }, + } as const) + + return ( + <> + { + expectTypeOf(value).toEqualTypeOf<'test'>() + return null + }, + }} + children={() => null} + /> + { + expectTypeOf(value).toEqualTypeOf<84>() + return null + }, + }} + children={() => null} + /> + + ) + } +}) + +it('should type array subfields', () => { + type FormDefinition = { + nested: { + people: { + name: string + age: number + }[] + } + } + + function App() { + const form = useForm({ + defaultValues: { + nested: { + people: [], + }, + } as FormDefinition, + onSubmit({ value }) { + alert(JSON.stringify(value)) + }, + }) + + return ( + + {(field) => + field.state.value.map((_, i) => ( + + {(subField) => ( + subField.handleChange(e.currentTarget.value)} + /> + )} + + )) + } + + ) + } +}) diff --git a/packages/preact-form/tests/useField.test.tsx b/packages/preact-form/tests/useField.test.tsx new file mode 100644 index 000000000..460b76404 --- /dev/null +++ b/packages/preact-form/tests/useField.test.tsx @@ -0,0 +1,1380 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { render, waitFor, within } from '@testing-library/preact' +import { userEvent } from '@testing-library/user-event' +import { StrictMode, useState } from 'preact/compat' +import { useStore } from '@tanstack/preact-store' +import { useForm } from '../src/index' +import { sleep } from './utils' +import type { AnyFieldApi } from '../src/index' + +let user: ReturnType + +beforeEach(() => { + user = userEvent.setup() +}) + +describe('useField', () => { + it('should allow to set default value', () => { + type Person = { + firstName: string + lastName: string + } + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return ( + <> + { + return ( + field.handleChange(e.currentTarget.value)} + /> + ) + }} + /> + + ) + } + + const { getByTestId } = render() + const input = getByTestId('fieldinput') + expect(input).toHaveValue('FirstName') + }) + + it('should use field default value first', () => { + type Person = { + firstName: string + lastName: string + } + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return ( + <> + { + return ( + field.handleChange(e.currentTarget.value)} + /> + ) + }} + /> + + ) + } + + const { getByTestId } = render() + const input = getByTestId('fieldinput') + expect(input).toHaveValue('otherName') + }) + + it('should not validate on change if isTouched is false', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + }) + + return ( + <> + (value === 'other' ? error : undefined), + }} + children={(field) => ( +
+ + field.setValue(e.currentTarget.value, { + dontUpdateMeta: true, + }) + } + /> +

{field.getMeta().errors}

+
+ )} + /> + + ) + } + + const { getByTestId, queryByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + expect(queryByText(error)).not.toBeInTheDocument() + }) + + it('should validate on change if isTouched is true', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + }) + + return ( + <> + (value === 'other' ? error : undefined), + }} + children={(field) => ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{field.getMeta().errorMap.onChange}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate on change and on blur', async () => { + type Person = { + firstName: string + lastName: string + } + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + }) + + return ( + <> + + value === 'other' ? onChangeError : undefined, + onBlur: ({ value }) => + value === 'other' ? onBlurError : undefined, + }} + children={(field) => ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{field.getMeta().errorMap.onChange}

+

{field.getMeta().errorMap.onBlur}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + expect(queryByText(onBlurError)).toBeInTheDocument() + }) + + it('should properly update conditionally rendered fields', async () => { + type FormValues = { + firstField: string + secondField: string + showFirstField: boolean + } + + function Comp() { + const form = useForm({ + defaultValues: { + firstField: '', + secondField: '', + showFirstField: true, + } as FormValues, + }) + + return ( + <> + + {({ handleChange, state }) => ( +
+ Show first field + { + handleChange(e.currentTarget.checked) + }} + /> +
+ )} +
+ state.values.showFirstField}> + {(someFlagChecked) => { + if (someFlagChecked) { + return ( + + {({ handleChange, state }) => ( + + )} + + ) + } + + return ( + + {({ handleChange, state }) => ( + + )} + + ) + }} + + + ) + } + + const { getByTestId } = render() + + const showFirstFieldInput = getByTestId('show-first-field') + + await user.type(getByTestId('first-field'), 'hello') + expect((getByTestId('first-field') as HTMLInputElement).value).toBe('hello') + + await user.click(showFirstFieldInput) + await user.type(getByTestId('second-field'), 'world') + expect((getByTestId('second-field') as HTMLInputElement).value).toBe( + 'world', + ) + + await user.click(showFirstFieldInput) + expect((getByTestId('first-field') as HTMLInputElement).value).toBe('hello') + }) + + it('should validate async on change', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + }) + + return ( + <> + { + await sleep(10) + return error + }, + }} + children={(field) => ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{field.getMeta().errorMap.onChange}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate async on change and async on blur', async () => { + type Person = { + firstName: string + lastName: string + } + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + }) + + return ( + <> + { + await sleep(10) + return onChangeError + }, + onBlurAsync: async () => { + await sleep(10) + return onBlurError + }, + }} + children={(field) => ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{field.getMeta().errorMap.onChange}

+

{field.getMeta().errorMap.onBlur}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(onChangeError)) + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + await waitFor(() => getByText(onBlurError)) + expect(getByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change with debounce', async () => { + type Person = { + firstName: string + lastName: string + } + const mockFn = vi.fn() + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + }) + + return ( + <> + { + mockFn() + await sleep(10) + return error + }, + }} + children={(field) => ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{field.getMeta().errors}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + // mockFn will have been called 5 times without onChangeAsyncDebounceMs + expect(mockFn).toHaveBeenCalledTimes(0) + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it('should handle strict mode properly with conditional fields', async () => { + function FieldInfo({ field }: { field: AnyFieldApi }) { + return ( + <> + {field.state.meta.isTouched && field.state.meta.errors.length ? ( + {field.state.meta.errors.join(',')} + ) : null} + {field.state.meta.isValidating ? 'Validating...' : null} + + ) + } + + function Comp() { + const [showField, setShowField] = useState(true) + + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + onSubmit: async () => {}, + }) + + return ( +
+
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > +
+ {/* A type-safe field component*/} + {showField ? ( + + !value ? 'A first name is required' : undefined, + }} + children={(field) => { + // Avoid hasty abstractions. Render props are great! + return ( + <> + + field.handleChange(e.currentTarget.value)} + /> + + + ) + }} + /> + ) : null} +
+
+ ( + <> + + field.handleChange(e.currentTarget.value)} + /> + + + )} + /> +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + + +
+ ) + } + + const { getByText, findByText, queryByText } = render( + + + , + ) + + await user.click(getByText('Submit')) + expect(await findByText('A first name is required')).toBeInTheDocument() + await user.click(getByText('Hide field')) + await user.click(getByText('Submit')) + expect(queryByText('A first name is required')).not.toBeInTheDocument() + }) + + it('should handle arrays with primitive values', async () => { + const fn = vi.fn() + function Comp() { + const form = useForm({ + defaultValues: { + people: [] as Array, + }, + onSubmit: ({ value }) => fn(value), + }) + + return ( +
+
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > + + {(field) => { + return ( +
+ {field.state.value.map((_, i) => { + return ( + + {(subField) => { + return ( +
+ + +
+ ) + }} +
+ ) + })} + +
+ ) + }} +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + +
+ ) + } + + const { getByText, findByLabelText, queryByText, findByText } = render( + , + ) + + expect(queryByText('Name for person 0')).not.toBeInTheDocument() + expect(queryByText('Name for person 1')).not.toBeInTheDocument() + await user.click(getByText('Add person')) + const input = await findByLabelText('Name for person 0') + expect(input).toBeInTheDocument() + await user.type(input, 'John') + + await user.click(getByText('Add person')) + const input2 = await findByLabelText('Name for person 1') + expect(input).toBeInTheDocument() + await user.type(input2, 'Jack') + + expect(queryByText('Name for person 0')).toBeInTheDocument() + expect(queryByText('Name for person 1')).toBeInTheDocument() + await user.click(getByText('Remove person 1')) + expect(queryByText('Name for person 0')).toBeInTheDocument() + expect(queryByText('Name for person 1')).not.toBeInTheDocument() + + await user.click(await findByText('Submit')) + expect(fn).toHaveBeenCalledWith({ people: ['John'] }) + }) + + it('should handle arrays with subvalues', async () => { + const fn = vi.fn() + function Comp() { + const form = useForm({ + defaultValues: { + people: [] as Array<{ age: number; name: string }>, + }, + onSubmit: ({ value }) => fn(value), + }) + + return ( +
+
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > + + {(field) => { + return ( +
+ {field.state.value.map((_, i) => { + return ( + + {(subField) => { + return ( +
+ + +
+ ) + }} +
+ ) + })} + +
+ ) + }} +
+ [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> + +
+ ) + } + + const { getByText, findByLabelText, queryByText, findByText } = render( + , + ) + + expect(queryByText('Name for person 0')).not.toBeInTheDocument() + expect(queryByText('Name for person 1')).not.toBeInTheDocument() + await user.click(getByText('Add person')) + const input = await findByLabelText('Name for person 0') + expect(input).toBeInTheDocument() + await user.type(input, 'John') + + await user.click(getByText('Add person')) + const input2 = await findByLabelText('Name for person 1') + expect(input).toBeInTheDocument() + await user.type(input2, 'Jack') + + expect(queryByText('Name for person 0')).toBeInTheDocument() + expect(queryByText('Name for person 1')).toBeInTheDocument() + await user.click(getByText('Remove person 1')) + expect(queryByText('Name for person 0')).toBeInTheDocument() + expect(queryByText('Name for person 1')).not.toBeInTheDocument() + + await user.click(await findByText('Submit')) + expect(fn).toHaveBeenCalledWith({ people: [{ name: 'John', age: 0 }] }) + }) + + it('should handle sync linked fields', async () => { + const fn = vi.fn() + function Comp() { + const form = useForm({ + defaultValues: { + password: '', + confirm_password: '', + }, + onSubmit: ({ value }) => fn(value), + }) + + return ( +
+ + {(field) => { + return ( +
+ +
+ ) + }} +
+ { + if (value !== fieldApi.form.getFieldValue('password')) { + return 'Passwords do not match' + } + return undefined + }, + }} + > + {(field) => { + return ( +
+ + {field.state.meta.errors.map((err) => { + return
{err}
+ })} +
+ ) + }} +
+
+ ) + } + + const { findByLabelText, queryByText, findByText } = render() + + const passwordInput = await findByLabelText('Password') + const confirmPasswordInput = await findByLabelText('Confirm Password') + await user.type(passwordInput, 'password') + await user.type(confirmPasswordInput, 'password') + expect(queryByText('Passwords do not match')).not.toBeInTheDocument() + await user.type(confirmPasswordInput, '1') + expect(await findByText('Passwords do not match')).toBeInTheDocument() + }) + + it('should handle deeply nested values in StrictMode', async () => { + function Comp() { + const form = useForm({ + defaultValues: { + name: { first: 'Test', last: 'User' }, + }, + }) + + return ( +

{field.state.value}

} + /> + ) + } + + const { queryByText, findByText } = render( + + + , + ) + + expect(queryByText('Test')).not.toBeInTheDocument() + expect(await findByText('User')).toBeInTheDocument() + }) + + it('should validate async on submit without debounce', async () => { + type Person = { + firstName: string + lastName: string + } + const mockFn = vi.fn() + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + validators: { + onChangeAsyncDebounceMs: 1000000, + onChangeAsync: async () => { + mockFn() + await sleep(10) + return error + }, + }, + }) + const errors = useStore(form.store, (s) => s.errors) + + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{errors}

+
+ )} + /> + + + ) + } + + const { getByRole, getByText } = render() + await user.click(getByRole('button', { name: 'Submit' })) + + await waitFor(() => expect(mockFn).toHaveBeenCalledTimes(1)) + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate allow pushvalue to implicitly set a default value', async () => { + type Person = { + people: Array + } + + function Comp() { + const form = useForm({ + defaultValues: { + people: [], + } as Person, + }) + + return ( + + {(field) => { + return ( +
+
{JSON.stringify(field.state.value)}
+ {field.state.value.map((_, i) => { + return ( + + {(subField) => { + return ( +
+ +
+ ) + }} +
+ ) + })} + +
+ ) + }} +
+ ) + } + + const { getByText, queryByText } = render( + + + , + ) + expect(getByText('[]')).toBeInTheDocument() + await user.click(getByText('Add person')) + expect(getByText(`[""]`)).toBeInTheDocument() + }) + + it('should validate allow pushvalue to implicitly set a pushed default value', async () => { + type Person = { + people: Array + } + + function Comp() { + const form = useForm({ + defaultValues: { + people: [], + } as Person, + }) + + return ( + + {(field) => { + return ( +
+
{JSON.stringify(field.state.value)}
+ {field.state.value.map((_, i) => { + return ( + + {(subField) => { + return ( +
+ +
+ ) + }} +
+ ) + })} + +
+ ) + }} +
+ ) + } + + const { getByText, queryByText } = render( + + + , + ) + expect(getByText('[]')).toBeInTheDocument() + await user.click(getByText('Add person')) + expect(getByText(`["Test"]`)).toBeInTheDocument() + }) + + it('should handle removing element from array', async () => { + type Person = { + name: string + id: number + } + + const fakePeople = { + jack: { + id: 5, + name: 'Jack', + }, + molly: { + id: 6, + name: 'Molly', + }, + george: { + id: 7, + name: 'George', + }, + } satisfies Record + + function Comp() { + const form = useForm({ + defaultValues: { + people: [fakePeople.jack, fakePeople.molly, fakePeople.george], + }, + }) + + return ( + + {(field) => { + return ( +
+
+ {field.state.value.map((item, i) => { + return ( + + {(subField) => { + return ( +
+ +
+ ) + }} +
+ ) + })} +
+ +
+ ) + }} +
+ ) + } + + const { getByText, queryByText, getByTestId } = render( + + + , + ) + + let exisingPeople: Person[] = [ + fakePeople.jack, + fakePeople.molly, + fakePeople.george, + ] + exisingPeople.forEach((person) => + expect(getByText(person.name)).toBeInTheDocument(), + ) + const container = getByTestId('container') + expect(within(container).getAllByRole('textbox')).toHaveLength(3) + + await user.click(getByText('Remove person')) + + expect(within(container).getAllByRole('textbox')).toHaveLength(2) + exisingPeople = [fakePeople.jack, fakePeople.george] + exisingPeople.forEach((person) => + expect(getByText(person.name)).toBeInTheDocument(), + ) + expect(queryByText(fakePeople.molly.name)).not.toBeInTheDocument() + }) + + it('should not rerender unrelated fields', async () => { + const renderCount = { + field1: 0, + field2: 0, + } + + function Comp() { + const form = useForm({ + defaultValues: { + field1: '', + field2: '', + }, + }) + + return ( + <> + + {(field) => { + renderCount.field1++ + return ( + field.handleChange(e.currentTarget.value)} + /> + ) + }} + + + {(field) => { + renderCount.field2++ + return ( + field.handleChange(e.currentTarget.value)} + /> + ) + }} + + + ) + } + + const { getByTestId } = render( + + + , + ) + + const field1InitialRender = renderCount.field1 + const field2InitialRender = renderCount.field2 + + await user.type(getByTestId('field1'), 'test') + + // field1 should have rerendered + expect(renderCount.field1).toBeGreaterThan(field1InitialRender) + // field2 should not have rerendered + expect(renderCount.field2).toBe(field2InitialRender) + }) + + it('should not rerender array field when child field value changes', async () => { + // Test for https://github.com/TanStack/form/issues/1925 + // Array fields should only re-render when the array length changes, + // not when a property on an array element is mutated + const renderCount = { + arrayField: 0, + childField: 0, + } + + function Comp() { + const form = useForm({ + defaultValues: { + people: [{ name: 'John' }, { name: 'Jane' }], + }, + }) + + return ( + + {(arrayField) => { + renderCount.arrayField++ + return ( +
+ {arrayField.state.value.map((_, i) => ( + + {(field) => { + if (i === 0) renderCount.childField++ + return ( + field.handleChange(e.currentTarget.value)} + /> + ) + }} + + ))} + +
+ ) + }} +
+ ) + } + + const { getByTestId } = render( + + + , + ) + + const arrayFieldInitialRender = renderCount.arrayField + const childFieldInitialRender = renderCount.childField + + // Type into the first child field + await user.type(getByTestId('person-0'), 'ny') + + // Child field should have rerendered + expect(renderCount.childField).toBeGreaterThan(childFieldInitialRender) + // Array field should NOT have rerendered (this was the bug in #1925) + expect(renderCount.arrayField).toBe(arrayFieldInitialRender) + + // Verify typing still works + expect(getByTestId('person-0')).toHaveValue('Johnny') + + // Now add a new item - this SHOULD trigger array field re-render + const arrayFieldBeforeAdd = renderCount.arrayField + await user.click(getByTestId('add-person')) + + // Array field should have rerendered when length changes + expect(renderCount.arrayField).toBeGreaterThan(arrayFieldBeforeAdd) + }) + + it('should handle defaultValue without setstate-in-render error', async () => { + // Spy on console.error before rendering + const consoleErrorSpy = vi.spyOn(console, 'error') + + function Comp() { + const form = useForm({ + defaultValues: { + fieldOne: '', + fieldTwo: '', + }, + }) + + const fieldOne = useStore(form.store, (state) => state.values.fieldOne) + + return ( +
+ { + return ( + field.handleChange(e.currentTarget.value)} + /> + ) + }} + /> + {fieldOne && ( + null} + /> + )} + + ) + } + + const { getByTestId } = render() + await user.type(getByTestId('fieldOne'), 'John') + + // Should not log an error + expect(consoleErrorSpy).not.toHaveBeenCalled() + }) + + it('should allow field-level defaultValue', async () => { + function Comp() { + const form = useForm({ + defaultValues: { + name: undefined as string | undefined, + }, + }) + + return ( + + {(field) => { + expect(field.state.value).toEqual('a') + return {field.state.value} + }} + + ) + } + + const { queryByText } = render() + + expect(queryByText('a')).toBeInTheDocument() + expect(queryByText('never')).not.toBeInTheDocument() + }) +}) diff --git a/packages/preact-form/tests/useForm.test-d.tsx b/packages/preact-form/tests/useForm.test-d.tsx new file mode 100644 index 000000000..2e92cf29c --- /dev/null +++ b/packages/preact-form/tests/useForm.test-d.tsx @@ -0,0 +1,157 @@ +import { describe, expectTypeOf, it } from 'vitest' +import { formOptions, useForm } from '../src/index' +import type { FormAsyncValidateOrFn, FormValidateOrFn } from '../src/index' +import type { ReactFormExtendedApi } from '../src/useForm' + +describe('useForm', () => { + it('should type onSubmit properly', () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: 'test', + age: 84, + // as const is required here + } as const, + onSubmit({ value }) { + expectTypeOf(value.age).toEqualTypeOf<84>() + }, + }) + } + }) + + it('should type a validator properly', () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: 'test', + age: 84, + // as const is required here + } as const, + validators: { + onChange({ value }) { + expectTypeOf(value.age).toEqualTypeOf<84>() + return undefined + }, + }, + }) + } + }) + + it('should not have recursion problems and type register properly', () => { + const register = < + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta, + >( + f: ReactFormExtendedApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, + ) => f + + function Comp() { + const form = useForm({ + defaultValues: { + name: '', + title: '', + }, + }) + + const x = register(form) + + return null + } + }) + + it('types should be properly inferred when using formOptions', () => { + type Person = { + firstName: string + lastName: string + } + + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + const form = useForm(formOpts) + + expectTypeOf(form.state.values).toEqualTypeOf() + }) + + it('types should be properly inferred when passing args alongside formOptions', () => { + type Person = { + firstName: string + lastName: string + } + + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + const form = useForm({ + ...formOpts, + onSubmitMeta: { + test: 'test', + }, + }) + + expectTypeOf(form.handleSubmit).toEqualTypeOf<{ + (): Promise + (submitMeta: { test: string }): Promise + }>() + }) + + it('types should be properly inferred when formOptions are being overridden', () => { + type Person = { + firstName: string + lastName: string + } + + type PersonWithAge = Person & { + age: number + } + + const formOpts = formOptions({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + const form = useForm({ + ...formOpts, + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + age: 10, + }, + }) + + expectTypeOf(form.state.values).toExtend() + }) +}) diff --git a/packages/preact-form/tests/useForm.test.tsx b/packages/preact-form/tests/useForm.test.tsx new file mode 100644 index 000000000..e37171048 --- /dev/null +++ b/packages/preact-form/tests/useForm.test.tsx @@ -0,0 +1,1083 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { render, waitFor } from '@testing-library/preact' +import { userEvent } from '@testing-library/user-event' +import { useStore } from '@tanstack/preact-store' +import { useCallback, useEffect, useReducer, useState } from 'preact/compat' +import { mergeForm, useForm } from '../src/index' +import { sleep } from './utils' + +let user: ReturnType + +beforeEach(() => { + user = userEvent.setup() +}) + +describe('useForm', () => { + it('preserves field state', async () => { + type Person = { + firstName: string + lastName: string + } + + function Comp() { + const form = useForm({ + defaultValues: {} as Person, + }) + + return ( + <> + { + return ( + field.handleChange(e.currentTarget.value)} + /> + ) + }} + /> + + ) + } + + const { getByTestId, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText('FirstName')).not.toBeInTheDocument() + await user.type(input, 'FirstName') + expect(input).toHaveValue('FirstName') + }) + + it('should allow default values to be set', async () => { + type Person = { + firstName: string + lastName: string + } + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: 'FirstName', + lastName: 'LastName', + } as Person, + }) + + return ( + <> + { + return

{field.state.value}

+ }} + /> + + ) + } + + const { findByText, queryByText } = render() + expect(await findByText('FirstName')).toBeInTheDocument() + expect(queryByText('LastName')).not.toBeInTheDocument() + }) + + it('should handle submitting properly', async () => { + function Comp() { + const [submittedData, setSubmittedData] = useState<{ + firstName: string + } | null>(null) + + const form = useForm({ + defaultValues: { + firstName: 'FirstName', + }, + onSubmit: ({ value }) => { + setSubmittedData(value) + }, + }) + + return ( + <> + { + return ( + field.handleChange(e.currentTarget.value)} + placeholder={'First name'} + /> + ) + }} + /> + + {submittedData &&

Submitted data: {submittedData.firstName}

} + + ) + } + + const { findByPlaceholderText, getByText } = render() + const input = await findByPlaceholderText('First name') + await user.clear(input) + await user.type(input, 'OtherName') + await user.click(getByText('Submit')) + await waitFor(() => + expect(getByText('Submitted data: OtherName')).toBeInTheDocument(), + ) + }) + + it('should run on form mount', async () => { + function Comp() { + const [formMounted, setFormMounted] = useState(false) + const [mountForm, setMountForm] = useState(false) + + const form = useForm({ + defaultValues: { + firstName: 'FirstName', + }, + validators: { + onMount: () => { + setFormMounted(true) + return undefined + }, + }, + }) + + return ( + <> + {mountForm ? ( + <> +

{formMounted ? 'Form mounted' : 'Not mounted'}

+ + ) : ( + + )} + + ) + } + + const { getByText } = render() + await user.click(getByText('Mount form')) + await waitFor(() => expect(getByText('Form mounted')).toBeInTheDocument()) + }) + + it('should validate async on change for the form', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + validators: { + onChange() { + return error + }, + }, + }) + const onChangeError = useStore(form.store, (s) => s.errorMap.onChange) + return ( + <> + ( + field.handleChange(e.currentTarget.value)} + /> + )} + /> +

{onChangeError?.toString()}

+ + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it('should not validate on change if isTouched is false', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + validators: { + onChange: ({ value }) => + value.firstName === 'other' ? error : undefined, + }, + }) + + const errors = useStore(form.store, (s) => s.errors) + return ( + <> + ( +
+ + field.setValue(e.currentTarget.value, { + dontUpdateMeta: true, + }) + } + /> +

{errors}

+
+ )} + /> + + ) + } + + const { getByTestId, queryByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + expect(queryByText(error)).not.toBeInTheDocument() + }) + + it('should validate on change if isTouched is true', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + validators: { + onChange: ({ value }) => + value.firstName === 'other' ? error : undefined, + }, + }) + const errors = useStore(form.store, (s) => s.errorMap) + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{errors.onChange?.toString()}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(error)).toBeInTheDocument() + }) + + it('should validate on change and on blur', async () => { + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + }, + validators: { + onChange: ({ value }) => { + if (value.firstName === 'other') return onChangeError + return undefined + }, + onBlur: ({ value }) => { + if (value.firstName === 'other') return onBlurError + return undefined + }, + }, + }) + + const errors = useStore(form.store, (s) => s.errorMap) + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{errors.onChange?.toString()}

+

{errors.onBlur?.toString()}

+
+ )} + /> + + ) + } + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + expect(queryByText(onBlurError)).toBeInTheDocument() + }) + + it("should set field errors from the field's validators", async () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + }, + validators: { + onChange: ({ value }) => { + if (value.firstName === 'other') + return { + form: 'Something went wrong', + fields: { + firstName: 'Please enter a different value (onChangeError)', + }, + } + return undefined + }, + onBlur: ({ value }) => { + if (value.firstName === 'other') + return 'Please enter a different value (onBlurError)' + return undefined + }, + }, + }) + + const errors = useStore(form.store, (s) => s.errorMap) + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

+ {field.state.meta.errorMap.onChange} +

+
+ )} + /> +

{errors.onChange?.toString()}

+

{errors.onBlur?.toString()}

+ + ) + } + const { getByTestId } = render() + + expect(getByTestId('form-onchange')).toHaveTextContent('') + + const input = getByTestId('fieldinput') + + await user.type(input, 'other') + expect(getByTestId('form-onchange')).toHaveTextContent( + 'Something went wrong', + ) + expect(getByTestId('field-onchange')).toHaveTextContent( + 'Please enter a different value (onChangeError)', + ) + + await user.click(document.body) + expect(getByTestId('form-onblur')).toHaveTextContent( + 'Please enter a different value (onBlurError)', + ) + }) + + it('should validate async on change', async () => { + type Person = { + firstName: string + lastName: string + } + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + validators: { + onChangeAsync: async () => { + await sleep(10) + return error + }, + }, + }) + const errors = useStore(form.store, (s) => s.errorMap) + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{errors.onChange?.toString()}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + expect(queryByText(error)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it("should set field errors from the the form's onChangeAsync validator", async () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + validators: { + onChangeAsync: async ({ value }) => { + await sleep(10) + if (value.firstName === 'other') { + return { + form: 'Invalid form values', + fields: { + firstName: 'First name cannot be "other"', + }, + } + } + return null + }, + }, + }) + + const errors = useStore(form.store, (s) => s.errorMap) + + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

+ {field.state.meta.errorMap.onChange} +

+
+ )} + /> +

{errors.onChange?.toString()}

+ + ) + } + + const { getByTestId } = render() + const input = getByTestId('fieldinput') + const firstNameErrorElement = getByTestId('field-error') + const formErrorElement = getByTestId('form-error') + + expect(firstNameErrorElement).toBeEmptyDOMElement() + expect(formErrorElement).toBeEmptyDOMElement() + + await user.type(input, 'other') + + await waitFor(() => { + expect(firstNameErrorElement).not.toBeEmptyDOMElement() + }) + expect(firstNameErrorElement).toHaveTextContent( + 'First name cannot be "other"', + ) + expect(formErrorElement).toHaveTextContent('Invalid form values') + }) + + it('should validate async on change and async on blur', async () => { + type Person = { + firstName: string + lastName: string + } + const onChangeError = 'Please enter a different value (onChangeError)' + const onBlurError = 'Please enter a different value (onBlurError)' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + validators: { + onChangeAsync: async () => { + await sleep(10) + return onChangeError + }, + onBlurAsync: async () => { + await sleep(10) + return onBlurError + }, + }, + }) + const errors = useStore(form.store, (s) => s.errorMap) + + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{errors.onChange?.toString()}

+

{errors.onBlur?.toString()}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText, queryByText } = render() + const input = getByTestId('fieldinput') + + expect(queryByText(onChangeError)).not.toBeInTheDocument() + expect(queryByText(onBlurError)).not.toBeInTheDocument() + await user.type(input, 'other') + await waitFor(() => getByText(onChangeError)) + expect(getByText(onChangeError)).toBeInTheDocument() + await user.click(document.body) + await waitFor(() => getByText(onBlurError)) + expect(getByText(onBlurError)).toBeInTheDocument() + }) + + it('should validate async on change with debounce', async () => { + type Person = { + firstName: string + lastName: string + } + const mockFn = vi.fn() + const error = 'Please enter a different value' + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + } as Person, + validators: { + onChangeAsyncDebounceMs: 100, + onChangeAsync: async () => { + mockFn() + await sleep(10) + return error + }, + }, + }) + const errors = useStore(form.store, (s) => s.errors) + + return ( + <> + ( +
+ field.handleChange(e.currentTarget.value)} + /> +

{errors}

+
+ )} + /> + + ) + } + + const { getByTestId, getByText } = render() + const input = getByTestId('fieldinput') + await user.type(input, 'other') + // mockFn will have been called 5 times without onChangeAsyncDebounceMs + expect(mockFn).toHaveBeenCalledTimes(0) + await waitFor(() => getByText(error)) + expect(getByText(error)).toBeInTheDocument() + }) + + it("should set errors on the fields from the form's onChange validator", async () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + validators: { + onChange: ({ value }) => { + if (value.firstName === 'Tom') { + return { + form: 'Something went wrong', + fields: { firstName: 'Please enter a different value' }, + } + } + return null + }, + }, + }) + + const onChangeError = useStore(form.store, (s) => s.errorMap.onChange) + + return ( + <> + ( + <> + field.handleChange(e.currentTarget.value)} + /> + +

+ {field.state.meta.errors.join('')} +

+ + )} + /> +

{onChangeError?.toString()}

+ + ) + } + + const { getByTestId, queryByText } = render() + const input = getByTestId('fieldinput') + const fieldError = getByTestId('field-error') + const formError = getByTestId('form-error') + + expect( + queryByText('Please enter a different value'), + ).not.toBeInTheDocument() + expect(queryByText('Something went wrong')).not.toBeInTheDocument() + + await user.type(input, 'Tom') + await waitFor(() => + expect(fieldError.textContent).toBe('Please enter a different value'), + ) + await waitFor(() => + expect(formError.textContent).toBe('Something went wrong'), + ) + }) + + it('should not cause infinite re-renders when listening to state.errors', () => { + const fn = vi.fn() + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value) + }, + }) + + const { errors } = useStore(form.store, (state) => ({ + errors: state.errors, + })) + + useEffect(() => { + fn(errors) + }, [errors]) + + return null + } + + render() + + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('should not cause infinite re-renders when listening to state', () => { + const fn = vi.fn() + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value) + }, + }) + + const { values } = useStore(form.store) + + useEffect(() => { + fn(values) + }, [values]) + + return null + } + + render() + + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('form should reset default value when resetting in onSubmit', async () => { + function Comp() { + const form = useForm({ + defaultValues: { + name: '', + }, + onSubmit: ({ value }) => { + expect(value).toEqual({ name: 'another-test' }) + + form.reset(value) + }, + }) + + return ( +
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > + ( + field.handleChange(e.currentTarget.value)} + /> + )} + /> + + + + + + ) + } + + const { getByTestId } = render() + const input = getByTestId('fieldinput') + const submit = getByTestId('submit') + const reset = getByTestId('reset') + + await user.type(input, 'test') + await waitFor(() => expect(input).toHaveValue('test')) + + await user.click(reset) + await waitFor(() => expect(input).toHaveValue('')) + + await user.clear(input) + await user.type(input, 'another-test') + await user.click(submit) + await waitFor(() => expect(input).toHaveValue('another-test')) + }) + + it('should accept formId and return it', async () => { + function Comp() { + const form = useForm({ + formId: 'test', + }) + + return ( + <> +
{ + e.preventDefault() + form.handleSubmit() + }} + >
+ + state.submissionAttempts} + children={(submissionAttempts) => ( + {submissionAttempts} + )} + /> + + + + ) + } + + const { getByTestId } = render() + const target = getByTestId('formId-target') + const result = getByTestId('formId-result') + + await user.click(target) + expect(result).toHaveTextContent('1') + }) + + it('should allow custom component keys for arrays', async () => { + function Comp() { + const form = useForm({ + defaultValues: { + foo: [ + { name: 'nameA', id: 'a' }, + { name: 'nameB', id: 'b' }, + { name: 'nameC', id: 'c' }, + ], + }, + }) + + return ( + <> + + {(arrayField) => + arrayField.state.value.map((_, i) => ( + // eslint-disable-next-line @eslint-react/no-array-index-key + + {(field) => { + expect(field.name).toBe(`foo[${i}].name`) + expect(field.state.value).not.toBeUndefined() + return null + }} + + )) + } + + + + ) + } + + const { getByTestId } = render() + + const target = getByTestId('removeField') + await user.click(target) + }) + + it('should not error when using deleteField in edge cases', async () => { + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + validators: { + onChange: ({ value }) => { + const fields: Record = {} + + if (value.firstName.length === 0) { + fields.firstName = 'Last Name is required' + } + + return { fields } + }, + }, + }) + + return ( +
{ + e.preventDefault() + form.handleSubmit() + }} + > +

Personal Information

+ ( + field.handleChange(e.currentTarget.value)} + /> + )} + /> + ( + field.handleChange(e.currentTarget.value)} + /> + )} + /> + + + ) + } + + const { getByTestId } = render() + const removeButton = getByTestId('remove') + const input = getByTestId('input') + + await user.type(input, 'a') + await user.click(removeButton) + }) + + it('should handle stable transforms to update the base form on first render', async () => { + let renders = 0 + function Comp() { + const form = useForm({ + defaultValues: { + test: 'Hello', + }, + transform: useCallback((baseForm: unknown) => { + return mergeForm(baseForm as never, { + values: { + test: 'What', + }, + }) + }, []), + }) + + renders++ + + return ( + ( +

+ {field.state.value} {renders} +

+ )} + /> + ) + } + + const { getByText } = render() + getByText('What 1') + }) + + it('should handle stable transforms to update the base form on subsequent renders', async () => { + function Comp() { + const [renders, setRenders] = useState(0) + const form = useForm({ + defaultValues: { + test: 'Hello', + }, + transform: useCallback( + (baseForm: unknown) => { + return mergeForm(baseForm as never, { + values: { + test: renders === 0 ? 'First' : 'Another', + }, + }) + }, + [renders], + ), + }) + + return ( +
+

{field.state.value}

} + /> + +
+ ) + } + + const { findByText, getByText } = render() + await findByText('First') + await user.click(getByText('Rerender')) + await findByText('Another') + }) +}) diff --git a/packages/preact-form/tests/utils.ts b/packages/preact-form/tests/utils.ts new file mode 100644 index 000000000..1a3a619a2 --- /dev/null +++ b/packages/preact-form/tests/utils.ts @@ -0,0 +1,5 @@ +export function sleep(timeout: number): Promise { + return new Promise((resolve, _reject) => { + setTimeout(resolve, timeout) + }) +} diff --git a/packages/preact-form/tsconfig.docs.json b/packages/preact-form/tsconfig.docs.json new file mode 100644 index 000000000..2c9444e16 --- /dev/null +++ b/packages/preact-form/tsconfig.docs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "paths": { + "@tanstack/form-core": ["../form-core/src"] + } + }, + "exclude": ["tests", "eslint.config.js", "vite.config.ts"] +} diff --git a/packages/preact-form/tsconfig.json b/packages/preact-form/tsconfig.json new file mode 100644 index 000000000..f5c99bbdb --- /dev/null +++ b/packages/preact-form/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact", + "moduleResolution": "Bundler", + "paths": { + "@tanstack/form-core": ["../form-core/src"] + } + }, + "include": ["src", "tests", "eslint.config.js", "vite.config.ts"] +} diff --git a/packages/preact-form/vite.config.ts b/packages/preact-form/vite.config.ts new file mode 100644 index 000000000..a81e89b5b --- /dev/null +++ b/packages/preact-form/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/vite-config' +import preact from '@preact/preset-vite' +import packageJson from './package.json' + +const config = defineConfig({ + plugins: [preact()], + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + setupFiles: ['./tests/test-setup.ts'], + coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, + typecheck: { enabled: true }, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts'], + srcDir: './src', + }), +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 498efb24a..2d497fce3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,7 +130,7 @@ importers: version: 20.3.6(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.6(@angular/animations@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@tanstack/angular-form': specifier: ^1.28.3 - version: link:../../../packages/angular-form + version: 1.28.3(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) rxjs: specifier: ^7.8.2 version: 7.8.2 @@ -182,7 +182,7 @@ importers: version: 20.3.6(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.6(@angular/animations@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@tanstack/angular-form': specifier: ^1.28.3 - version: link:../../../packages/angular-form + version: 1.28.3(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) rxjs: specifier: ^7.8.2 version: 7.8.2 @@ -234,7 +234,7 @@ importers: version: 20.3.6(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.6(@angular/animations@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@tanstack/angular-form': specifier: ^1.28.3 - version: link:../../../packages/angular-form + version: 1.28.3(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) rxjs: specifier: ^7.8.2 version: 7.8.2 @@ -286,7 +286,7 @@ importers: version: 20.3.6(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.6(@angular/animations@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@tanstack/angular-form': specifier: ^1.28.3 - version: link:../../../packages/angular-form + version: 1.28.3(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) effect: specifier: ^3.17.14 version: 3.17.14 @@ -323,7 +323,7 @@ importers: dependencies: '@tanstack/lit-form': specifier: ^1.23.23 - version: link:../../../packages/lit-form + version: 1.23.23(lit@3.3.1) lit: specifier: ^3.3.1 version: 3.3.1 @@ -336,7 +336,7 @@ importers: dependencies: '@tanstack/lit-form': specifier: ^1.23.23 - version: link:../../../packages/lit-form + version: 1.23.23(lit@3.3.1) lit: specifier: ^3.3.1 version: 3.3.1 @@ -349,7 +349,7 @@ importers: dependencies: '@tanstack/lit-form': specifier: ^1.23.23 - version: link:../../../packages/lit-form + version: 1.23.23(lit@3.3.1) arktype: specifier: ^2.1.22 version: 2.1.23 @@ -377,7 +377,7 @@ importers: version: 2.4.0 '@tanstack/lit-form': specifier: ^1.23.23 - version: link:../../../packages/lit-form + version: 1.23.23(lit@3.3.1) lit: specifier: ^3.3.1 version: 3.3.1 @@ -393,10 +393,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) react: specifier: ^19.0.0 version: 19.1.0 @@ -421,7 +421,7 @@ importers: dependencies: '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -455,10 +455,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) react: specifier: ^19.0.0 version: 19.1.0 @@ -486,10 +486,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) react: specifier: ^19.0.0 version: 19.1.0 @@ -520,10 +520,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) react: specifier: ^19.0.0 version: 19.1.0 @@ -551,10 +551,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) react: specifier: ^19.0.0 version: 19.1.0 @@ -579,13 +579,13 @@ importers: dependencies: '@tanstack/react-form-nextjs': specifier: ^1.28.3 - version: link:../../../packages/react-form-nextjs + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-store': specifier: ^0.8.1 version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 16.0.5 - version: 16.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) + version: 16.0.5(@babel/core@7.28.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -610,13 +610,13 @@ importers: dependencies: '@tanstack/react-form-nextjs': specifier: ^1.28.3 - version: link:../../../packages/react-form-nextjs + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-store': specifier: ^0.8.1 version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 16.0.5 - version: 16.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) + version: 16.0.5(@babel/core@7.28.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -647,10 +647,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) '@tanstack/react-query': specifier: ^5.89.0 version: 5.90.5(react@19.1.0) @@ -687,7 +687,7 @@ importers: version: 2.17.1(typescript@5.8.2) '@tanstack/react-form-remix': specifier: ^1.28.3 - version: link:../../../packages/react-form-remix + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-store': specifier: ^0.8.1 version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -727,10 +727,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) react: specifier: ^19.0.0 version: 19.1.0 @@ -758,10 +758,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) arktype: specifier: ^2.1.22 version: 2.1.23 @@ -801,10 +801,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2)) '@tanstack/react-form-start': specifier: ^1.28.3 - version: link:../../../packages/react-form-start + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-router': specifier: ^1.134.9 version: 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -865,10 +865,10 @@ importers: version: 0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9) '@tanstack/react-form': specifier: ^1.28.3 - version: link:../../../packages/react-form + version: 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-form-devtools': specifier: ^0.2.16 - version: link:../../../packages/react-form-devtools + version: 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2)) '@yme/lay-postcss': specifier: 0.1.0 version: 0.1.0(postcss@8.5.6) @@ -911,7 +911,7 @@ importers: dependencies: '@tanstack/solid-form': specifier: ^1.28.3 - version: link:../../../packages/solid-form + version: 1.28.3(solid-js@1.9.9) solid-js: specifier: ^1.9.9 version: 1.9.9 @@ -933,7 +933,7 @@ importers: version: 0.7.7(csstype@3.1.3)(solid-js@1.9.9) '@tanstack/solid-form': specifier: ^1.28.3 - version: link:../../../packages/solid-form + version: 1.28.3(solid-js@1.9.9) '@tanstack/solid-form-devtools': specifier: workspace:* version: link:../../../packages/solid-form-devtools @@ -955,7 +955,7 @@ importers: dependencies: '@tanstack/solid-form': specifier: ^1.28.3 - version: link:../../../packages/solid-form + version: 1.28.3(solid-js@1.9.9) solid-js: specifier: ^1.9.9 version: 1.9.9 @@ -974,7 +974,7 @@ importers: dependencies: '@tanstack/solid-form': specifier: ^1.28.3 - version: link:../../../packages/solid-form + version: 1.28.3(solid-js@1.9.9) solid-js: specifier: ^1.9.9 version: 1.9.9 @@ -993,7 +993,7 @@ importers: dependencies: '@tanstack/solid-form': specifier: ^1.28.3 - version: link:../../../packages/solid-form + version: 1.28.3(solid-js@1.9.9) arktype: specifier: ^2.1.22 version: 2.1.23 @@ -1030,7 +1030,7 @@ importers: dependencies: '@tanstack/svelte-form': specifier: ^1.23.0 - version: link:../../../packages/svelte-form + version: 1.28.3(svelte@5.41.1) devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 @@ -1052,7 +1052,7 @@ importers: dependencies: '@tanstack/svelte-form': specifier: ^1.23.0 - version: link:../../../packages/svelte-form + version: 1.28.3(svelte@5.41.1) devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 @@ -1074,7 +1074,7 @@ importers: dependencies: '@tanstack/svelte-form': specifier: ^1.23.0 - version: link:../../../packages/svelte-form + version: 1.28.3(svelte@5.41.1) devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 @@ -1096,7 +1096,7 @@ importers: dependencies: '@tanstack/svelte-form': specifier: ^1.23.0 - version: link:../../../packages/svelte-form + version: 1.28.3(svelte@5.41.1) arktype: specifier: ^2.1.22 version: 2.1.23 @@ -1130,7 +1130,7 @@ importers: dependencies: '@tanstack/vue-form': specifier: ^1.28.3 - version: link:../../../packages/vue-form + version: 1.28.3(vue@3.5.16(typescript@5.8.2)) vue: specifier: ^3.5.13 version: 3.5.16(typescript@5.8.2) @@ -1152,7 +1152,7 @@ importers: dependencies: '@tanstack/vue-form': specifier: ^1.28.3 - version: link:../../../packages/vue-form + version: 1.28.3(vue@3.5.16(typescript@5.8.2)) vue: specifier: ^3.5.13 version: 3.5.16(typescript@5.8.2) @@ -1174,7 +1174,7 @@ importers: dependencies: '@tanstack/vue-form': specifier: ^1.28.3 - version: link:../../../packages/vue-form + version: 1.28.3(vue@3.5.16(typescript@5.8.2)) arktype: specifier: ^2.1.22 version: 2.1.23 @@ -1224,10 +1224,10 @@ importers: devDependencies: '@analogjs/vite-plugin-angular': specifier: ^1.21.1 - version: 1.21.3(ab51438384ee320509dd464c426c58b0) + version: 1.21.3(6hrchhdlutu5feeonfk5hjvpg4) '@analogjs/vitest-angular': specifier: ^1.21.1 - version: 1.21.3(@analogjs/vite-plugin-angular@1.21.3(ab51438384ee320509dd464c426c58b0))(@angular-devkit/architect@0.2003.6(chokidar@4.0.3))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + version: 1.21.3(@analogjs/vite-plugin-angular@1.21.3(6hrchhdlutu5feeonfk5hjvpg4))(@angular-devkit/architect@0.2003.6(chokidar@4.0.3))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) '@angular/common': specifier: ^20.3.1 version: 20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -1248,7 +1248,7 @@ importers: version: 20.3.6(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.6)(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.6(@angular/animations@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))) '@testing-library/angular': specifier: ^17.4.0 - version: 17.4.0(e09af4985e48ee7e92e2c384a3ed72ed) + version: 17.4.0(p6hqrkr4yijphtsunh45ojkguy) ng-packagr: specifier: ^20.3.0 version: 20.3.0(@angular/compiler-cli@20.3.6(@angular/compiler@20.3.6)(typescript@5.8.2))(tslib@2.8.1)(typescript@5.8.2) @@ -1291,7 +1291,7 @@ importers: version: 0.4.4(csstype@3.1.3)(solid-js@1.9.9) '@tanstack/devtools-utils': specifier: ^0.3.0 - version: 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) + version: 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) '@tanstack/form-core': specifier: workspace:* version: link:../form-core @@ -1325,6 +1325,28 @@ importers: specifier: ^3.3.1 version: 3.3.1 + packages/preact-form: + dependencies: + '@tanstack/form-core': + specifier: workspace:* + version: link:../form-core + '@tanstack/preact-store': + specifier: ^0.10.2 + version: 0.10.2(preact@10.28.3) + devDependencies: + '@preact/preset-vite': + specifier: ^2.10.2 + version: 2.10.3(@babel/core@7.28.5)(preact@10.28.3)(rollup@4.52.5)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + '@testing-library/preact': + specifier: ^3.2.4 + version: 3.2.4(preact@10.28.3) + preact: + specifier: ^10.11.1 + version: 10.28.3 + vite: + specifier: ^7.2.2 + version: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + packages/react-form: dependencies: '@tanstack/form-core': @@ -1360,7 +1382,7 @@ importers: dependencies: '@tanstack/devtools-utils': specifier: ^0.3.0 - version: 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) + version: 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) '@tanstack/form-devtools': specifier: workspace:* version: link:../form-devtools @@ -1497,7 +1519,7 @@ importers: dependencies: '@tanstack/devtools-utils': specifier: ^0.3.0 - version: 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) + version: 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) '@tanstack/form-devtools': specifier: workspace:* version: link:../form-devtools @@ -1869,6 +1891,10 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.0': resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} @@ -1889,6 +1915,10 @@ packages: resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -1930,6 +1960,10 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.3': resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} @@ -1944,6 +1978,10 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.27.1': resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} engines: {node: '>=6.9.0'} @@ -2002,6 +2040,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} @@ -2069,6 +2112,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.27.1': resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} engines: {node: '>=6.9.0'} @@ -2315,6 +2364,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -2327,6 +2382,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-regenerator@7.28.3': resolution: {integrity: sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==} engines: {node: '>=6.9.0'} @@ -2436,6 +2497,10 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.3': resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} engines: {node: '>=6.9.0'} @@ -2444,6 +2509,10 @@ packages: resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.2': resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} @@ -2452,6 +2521,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + '@changesets/apply-release-plan@7.0.14': resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} @@ -4207,6 +4280,29 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@preact/preset-vite@2.10.3': + resolution: {integrity: sha512-1SiS+vFItpkNdBs7q585PSAIln0wBeBdcpJYbzPs1qipsb/FssnkUioNXuRsb8ZnU8YEQHr+3v8+/mzWSnTQmg==} + peerDependencies: + '@babel/core': 7.x + vite: 2.x || 3.x || 4.x || 5.x || 6.x || 7.x + + '@prefresh/babel-plugin@0.5.2': + resolution: {integrity: sha512-AOl4HG6dAxWkJ5ndPHBgBa49oo/9bOiJuRDKHLSTyH+Fd9x00shTXpdiTj1W41l6oQIwUOAgJeHMn4QwIDpHkA==} + + '@prefresh/core@1.5.9': + resolution: {integrity: sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==} + peerDependencies: + preact: ^10.0.0 || ^11.0.0-0 + + '@prefresh/utils@1.2.1': + resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} + + '@prefresh/vite@2.4.11': + resolution: {integrity: sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==} + peerDependencies: + preact: ^10.4.0 || ^11.0.0-0 + vite: '>=2.0.0' + '@publint/pack@0.1.2': resolution: {integrity: sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==} engines: {node: '>=18'} @@ -4313,6 +4409,10 @@ packages: rollup: optional: true + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -4773,6 +4873,11 @@ packages: '@swc/types@0.1.24': resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} + '@tanstack/angular-form@1.28.3': + resolution: {integrity: sha512-EkZJXcudNLHH4BWGEORYnginSQartEuV1C7QLEvW+L8e1W28xPOY3H2LPTW0c8ehOLHXugaXue8X+UO+ITKXSg==} + peerDependencies: + '@angular/core': '>=19.0.0' + '@tanstack/angular-store@0.8.1': resolution: {integrity: sha512-Zb8e1QVeBoSu/s1R3fXczctEqB7lZrdPL87/9INwCaRSY3jPqNn3SlzP8yvwvBwv7axaFgfUrhQJXlnACC3Vnw==} peerDependencies: @@ -4854,14 +4959,32 @@ packages: resolution: {integrity: sha512-2g+PuGR3GuvvCiR3xZs+IMqAvnYU9bvH+jRml0BFBSxHBj22xFCTNvJWhvgj7uICFF9IchDkFUto91xDPMu5cg==} engines: {node: '>=18'} + '@tanstack/form-core@1.28.3': + resolution: {integrity: sha512-DBhnu1d5VfACAYOAZJO8tsEUHjWczZMJY8v/YrtAJNWpwvL/3ogDuz8e6yUB2m/iVTNq6K8yrnVN2nrX0/BX/w==} + + '@tanstack/form-devtools@0.2.16': + resolution: {integrity: sha512-jnCHyeP7Gy0NRGkjJSLQu7/IBpZBknf/5MV5+e24kMzoSs0x4LdhcwVVZwKszNFTBNFdXePPyhOXHand/r9Jkg==} + peerDependencies: + solid-js: '>=1.9.9' + '@tanstack/history@1.133.28': resolution: {integrity: sha512-B7+x7eP2FFvi3fgd3rNH9o/Eixt+pp0zCIdGhnQbAJjFrlwIKGjGnwyJjhWJ5fMQlGks/E2LdDTqEV4W9Plx7g==} engines: {node: '>=12'} + '@tanstack/lit-form@1.23.23': + resolution: {integrity: sha512-H2w++Ka/WNxY8+CdHg1J1zyBKj2Q1OeKjDjqDKeRmxDx9Fpt3gDdHgxA6RunOI9JooT6XHFUXHmStl0sLXLEqQ==} + peerDependencies: + lit: ^3.0.0 + '@tanstack/pacer-lite@0.1.1': resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} + '@tanstack/preact-store@0.10.2': + resolution: {integrity: sha512-fe2XUWlomNczbyMaOk4TtMRfIUq3Pn4S/jgGAS6jYOCMKGjHNrwhdTA4EtGeG86DMxPL7NyObOsNy/6rA4dqCw==} + peerDependencies: + preact: ^10.0.0 + '@tanstack/query-core@5.90.5': resolution: {integrity: sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w==} @@ -4874,6 +4997,47 @@ packages: react: '>=16.8' react-dom: '>=16.8' + '@tanstack/react-form-devtools@0.2.16': + resolution: {integrity: sha512-8zZZoi3XlIjEjDEr1LREmdTyKkYIGgBDgSqx4WVg/VLoqGw/j5mfXwjzbYhPNHd9eHzjmsweOJ9jzozbWKRh4g==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-form-nextjs@1.28.3': + resolution: {integrity: sha512-32gXlrpsoOZ0ZBDf54enLWnbczi71pxHXpEXAvHAUxuYrLB7Tl+BxN0NIRB0DoV688goeHkZRRQ3PT85V0wTZw==} + peerDependencies: + '@tanstack/react-start': ^1.134.9 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + + '@tanstack/react-form-remix@1.28.3': + resolution: {integrity: sha512-TXEMPxfiu3q9UL2+8ZQv7HrWw1OZ+OuvPmeSPkfncMGhpT8l3IspoYyQOZJEMtffN5kBfrijwZaygoPxJvAHXQ==} + peerDependencies: + '@tanstack/react-start': ^1.134.9 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + + '@tanstack/react-form-start@1.28.3': + resolution: {integrity: sha512-fQ2R2JMW66Up4UDpqGHUTGZn3xVofvGU9sDuMOOThfM0Z10krhdzOCV3I0W//mpNZhIvufCIQRbTx6SxZOkfoQ==} + peerDependencies: + '@tanstack/react-start': ^1.134.9 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + + '@tanstack/react-form@1.28.3': + resolution: {integrity: sha512-84yd0swZRcyC3Q46dYBH6bHf1tlIY1flchbdG3VwArg/wLVW5RdBenIrJhleHjk2OxXuF+9HoKQbHglJyWIXQA==} + peerDependencies: + '@tanstack/react-start': '*' + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@tanstack/react-start': + optional: true + '@tanstack/react-query@5.90.5': resolution: {integrity: sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q==} peerDependencies: @@ -4957,6 +5121,11 @@ packages: peerDependencies: solid-js: '>=1.9.7' + '@tanstack/solid-form@1.28.3': + resolution: {integrity: sha512-1SWqeb92EEgxCLUW12EmbVCPHfrdnVLeLtnZcY7HWy/ct9eyTZ4E/hPRZDIKad1E60SuDS09J8uW7Jmv1W552A==} + peerDependencies: + solid-js: '>=1.9.9' + '@tanstack/solid-store@0.8.1': resolution: {integrity: sha512-1p4TTJGIZJ2J7130aTo7oWfHVRSCd9DxLP3HzcDMnzn56pz8krlyBEzsE+z/sHGXP0EC/JjT02fgj2L9+fmf8Q==} peerDependencies: @@ -4983,6 +5152,11 @@ packages: '@tanstack/store@0.8.1': resolution: {integrity: sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==} + '@tanstack/svelte-form@1.28.3': + resolution: {integrity: sha512-95opkSn2N8fYOWi7K1cxSRPh38bVm3/tVZshEVSbGACOUioyqt2RSJpQ0DobdRLcStdP78slyZTXA3GDUhSNIw==} + peerDependencies: + svelte: ^5.0.0 + '@tanstack/svelte-store@0.9.1': resolution: {integrity: sha512-4RYp0CXSB9tjlUZNl29mjraWeRquKzuaW+bGGI4s3kS6BWatgt7BfX4OtoLT8MTBdepW9ARwqHZ3s8YGpfOZkQ==} peerDependencies: @@ -5000,6 +5174,11 @@ packages: resolution: {integrity: sha512-FOl8EF6SAcljanKSm5aBeJaflFcxQAytTbxtNW8HC6D4x+UBW68IC4tBcrlrsI0wXHBmC/Gz4Ovvv8qCtiXSgQ==} engines: {node: '>=18'} + '@tanstack/vue-form@1.28.3': + resolution: {integrity: sha512-WHs9oBmMLvp5RIqa33mSLYgEM9nbG6UlwXEXWyG8S25gn8gOgcaLpPoE7kvqlT+nyPbZhPAjJXBsn7Uzsd8xvA==} + peerDependencies: + vue: ^3.4.0 + '@tanstack/vue-store@0.8.1': resolution: {integrity: sha512-37rNptEo86+2Jm2kTLvgVtboRRwHkksjxCKCrdl73eeZIU0jU34ZMP/ayd5+bCCo6epdbrqcb13gjUBSGp4Blg==} peerDependencies: @@ -5023,6 +5202,10 @@ packages: resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + '@testing-library/dom@9.3.4': resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} engines: {node: '>=14'} @@ -5031,6 +5214,12 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/preact@3.2.4': + resolution: {integrity: sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==} + engines: {node: '>= 12'} + peerDependencies: + preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0' + '@testing-library/react@16.3.0': resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} engines: {node: '>=18'} @@ -5889,6 +6078,11 @@ packages: babel-plugin-react-compiler@19.1.0-rc.3: resolution: {integrity: sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==} + babel-plugin-transform-hook-names@1.0.2: + resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} + peerDependencies: + '@babel/core': ^7.12.10 + babel-preset-solid@1.9.6: resolution: {integrity: sha512-HXTK9f93QxoH8dYn1M2mJdOlWgMsR88Lg/ul6QCZGkNTktjTE5HAf93YxQumHoCudLEtZrU1cFCMFOVho6GqFg==} peerDependencies: @@ -8493,6 +8687,9 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} hasBin: true + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} @@ -8970,6 +9167,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact@10.28.3: + resolution: {integrity: sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9589,6 +9789,9 @@ packages: resolution: {integrity: sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==} engines: {node: ^18.17.0 || >=20.5.0} + simple-code-frame@1.3.0: + resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -9701,6 +9904,10 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stack-trace@1.0.0-pre2: + resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} + engines: {node: '>=16'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -10368,6 +10575,11 @@ packages: '@testing-library/jest-dom': optional: true + vite-prerender-plugin@0.5.12: + resolution: {integrity: sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==} + peerDependencies: + vite: 5.x || 6.x || 7.x + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -10940,7 +11152,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 - '@analogjs/vite-plugin-angular@1.21.3(ab51438384ee320509dd464c426c58b0)': + '@analogjs/vite-plugin-angular@1.21.3(6hrchhdlutu5feeonfk5hjvpg4)': dependencies: ts-morph: 21.0.1 vfile: 6.0.3 @@ -10948,9 +11160,9 @@ snapshots: '@angular-devkit/build-angular': 20.3.6(@angular/compiler-cli@20.3.6(@angular/compiler@20.3.6)(typescript@5.8.2))(@angular/compiler@20.3.6)(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.6(@angular/animations@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@swc/core@1.13.5)(@types/node@24.1.0)(chokidar@4.0.3)(jiti@2.6.1)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.6(@angular/compiler@20.3.6)(typescript@5.8.2))(tslib@2.8.1)(typescript@5.8.2))(sugarss@5.0.1(postcss@8.5.6))(tsx@4.19.4)(typescript@5.8.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(yaml@2.8.1) '@angular/build': 20.3.6(@angular/compiler-cli@20.3.6(@angular/compiler@20.3.6)(typescript@5.8.2))(@angular/compiler@20.3.6)(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.6(@angular/animations@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.1.0)(chokidar@4.0.3)(jiti@2.6.1)(less@4.4.0)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.6(@angular/compiler@20.3.6)(typescript@5.8.2))(tslib@2.8.1)(typescript@5.8.2))(postcss@8.5.6)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tslib@2.8.1)(tsx@4.19.4)(typescript@5.8.2)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(yaml@2.8.1) - '@analogjs/vitest-angular@1.21.3(@analogjs/vite-plugin-angular@1.21.3(ab51438384ee320509dd464c426c58b0))(@angular-devkit/architect@0.2003.6(chokidar@4.0.3))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': + '@analogjs/vitest-angular@1.21.3(@analogjs/vite-plugin-angular@1.21.3(6hrchhdlutu5feeonfk5hjvpg4))(@angular-devkit/architect@0.2003.6(chokidar@4.0.3))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': dependencies: - '@analogjs/vite-plugin-angular': 1.21.3(ab51438384ee320509dd464c426c58b0) + '@analogjs/vite-plugin-angular': 1.21.3(6hrchhdlutu5feeonfk5hjvpg4) '@angular-devkit/architect': 0.2003.6(chokidar@4.0.3) vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) @@ -11266,6 +11478,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.0': {} '@babel/core@7.28.3': @@ -11324,6 +11542,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.2 @@ -11387,6 +11613,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -11411,6 +11644,8 @@ snapshots: '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -11474,6 +11709,10 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -11541,6 +11780,11 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -11818,6 +12062,13 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -11828,6 +12079,17 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.5) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/plugin-transform-regenerator@7.28.3(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -12020,6 +12282,12 @@ snapshots: '@babel/parser': 7.28.3 '@babel/types': 7.28.2 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@babel/traverse@7.28.3': dependencies: '@babel/code-frame': 7.27.1 @@ -12044,6 +12312,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -12054,6 +12334,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@changesets/apply-release-plan@7.0.14': dependencies: '@changesets/config': 3.1.2 @@ -13682,6 +13967,43 @@ snapshots: '@popperjs/core@2.11.8': {} + '@preact/preset-vite@2.10.3(@babel/core@7.28.5)(preact@10.28.3)(rollup@4.52.5)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5) + '@prefresh/vite': 2.4.11(preact@10.28.3)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + '@rollup/pluginutils': 5.1.4(rollup@4.52.5) + babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5) + debug: 4.4.3 + picocolors: 1.1.1 + vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + vite-prerender-plugin: 0.5.12(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + transitivePeerDependencies: + - preact + - rollup + - supports-color + + '@prefresh/babel-plugin@0.5.2': {} + + '@prefresh/core@1.5.9(preact@10.28.3)': + dependencies: + preact: 10.28.3 + + '@prefresh/utils@1.2.1': {} + + '@prefresh/vite@2.4.11(preact@10.28.3)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))': + dependencies: + '@babel/core': 7.28.5 + '@prefresh/babel-plugin': 0.5.2 + '@prefresh/core': 1.5.9(preact@10.28.3) + '@prefresh/utils': 1.2.1 + '@rollup/pluginutils': 4.2.1 + preact: 10.28.3 + vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@publint/pack@0.1.2': {} '@remix-run/dev@2.17.1(@remix-run/react@2.17.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.2))(@remix-run/serve@2.17.1(typescript@5.8.2))(@types/node@24.1.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(typescript@5.8.2)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(yaml@2.8.1)': @@ -13865,6 +14187,11 @@ snapshots: optionalDependencies: rollup: 4.52.5 + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + '@rollup/pluginutils@5.1.4(rollup@4.52.5)': dependencies: '@types/estree': 1.0.8 @@ -14258,6 +14585,15 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@tanstack/angular-form@1.28.3(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))': + dependencies: + '@angular/core': 20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1) + '@tanstack/angular-store': 0.8.1(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) + '@tanstack/form-core': 1.28.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@angular/common' + '@tanstack/angular-store@0.8.1(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))': dependencies: '@angular/common': 20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -14303,11 +14639,24 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-utils@0.3.0(@types/react@19.1.6)(csstype@3.1.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3))': + '@tanstack/devtools-utils@0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2))': + dependencies: + '@tanstack/devtools-ui': 0.4.4(csstype@3.1.3)(solid-js@1.9.9) + optionalDependencies: + '@types/react': 19.1.6 + preact: 10.28.3 + react: 19.1.0 + solid-js: 1.9.9 + vue: 3.5.16(typescript@5.8.2) + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-utils@0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3))': dependencies: '@tanstack/devtools-ui': 0.4.4(csstype@3.1.3)(solid-js@1.9.9) optionalDependencies: '@types/react': 19.1.6 + preact: 10.28.3 react: 19.1.0 solid-js: 1.9.9 vue: 3.5.16(typescript@5.9.3) @@ -14376,10 +14725,58 @@ snapshots: - supports-color - typescript + '@tanstack/form-core@1.28.3': + dependencies: + '@tanstack/devtools-event-client': 0.4.0 + '@tanstack/pacer-lite': 0.1.1 + '@tanstack/store': 0.8.1 + + '@tanstack/form-devtools@0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2))': + dependencies: + '@tanstack/devtools-ui': 0.4.4(csstype@3.1.3)(solid-js@1.9.9) + '@tanstack/devtools-utils': 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2)) + '@tanstack/form-core': 1.28.3 + clsx: 2.1.1 + dayjs: 1.11.18 + goober: 2.1.18(csstype@3.1.3) + solid-js: 1.9.9 + transitivePeerDependencies: + - '@types/react' + - csstype + - preact + - react + - vue + + '@tanstack/form-devtools@0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3))': + dependencies: + '@tanstack/devtools-ui': 0.4.4(csstype@3.1.3)(solid-js@1.9.9) + '@tanstack/devtools-utils': 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) + '@tanstack/form-core': 1.28.3 + clsx: 2.1.1 + dayjs: 1.11.18 + goober: 2.1.18(csstype@3.1.3) + solid-js: 1.9.9 + transitivePeerDependencies: + - '@types/react' + - csstype + - preact + - react + - vue + '@tanstack/history@1.133.28': {} + '@tanstack/lit-form@1.23.23(lit@3.3.1)': + dependencies: + '@tanstack/form-core': 1.28.3 + lit: 3.3.1 + '@tanstack/pacer-lite@0.1.1': {} + '@tanstack/preact-store@0.10.2(preact@10.28.3)': + dependencies: + '@tanstack/store': 0.8.1 + preact: 10.28.3 + '@tanstack/query-core@5.90.5': {} '@tanstack/react-devtools@0.7.8(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.9)': @@ -14395,6 +14792,81 @@ snapshots: - solid-js - utf-8-validate + '@tanstack/react-form-devtools@0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2))': + dependencies: + '@tanstack/devtools-utils': 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2)) + '@tanstack/form-devtools': 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.8.2)) + react: 19.1.0 + transitivePeerDependencies: + - '@types/react' + - csstype + - preact + - solid-js + - vue + + '@tanstack/react-form-devtools@0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3))': + dependencies: + '@tanstack/devtools-utils': 0.3.0(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) + '@tanstack/form-devtools': 0.2.16(@types/react@19.1.6)(csstype@3.1.3)(preact@10.28.3)(react@19.1.0)(solid-js@1.9.9)(vue@3.5.16(typescript@5.9.3)) + react: 19.1.0 + transitivePeerDependencies: + - '@types/react' + - csstype + - preact + - solid-js + - vue + + '@tanstack/react-form-nextjs@1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/react-form': 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + decode-formdata: 0.9.0 + react: 19.1.0 + optionalDependencies: + '@tanstack/react-start': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)) + transitivePeerDependencies: + - react-dom + + '@tanstack/react-form-remix@1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/react-form': 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + decode-formdata: 0.9.0 + react: 19.1.0 + optionalDependencies: + '@tanstack/react-start': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)) + transitivePeerDependencies: + - react-dom + + '@tanstack/react-form-start@1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/react-form': 1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + decode-formdata: 0.9.0 + devalue: 5.4.2 + react: 19.1.0 + optionalDependencies: + '@tanstack/react-start': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)) + transitivePeerDependencies: + - react-dom + + '@tanstack/react-form@1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/form-core': 1.28.3 + '@tanstack/react-store': 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@tanstack/react-start': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)) + transitivePeerDependencies: + - react-dom + + '@tanstack/react-form@1.28.3(@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/form-core': 1.28.3 + '@tanstack/react-store': 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@tanstack/react-start': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)) + transitivePeerDependencies: + - react-dom + '@tanstack/react-query@5.90.5(react@19.1.0)': dependencies: '@tanstack/query-core': 5.90.5 @@ -14433,6 +14905,27 @@ snapshots: transitivePeerDependencies: - crossws + '@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6))': + dependencies: + '@tanstack/react-router': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-start-client': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-start-server': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/router-utils': 1.133.19 + '@tanstack/start-client-core': 1.135.2 + '@tanstack/start-plugin-core': 1.135.2(@tanstack/react-router@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)) + '@tanstack/start-server-core': 1.135.2 + pathe: 2.0.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + transitivePeerDependencies: + - '@rsbuild/core' + - crossws + - supports-color + - vite-plugin-solid + - webpack + optional: true + '@tanstack/react-start@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5))': dependencies: '@tanstack/react-router': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -14483,6 +14976,31 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/router-plugin@1.135.2(@tanstack/react-router@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6))': + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + '@tanstack/router-core': 1.135.2 + '@tanstack/router-generator': 1.135.2 + '@tanstack/router-utils': 1.133.19 + '@tanstack/virtual-file-routes': 1.133.19 + babel-dead-code-elimination: 1.0.10 + chokidar: 3.6.0 + unplugin: 2.3.10 + zod: 3.25.76 + optionalDependencies: + '@tanstack/react-router': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + webpack: 5.101.2(@swc/core@1.13.5)(esbuild@0.17.6) + transitivePeerDependencies: + - supports-color + optional: true + '@tanstack/router-plugin@1.135.2(@tanstack/react-router@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5))': dependencies: '@babel/core': 7.28.3 @@ -14545,6 +15063,12 @@ snapshots: - csstype - utf-8-validate + '@tanstack/solid-form@1.28.3(solid-js@1.9.9)': + dependencies: + '@tanstack/form-core': 1.28.3 + '@tanstack/solid-store': 0.8.1(solid-js@1.9.9) + solid-js: 1.9.9 + '@tanstack/solid-store@0.8.1(solid-js@1.9.9)': dependencies: '@tanstack/store': 0.8.1 @@ -14558,6 +15082,39 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 + '@tanstack/start-plugin-core@1.135.2(@tanstack/react-router@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6))': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/core': 7.28.3 + '@babel/types': 7.28.2 + '@rolldown/pluginutils': 1.0.0-beta.40 + '@tanstack/router-core': 1.135.2 + '@tanstack/router-generator': 1.135.2 + '@tanstack/router-plugin': 1.135.2(@tanstack/react-router@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)) + '@tanstack/router-utils': 1.133.19 + '@tanstack/server-functions-plugin': 1.134.5(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + '@tanstack/start-client-core': 1.135.2 + '@tanstack/start-server-core': 1.135.2 + babel-dead-code-elimination: 1.0.10 + cheerio: 1.0.0 + exsolve: 1.0.7 + pathe: 2.0.3 + srvx: 0.8.16 + tinyglobby: 0.2.15 + ufo: 1.6.1 + vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) + xmlbuilder2: 3.1.1 + zod: 3.25.76 + transitivePeerDependencies: + - '@rsbuild/core' + - '@tanstack/react-router' + - crossws + - supports-color + - vite-plugin-solid + - webpack + optional: true + '@tanstack/start-plugin-core@1.135.2(@tanstack/react-router@1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)))(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1))(webpack@5.101.2(@swc/core@1.13.5))': dependencies: '@babel/code-frame': 7.26.2 @@ -14608,6 +15165,12 @@ snapshots: '@tanstack/store@0.8.1': {} + '@tanstack/svelte-form@1.28.3(svelte@5.41.1)': + dependencies: + '@tanstack/form-core': 1.28.3 + '@tanstack/svelte-store': 0.9.1(svelte@5.41.1) + svelte: 5.41.1 + '@tanstack/svelte-store@0.9.1(svelte@5.41.1)': dependencies: '@tanstack/store': 0.8.1 @@ -14636,13 +15199,27 @@ snapshots: - typescript - vite + '@tanstack/vue-form@1.28.3(vue@3.5.16(typescript@5.8.2))': + dependencies: + '@tanstack/form-core': 1.28.3 + '@tanstack/vue-store': 0.8.1(vue@3.5.16(typescript@5.8.2)) + vue: 3.5.16(typescript@5.8.2) + transitivePeerDependencies: + - '@vue/composition-api' + + '@tanstack/vue-store@0.8.1(vue@3.5.16(typescript@5.8.2))': + dependencies: + '@tanstack/store': 0.8.1 + vue: 3.5.16(typescript@5.8.2) + vue-demi: 0.14.10(vue@3.5.16(typescript@5.8.2)) + '@tanstack/vue-store@0.8.1(vue@3.5.16(typescript@5.9.3))': dependencies: '@tanstack/store': 0.8.1 vue: 3.5.16(typescript@5.9.3) vue-demi: 0.14.10(vue@3.5.16(typescript@5.9.3)) - '@testing-library/angular@17.4.0(e09af4985e48ee7e92e2c384a3ed72ed)': + '@testing-library/angular@17.4.0(p6hqrkr4yijphtsunh45ojkguy)': dependencies: '@angular/animations': 20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) '@angular/common': 20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -14663,6 +15240,17 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.3 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/dom@9.3.4': dependencies: '@babel/code-frame': 7.27.1 @@ -14683,6 +15271,11 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@testing-library/preact@3.2.4(preact@10.28.3)': + dependencies: + '@testing-library/dom': 8.20.1 + preact: 10.28.3 + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.28.3 @@ -15736,6 +16329,10 @@ snapshots: dependencies: '@babel/types': 7.28.2 + babel-plugin-transform-hook-names@1.0.2(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + babel-preset-solid@1.9.6(@babel/core@7.28.3): dependencies: '@babel/core': 7.28.3 @@ -18704,7 +19301,7 @@ snapshots: neo-async@2.6.2: {} - next@16.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0): + next@16.0.5(@babel/core@7.28.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0): dependencies: '@next/env': 16.0.5 '@swc/helpers': 0.5.15 @@ -18712,7 +19309,7 @@ snapshots: postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - styled-jsx: 5.1.6(react@19.1.0) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.1.0) optionalDependencies: '@next/swc-darwin-arm64': 16.0.5 '@next/swc-darwin-x64': 16.0.5 @@ -18796,6 +19393,11 @@ snapshots: transitivePeerDependencies: - supports-color + node-html-parser@6.1.13: + dependencies: + css-select: 5.1.0 + he: 1.2.0 + node-machine-id@1.1.12: {} node-releases@2.0.19: {} @@ -19388,6 +19990,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.28.3: {} + prelude-ls@1.2.1: {} premove@4.0.0: {} @@ -20127,6 +20731,10 @@ snapshots: transitivePeerDependencies: - supports-color + simple-code-frame@1.3.0: + dependencies: + kolorist: 1.8.0 + slash@3.0.0: {} slice-ansi@5.0.0: @@ -20254,6 +20862,8 @@ snapshots: stable-hash-x@0.2.0: {} + stack-trace@1.0.0-pre2: {} + stackback@0.0.2: {} statuses@1.5.0: {} @@ -20338,10 +20948,12 @@ snapshots: dependencies: inline-style-parser: 0.1.1 - styled-jsx@5.1.6(react@19.1.0): + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.1.0): dependencies: client-only: 0.0.1 react: 19.1.0 + optionalDependencies: + '@babel/core': 7.28.5 stylis@4.2.0: {} @@ -20440,6 +21052,19 @@ snapshots: term-size@2.2.1: {} + terser-webpack-plugin@5.3.14(@swc/core@1.13.5)(esbuild@0.17.6)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.1 + webpack: 5.101.2(@swc/core@1.13.5)(esbuild@0.17.6) + optionalDependencies: + '@swc/core': 1.13.5 + esbuild: 0.17.6 + optional: true + terser-webpack-plugin@5.3.14(@swc/core@1.13.5)(esbuild@0.25.9)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9)): dependencies: '@jridgewell/trace-mapping': 0.3.29 @@ -20952,6 +21577,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite-prerender-plugin@0.5.12(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)): + dependencies: + kolorist: 1.8.0 + magic-string: 0.30.19 + node-html-parser: 6.1.13 + simple-code-frame: 1.3.0 + source-map: 0.7.6 + stack-trace: 1.0.0-pre2 + vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)): dependencies: debug: 4.4.3 @@ -21065,6 +21700,10 @@ snapshots: vue-component-type-helpers@2.2.12: {} + vue-demi@0.14.10(vue@3.5.16(typescript@5.8.2)): + dependencies: + vue: 3.5.16(typescript@5.8.2) + vue-demi@0.14.10(vue@3.5.16(typescript@5.9.3)): dependencies: vue: 3.5.16(typescript@5.9.3) @@ -21205,6 +21844,39 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.25.4 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.1 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(@swc/core@1.13.5)(esbuild@0.17.6)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.17.6)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + optional: true + webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9): dependencies: '@types/eslint-scope': 3.7.7 From 3568d4f97f4f19a528516eed734170714d2b4c2f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:55:24 +0000 Subject: [PATCH 2/2] ci: apply automated fixes and generate docs --- packages/preact-form/eslint.config.js | 4 +-- packages/preact-form/src/createFormHook.tsx | 7 ++++- packages/preact-form/src/useForm.tsx | 6 ++++- packages/preact-form/tests/useField.test.tsx | 28 +++++++++++++++----- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/preact-form/eslint.config.js b/packages/preact-form/eslint.config.js index 6f18a0590..c7c2a6838 100644 --- a/packages/preact-form/eslint.config.js +++ b/packages/preact-form/eslint.config.js @@ -12,6 +12,6 @@ export default [ { rules: { '@eslint-react/no-use-context': 'off', - } - } + }, + }, ] diff --git a/packages/preact-form/src/createFormHook.tsx b/packages/preact-form/src/createFormHook.tsx index 720e17605..3abd1d333 100644 --- a/packages/preact-form/src/createFormHook.tsx +++ b/packages/preact-form/src/createFormHook.tsx @@ -129,7 +129,12 @@ export function createFormHookContexts() { > } - return { fieldContext: FieldContext, useFieldContext, useFormContext, formContext: FormContext } + return { + fieldContext: FieldContext, + useFieldContext, + useFormContext, + formContext: FormContext, + } } interface CreateFormHookProps< diff --git a/packages/preact-form/src/useForm.tsx b/packages/preact-form/src/useForm.tsx index 633460f08..2a53b3868 100644 --- a/packages/preact-form/src/useForm.tsx +++ b/packages/preact-form/src/useForm.tsx @@ -14,7 +14,11 @@ import type { FormState, FormValidateOrFn, } from '@tanstack/form-core' -import type { FunctionComponent, PropsWithChildren, ReactNode } from 'preact/compat' +import type { + FunctionComponent, + PropsWithChildren, + ReactNode, +} from 'preact/compat' import type { FieldComponent } from './useField' import type { NoInfer } from '@tanstack/preact-store' diff --git a/packages/preact-form/tests/useField.test.tsx b/packages/preact-form/tests/useField.test.tsx index 460b76404..dfaa3acf0 100644 --- a/packages/preact-form/tests/useField.test.tsx +++ b/packages/preact-form/tests/useField.test.tsx @@ -549,7 +549,9 @@ describe('useField', () => { name={field.name} value={field.state.value} onBlur={field.handleBlur} - onChange={(e) => field.handleChange(e.currentTarget.value)} + onChange={(e) => + field.handleChange(e.currentTarget.value) + } /> @@ -568,7 +570,9 @@ describe('useField', () => { name={field.name} value={field.state.value} onBlur={field.handleBlur} - onChange={(e) => field.handleChange(e.currentTarget.value)} + onChange={(e) => + field.handleChange(e.currentTarget.value) + } /> @@ -638,7 +642,9 @@ describe('useField', () => { - subField.handleChange(e.currentTarget.value) + subField.handleChange( + e.currentTarget.value, + ) } /> @@ -734,7 +740,9 @@ describe('useField', () => { - subField.handleChange(e.currentTarget.value) + subField.handleChange( + e.currentTarget.value, + ) } /> @@ -820,7 +828,9 @@ describe('useField', () => {
Password
field.handleChange(e.currentTarget.value)} + onChange={(e) => + field.handleChange(e.currentTarget.value) + } /> @@ -846,7 +856,9 @@ describe('useField', () => {
Confirm Password
field.handleChange(e.currentTarget.value)} + onChange={(e) => + field.handleChange(e.currentTarget.value) + } /> {field.state.meta.errors.map((err) => { @@ -1259,7 +1271,9 @@ describe('useField', () => { field.handleChange(e.currentTarget.value)} + onChange={(e) => + field.handleChange(e.currentTarget.value) + } /> ) }}