From a174fde4c55e2480953231b9989ceab3f649034f Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:11:13 +0100 Subject: [PATCH 1/7] feat: allow nested field components for react-form --- .../src/fieldGroups/numberRange.tsx | 39 +++++++++++++++++++ examples/react/field-groups/src/index.tsx | 29 ++++++++++++++ .../src/FieldGroup/FieldGroupApi.lib.ts | 28 ++++++++++--- .../src/FieldGroup/FieldGroupApi.public.ts | 8 ++++ .../src/FieldGroup/withFields.lib.ts | 4 +- .../src/FieldGroup/withFields.public.ts | 9 ++++- 6 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 examples/react/field-groups/src/fieldGroups/numberRange.tsx create mode 100644 packages/form-core/src/FieldGroup/FieldGroupApi.public.ts diff --git a/examples/react/field-groups/src/fieldGroups/numberRange.tsx b/examples/react/field-groups/src/fieldGroups/numberRange.tsx new file mode 100644 index 0000000000..041ded895d --- /dev/null +++ b/examples/react/field-groups/src/fieldGroups/numberRange.tsx @@ -0,0 +1,39 @@ +import { defineFieldGroup } from '@tanstack/react-form' +import { LowerBoundField, UpperBoundField } from './fieldBounds' + +const numberRange = defineFieldGroup(({ strict }) => ({ + minimum: strict(), + maximum: strict(), +})) + +interface NumberRangeFieldProps { + fields: typeof numberRange.fields + label: string +} + +function NumberRangeFieldComponent({ fields, label }: NumberRangeFieldProps) { + return ( +
+ {label} + + +
+ ) +} + +export const NumberRangeField = numberRange.bindComponent( + NumberRangeFieldComponent, + 'fields', +) diff --git a/examples/react/field-groups/src/index.tsx b/examples/react/field-groups/src/index.tsx index 2ae25b15ed..2783318d35 100644 --- a/examples/react/field-groups/src/index.tsx +++ b/examples/react/field-groups/src/index.tsx @@ -5,6 +5,7 @@ import ReactDOM from 'react-dom/client' import './index.css' import { DateRangeField } from './fieldGroups/dateRange' import { LowerBoundField, UpperBoundField } from './fieldGroups/fieldBounds' +import { NumberRangeField } from './fieldGroups/numberRange' // The bounds field groups package validation and blur normalization once, then // each form binds the group's virtual names to its own real field paths. @@ -69,6 +70,30 @@ function AgeRangeForm() { ) } +function NestedFieldGroupsForm() { + const form = useForm({ + defaultValues: { + filters: { minBudget: '10', maxBudget: '100' }, + }, + }) + + return ( + <> + + state.values.filters}> + {(filters) =>
{JSON.stringify(filters, null, 2)}
} +
+ + ) +} + // DateRangeField has access to both dates, but it still knows nothing about the // array it is rendered from. The parent form supplies those concrete paths here. function SwappableDateRangesForm() { @@ -129,6 +154,10 @@ function App() {

Age Range Form

+
+

Nested Field Groups

+ +

Swappable Date Ranges Form

diff --git a/packages/form-core/src/FieldGroup/FieldGroupApi.lib.ts b/packages/form-core/src/FieldGroup/FieldGroupApi.lib.ts index 81b3974542..8cdd00abda 100644 --- a/packages/form-core/src/FieldGroup/FieldGroupApi.lib.ts +++ b/packages/form-core/src/FieldGroup/FieldGroupApi.lib.ts @@ -5,11 +5,12 @@ import type { AnyFieldApiOptions } from '../FieldApi/FieldApi.lib' import type { AnyInternalFormApi } from '../FormApi/FormApi.lib' import type { InternalFieldUpdateOptions } from '../types.lib' import type { ReadonlyAtom } from '@tanstack/store' +import type { FieldGroupApi } from './FieldGroupApi.public' export type InternalFieldGroupBindings = Record export interface InternalFieldGroupOptions { - form: AnyInternalFormApi + form: AnyInternalFormApi | InternalFieldGroupApi fieldNames: ReadonlyArray getBindings: () => InternalFieldGroupBindings | undefined } @@ -28,15 +29,21 @@ function getRootSegmentEnd(name: string): number { return Math.min(dotIndex, arrayIndex) } -export class InternalFieldGroupApi { +export class InternalFieldGroupApi implements FieldGroupApi { readonly atom: ReadonlyAtom> readonly form: AnyInternalFormApi readonly fieldNames: ReadonlyArray + private readonly parent?: InternalFieldGroupApi private readonly getBindings: () => InternalFieldGroupBindings | undefined private readonly resolvedNameCache = new Map() constructor(options: InternalFieldGroupOptions) { - this.form = options.form + if (options.form instanceof InternalFieldGroupApi) { + this.parent = options.form + this.form = options.form.form + } else { + this.form = options.form + } this.fieldNames = options.fieldNames this.getBindings = options.getBindings this.atom = createAtom( @@ -58,6 +65,13 @@ export class InternalFieldGroupApi { } _resolveFieldName(fieldName: string): string { + const parentFieldName = this.resolveLocalFieldName(fieldName) + return this.parent + ? this.parent._resolveFieldName(parentFieldName) + : parentFieldName + } + + private resolveLocalFieldName(fieldName: string): string { const bindings = this.getBindings() if (bindings === undefined) return fieldName @@ -83,11 +97,15 @@ export class InternalFieldGroupApi { options: TOptions, mergeOptions: (props: TOptions, overrides: Partial) => TOptions, ): AnyFieldApiOptions { - return transformFieldOptionsFieldNames( + const parentOptions = transformFieldOptionsFieldNames( options, - (fieldName) => this._resolveFieldName(fieldName), + (fieldName) => this.resolveLocalFieldName(fieldName), mergeOptions, ) + + return this.parent + ? this.parent._getFormFieldOptions(parentOptions, mergeOptions) + : parentOptions } getFieldValue = (fieldName: string) => diff --git a/packages/form-core/src/FieldGroup/FieldGroupApi.public.ts b/packages/form-core/src/FieldGroup/FieldGroupApi.public.ts new file mode 100644 index 0000000000..bf8e45f44c --- /dev/null +++ b/packages/form-core/src/FieldGroup/FieldGroupApi.public.ts @@ -0,0 +1,8 @@ +import type { ReadonlyAtom } from '@tanstack/store' +import type { FormApiArrayMethods } from '../FormApi/FormApiArrayMethods.types.public' +import type { FormApiFieldMethods } from '../FormApi/FormApiFieldMethods.types.public' + +export interface FieldGroupApi + extends FormApiFieldMethods, FormApiArrayMethods { + atom: ReadonlyAtom +} diff --git a/packages/react-form/src/FieldGroup/withFields.lib.ts b/packages/react-form/src/FieldGroup/withFields.lib.ts index a4673ac72b..9bfb53b743 100644 --- a/packages/react-form/src/FieldGroup/withFields.lib.ts +++ b/packages/react-form/src/FieldGroup/withFields.lib.ts @@ -7,11 +7,12 @@ import { import type { InternalFieldGroupBindings } from '@tanstack/form-core/internals' import type { FunctionComponent } from 'react' import type { AnyFieldGroupApi } from './FieldGroupApi.public' +import type { InternalReactFormApi } from '../ReactForm/ReactFormApi.lib' function attachReactFieldGroupComponents( group: InternalFieldGroupApi, - form: any, ): AnyFieldGroupApi { + const form = group.form as InternalReactFormApi const Field: FunctionComponent = (props) => { const FormField = form.Field return React.createElement( @@ -92,7 +93,6 @@ function withFieldsRuntime( fieldNames, getBindings: () => bindingsRef.current, }), - form, ), [form], ) diff --git a/packages/react-form/src/FieldGroup/withFields.public.ts b/packages/react-form/src/FieldGroup/withFields.public.ts index 3f5c7e7268..08767910f8 100644 --- a/packages/react-form/src/FieldGroup/withFields.public.ts +++ b/packages/react-form/src/FieldGroup/withFields.public.ts @@ -105,9 +105,14 @@ export type FieldGroupWithFieldsFn< >( Component: (props: TProps) => ReactNode, fieldsPropName: TFieldsPropName, -) => ( +) => < + TFormData, + TParentFieldComponents extends FieldGroupFieldComponentsOf, +>( props: Omit & { - form: FieldGroupForm, TFormData> + form: + | FieldGroupForm + | FieldGroupApi } & FieldGroupFieldBindingsProps< FieldGroupFieldsOf, TFormData, From f177e4a6fe10b83840b2db4aa020f908b6dad63f Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:51:19 +0100 Subject: [PATCH 2/7] feat: allow nested field groups in Preact --- .../src/fieldGroups/numberRange.tsx | 39 +++++++++++++++++++ examples/preact/field-groups/src/index.tsx | 29 ++++++++++++++ .../src/FieldGroup/withFields.lib.ts | 4 +- .../src/FieldGroup/withFields.public.ts | 9 ++++- 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 examples/preact/field-groups/src/fieldGroups/numberRange.tsx diff --git a/examples/preact/field-groups/src/fieldGroups/numberRange.tsx b/examples/preact/field-groups/src/fieldGroups/numberRange.tsx new file mode 100644 index 0000000000..532d1a713a --- /dev/null +++ b/examples/preact/field-groups/src/fieldGroups/numberRange.tsx @@ -0,0 +1,39 @@ +import { defineFieldGroup } from '@tanstack/preact-form' +import { LowerBoundField, UpperBoundField } from './fieldBounds' + +const numberRange = defineFieldGroup(({ strict }) => ({ + minimum: strict(), + maximum: strict(), +})) + +interface NumberRangeFieldProps { + fields: typeof numberRange.fields + label: string +} + +function NumberRangeFieldComponent({ fields, label }: NumberRangeFieldProps) { + return ( +
+ {label} + + +
+ ) +} + +export const NumberRangeField = numberRange.bindComponent( + NumberRangeFieldComponent, + 'fields', +) diff --git a/examples/preact/field-groups/src/index.tsx b/examples/preact/field-groups/src/index.tsx index 0cc792a5eb..a81664429c 100644 --- a/examples/preact/field-groups/src/index.tsx +++ b/examples/preact/field-groups/src/index.tsx @@ -3,6 +3,7 @@ import { render } from 'preact' import './index.css' import { DateRangeField } from './fieldGroups/dateRange' import { LowerBoundField, UpperBoundField } from './fieldGroups/fieldBounds' +import { NumberRangeField } from './fieldGroups/numberRange' // The bounds field groups package validation and blur normalization once, then // each form binds the group's virtual names to its own real field paths. @@ -67,6 +68,30 @@ function AgeRangeForm() { ) } +function NestedFieldGroupsForm() { + const form = useForm({ + defaultValues: { + filters: { minBudget: '10', maxBudget: '100' }, + }, + }) + + return ( + <> + + state.values.filters}> + {(filters) =>
{JSON.stringify(filters, null, 2)}
} +
+ + ) +} + // DateRangeField has access to both dates, but it still knows nothing about the // array it is rendered from. The parent form supplies those concrete paths here. function SwappableDateRangesForm() { @@ -127,6 +152,10 @@ function App() {

Age Range Form

+
+

Nested Field Groups

+ +

Swappable Date Ranges Form

diff --git a/packages/preact-form/src/FieldGroup/withFields.lib.ts b/packages/preact-form/src/FieldGroup/withFields.lib.ts index b7d39d7e53..ce6d9f0187 100644 --- a/packages/preact-form/src/FieldGroup/withFields.lib.ts +++ b/packages/preact-form/src/FieldGroup/withFields.lib.ts @@ -7,11 +7,12 @@ import { import type { InternalFieldGroupBindings } from '@tanstack/form-core/internals' import type { FunctionComponent } from 'preact/compat' import type { AnyFieldGroupApi } from './FieldGroupApi.public' +import type { InternalPreactFormApi } from '../PreactForm/PreactFormApi.lib' function attachPreactFieldGroupComponents( group: InternalFieldGroupApi, - form: any, ): AnyFieldGroupApi { + const form = group.form as InternalPreactFormApi const Field: FunctionComponent = (props) => { const FormField = form.Field return Preact.createElement( @@ -92,7 +93,6 @@ function withFieldsRuntime( fieldNames, getBindings: () => bindingsRef.current, }), - form, ), [form], ) diff --git a/packages/preact-form/src/FieldGroup/withFields.public.ts b/packages/preact-form/src/FieldGroup/withFields.public.ts index dc6430bbec..787494e9ba 100644 --- a/packages/preact-form/src/FieldGroup/withFields.public.ts +++ b/packages/preact-form/src/FieldGroup/withFields.public.ts @@ -58,9 +58,14 @@ export type FieldGroupWithFieldsFn< >( Component: (props: TProps) => CrossVersionPreactNode, fieldsPropName: TFieldsPropName, -) => ( +) => < + TFormData, + TParentFieldComponents extends FieldGroupFieldComponentsOf, +>( props: Omit & { - form: FieldGroupForm, TFormData> + form: + | FieldGroupForm + | FieldGroupApi } & FieldGroupFieldBindingsProps< FieldGroupFieldsOf, TFormData, From 752559c4f165fed44fa665834e491b1a59c75186 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:26:47 +0100 Subject: [PATCH 3/7] feat: allow nestable field groups in solid --- .../src/fieldGroups/numberRange.tsx | 35 +++++++++++++++++++ examples/solid/field-groups/src/index.tsx | 29 +++++++++++++++ .../src/FieldGroup/FieldGroupApi.public.ts | 12 +++---- .../src/FieldGroup/withFields.lib.ts | 4 +-- .../src/FieldGroup/withFields.public.ts | 9 +++-- 5 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 examples/solid/field-groups/src/fieldGroups/numberRange.tsx diff --git a/examples/solid/field-groups/src/fieldGroups/numberRange.tsx b/examples/solid/field-groups/src/fieldGroups/numberRange.tsx new file mode 100644 index 0000000000..6517ec4ed0 --- /dev/null +++ b/examples/solid/field-groups/src/fieldGroups/numberRange.tsx @@ -0,0 +1,35 @@ +import { defineFieldGroup } from '@tanstack/solid-form' +import { LowerBoundField, UpperBoundField } from './fieldBounds' + +const numberRange = defineFieldGroup(({ strict }) => ({ + minimum: strict(), + maximum: strict(), +})) + +interface NumberRangeFieldProps { + fields: typeof numberRange.fields + label: string +} + +function NumberRangeFieldComponent(props: NumberRangeFieldProps) { + return ( +
+ {props.label} + + +
+ ) +} + +export const NumberRangeField = numberRange.bindComponent( + NumberRangeFieldComponent, + 'fields', +) diff --git a/examples/solid/field-groups/src/index.tsx b/examples/solid/field-groups/src/index.tsx index cd2791fe02..868a12e3b7 100644 --- a/examples/solid/field-groups/src/index.tsx +++ b/examples/solid/field-groups/src/index.tsx @@ -3,6 +3,7 @@ import { render } from 'solid-js/web' import { createForm } from '@tanstack/solid-form' import { DateRangeField } from './fieldGroups/dateRange' import { LowerBoundField, UpperBoundField } from './fieldGroups/fieldBounds' +import { NumberRangeField } from './fieldGroups/numberRange' import './index.css' function PricingFilterForm() { @@ -59,6 +60,30 @@ function AgeRangeForm() { ) } +function NestedFieldGroupsForm() { + const form = createForm(() => ({ + defaultValues: { + filters: { minBudget: '10', maxBudget: '100' }, + }, + })) + + return ( + <> + + state.values.filters}> + {(filters) =>
{JSON.stringify(filters(), null, 2)}
} +
+ + ) +} + function SwappableDateRangesForm() { const form = createForm(() => ({ defaultValues: { @@ -109,6 +134,10 @@ function App() {

Age Range Form

+
+

Nested Field Groups

+ +

Swappable Date Ranges Form

diff --git a/packages/solid-form/src/FieldGroup/FieldGroupApi.public.ts b/packages/solid-form/src/FieldGroup/FieldGroupApi.public.ts index 363277ec4f..43faa2ba1f 100644 --- a/packages/solid-form/src/FieldGroup/FieldGroupApi.public.ts +++ b/packages/solid-form/src/FieldGroup/FieldGroupApi.public.ts @@ -17,8 +17,8 @@ import type { import type { ReadonlyAtom } from '@tanstack/solid-store' export interface FieldGroupFieldComponent< - TFieldData, - TFieldComponents extends Record>, + in out TFieldData, + in out TFieldComponents extends Record>, > { >( props: SolidFormFieldProps< @@ -39,8 +39,8 @@ export interface FieldGroupFieldComponent< } export interface FieldGroupArrayFieldComponent< - TFieldData, - TFieldComponents extends Record>, + in out TFieldData, + in out TFieldComponents extends Record>, > { >>( props: SolidFormFieldProps< @@ -71,8 +71,8 @@ export type FieldGroupSubscribeComponent = ( ) => JSX.Element export interface FieldGroupApi< - TFieldData, - TFieldComponents extends Record> = Record< + in out TFieldData, + in out TFieldComponents extends Record> = Record< never, never >, diff --git a/packages/solid-form/src/FieldGroup/withFields.lib.ts b/packages/solid-form/src/FieldGroup/withFields.lib.ts index 712814dcfe..565f3a238a 100644 --- a/packages/solid-form/src/FieldGroup/withFields.lib.ts +++ b/packages/solid-form/src/FieldGroup/withFields.lib.ts @@ -6,11 +6,12 @@ import { import { createComponent, mergeProps, splitProps } from 'solid-js' import type { Component } from 'solid-js' import type { AnyFieldGroupApi } from './FieldGroupApi.public' +import type { InternalSolidFormApi } from '../SolidFormApi.lib' function attachSolidFieldGroupComponents( group: InternalFieldGroupApi, - form: any, ): AnyFieldGroupApi { + const form = group.form as InternalSolidFormApi const Field: Component = (props) => createComponent( form.Field as Component, @@ -65,7 +66,6 @@ function withFieldsRuntime( fieldNames, getBindings: () => localProps[fieldsPropName], }), - form, ) return createComponent( diff --git a/packages/solid-form/src/FieldGroup/withFields.public.ts b/packages/solid-form/src/FieldGroup/withFields.public.ts index 81b50147f6..61c5197ab6 100644 --- a/packages/solid-form/src/FieldGroup/withFields.public.ts +++ b/packages/solid-form/src/FieldGroup/withFields.public.ts @@ -54,9 +54,14 @@ export type FieldGroupWithFieldsFn< >( Component: (props: TProps) => JSX.Element, fieldsPropName: TFieldsPropName, -) => ( +) => < + TFormData, + TParentFieldComponents extends FieldGroupFieldComponentsOf, +>( props: Omit & { - form: FieldGroupForm, TFormData> + form: + | FieldGroupForm + | FieldGroupApi } & FieldGroupFieldBindingsProps< FieldGroupFieldsOf, TFormData, From ad7972b178c2608ff3d56ed44272916e24b03fec Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:58:44 +0100 Subject: [PATCH 4/7] WIP: Add Vue example for field groups --- examples/vue/field-groups/index.html | 12 +++++ examples/vue/field-groups/package.json | 22 +++++++++ examples/vue/field-groups/src/App.vue | 28 ++++++++++++ examples/vue/field-groups/src/FieldError.vue | 11 +++++ examples/vue/field-groups/src/StringField.vue | 32 +++++++++++++ .../src/fieldGroups/DateRangeField.vue | 43 ++++++++++++++++++ .../src/fieldGroups/LowerBoundField.vue | 27 +++++++++++ .../src/fieldGroups/NumberRangeField.vue | 23 ++++++++++ .../src/fieldGroups/UpperBoundField.vue | 45 +++++++++++++++++++ .../field-groups/src/fieldGroups/dateRange.ts | 7 +++ .../src/fieldGroups/definitions.ts | 21 +++++++++ .../src/fieldGroups/fieldBounds.ts | 12 +++++ .../src/fieldGroups/numberRange.ts | 7 +++ .../field-groups/src/forms/AgeRangeForm.vue | 19 ++++++++ .../src/forms/NestedFieldGroupsForm.vue | 19 ++++++++ .../src/forms/PricingFilterForm.vue | 19 ++++++++ .../src/forms/SwappableDateRangesForm.vue | 31 +++++++++++++ examples/vue/field-groups/src/main.ts | 4 ++ examples/vue/field-groups/tsconfig.json | 18 ++++++++ examples/vue/field-groups/vite.config.ts | 6 +++ 20 files changed, 406 insertions(+) create mode 100644 examples/vue/field-groups/index.html create mode 100644 examples/vue/field-groups/package.json create mode 100644 examples/vue/field-groups/src/App.vue create mode 100644 examples/vue/field-groups/src/FieldError.vue create mode 100644 examples/vue/field-groups/src/StringField.vue create mode 100644 examples/vue/field-groups/src/fieldGroups/DateRangeField.vue create mode 100644 examples/vue/field-groups/src/fieldGroups/LowerBoundField.vue create mode 100644 examples/vue/field-groups/src/fieldGroups/NumberRangeField.vue create mode 100644 examples/vue/field-groups/src/fieldGroups/UpperBoundField.vue create mode 100644 examples/vue/field-groups/src/fieldGroups/dateRange.ts create mode 100644 examples/vue/field-groups/src/fieldGroups/definitions.ts create mode 100644 examples/vue/field-groups/src/fieldGroups/fieldBounds.ts create mode 100644 examples/vue/field-groups/src/fieldGroups/numberRange.ts create mode 100644 examples/vue/field-groups/src/forms/AgeRangeForm.vue create mode 100644 examples/vue/field-groups/src/forms/NestedFieldGroupsForm.vue create mode 100644 examples/vue/field-groups/src/forms/PricingFilterForm.vue create mode 100644 examples/vue/field-groups/src/forms/SwappableDateRangesForm.vue create mode 100644 examples/vue/field-groups/src/main.ts create mode 100644 examples/vue/field-groups/tsconfig.json create mode 100644 examples/vue/field-groups/vite.config.ts diff --git a/examples/vue/field-groups/index.html b/examples/vue/field-groups/index.html new file mode 100644 index 0000000000..06acf435b0 --- /dev/null +++ b/examples/vue/field-groups/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Form Vue Field Groups Example + + +
+ + + diff --git a/examples/vue/field-groups/package.json b/examples/vue/field-groups/package.json new file mode 100644 index 0000000000..8e95b7eee4 --- /dev/null +++ b/examples/vue/field-groups/package.json @@ -0,0 +1,22 @@ +{ + "name": "@tanstack/form-example-vue-field-groups", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port=3005", + "build": "vite build", + "preview": "vite preview", + "test:types": "vue-tsc --noEmit" + }, + "dependencies": { + "@tanstack/vue-form": "^2.0.0-alpha.3", + "vue": "^3.6.0-rc.9", + "zod": "^4.4.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.9", + "typescript": "^6.0.2", + "vite": "^8.2.1", + "vue-tsc": "^3.3.11" + } +} diff --git a/examples/vue/field-groups/src/App.vue b/examples/vue/field-groups/src/App.vue new file mode 100644 index 0000000000..3a8d0ae0f1 --- /dev/null +++ b/examples/vue/field-groups/src/App.vue @@ -0,0 +1,28 @@ + + + diff --git a/examples/vue/field-groups/src/FieldError.vue b/examples/vue/field-groups/src/FieldError.vue new file mode 100644 index 0000000000..8823fe7de7 --- /dev/null +++ b/examples/vue/field-groups/src/FieldError.vue @@ -0,0 +1,11 @@ + + + diff --git a/examples/vue/field-groups/src/StringField.vue b/examples/vue/field-groups/src/StringField.vue new file mode 100644 index 0000000000..a5641dfc94 --- /dev/null +++ b/examples/vue/field-groups/src/StringField.vue @@ -0,0 +1,32 @@ + + + diff --git a/examples/vue/field-groups/src/fieldGroups/DateRangeField.vue b/examples/vue/field-groups/src/fieldGroups/DateRangeField.vue new file mode 100644 index 0000000000..16d9fc926a --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/DateRangeField.vue @@ -0,0 +1,43 @@ + + + diff --git a/examples/vue/field-groups/src/fieldGroups/LowerBoundField.vue b/examples/vue/field-groups/src/fieldGroups/LowerBoundField.vue new file mode 100644 index 0000000000..2839e738f8 --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/LowerBoundField.vue @@ -0,0 +1,27 @@ + + + diff --git a/examples/vue/field-groups/src/fieldGroups/NumberRangeField.vue b/examples/vue/field-groups/src/fieldGroups/NumberRangeField.vue new file mode 100644 index 0000000000..1669cb35d9 --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/NumberRangeField.vue @@ -0,0 +1,23 @@ + + + diff --git a/examples/vue/field-groups/src/fieldGroups/UpperBoundField.vue b/examples/vue/field-groups/src/fieldGroups/UpperBoundField.vue new file mode 100644 index 0000000000..e520666295 --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/UpperBoundField.vue @@ -0,0 +1,45 @@ + + + diff --git a/examples/vue/field-groups/src/fieldGroups/dateRange.ts b/examples/vue/field-groups/src/fieldGroups/dateRange.ts new file mode 100644 index 0000000000..ac96a92a14 --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/dateRange.ts @@ -0,0 +1,7 @@ +import DateRangeFieldComponent from './DateRangeField.vue' +import { dateRange } from './definitions' + +export const DateRangeField = dateRange.bindComponent( + DateRangeFieldComponent, + 'fields', +) diff --git a/examples/vue/field-groups/src/fieldGroups/definitions.ts b/examples/vue/field-groups/src/fieldGroups/definitions.ts new file mode 100644 index 0000000000..dad358259d --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/definitions.ts @@ -0,0 +1,21 @@ +import { defineFieldGroup } from '@tanstack/vue-form' + +// Each group declares virtual names. Callers bind them to their own paths. +export const lowerBound = defineFieldGroup(({ strict }) => ({ + value: strict(), +})) + +export const upperBound = defineFieldGroup(({ strict }) => ({ + value: strict(), + lowerBound: strict(), +})) + +export const numberRange = defineFieldGroup(({ strict }) => ({ + minimum: strict(), + maximum: strict(), +})) + +export const dateRange = defineFieldGroup(({ strict }) => ({ + start: strict(), + end: strict(), +})) diff --git a/examples/vue/field-groups/src/fieldGroups/fieldBounds.ts b/examples/vue/field-groups/src/fieldGroups/fieldBounds.ts new file mode 100644 index 0000000000..28df755193 --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/fieldBounds.ts @@ -0,0 +1,12 @@ +import LowerBoundFieldComponent from './LowerBoundField.vue' +import UpperBoundFieldComponent from './UpperBoundField.vue' +import { lowerBound, upperBound } from './definitions' + +export const LowerBoundField = lowerBound.bindComponent( + LowerBoundFieldComponent, + 'fields', +) +export const UpperBoundField = upperBound.bindComponent( + UpperBoundFieldComponent, + 'fields', +) diff --git a/examples/vue/field-groups/src/fieldGroups/numberRange.ts b/examples/vue/field-groups/src/fieldGroups/numberRange.ts new file mode 100644 index 0000000000..b5f0319d77 --- /dev/null +++ b/examples/vue/field-groups/src/fieldGroups/numberRange.ts @@ -0,0 +1,7 @@ +import NumberRangeFieldComponent from './NumberRangeField.vue' +import { numberRange } from './definitions' + +export const NumberRangeField = numberRange.bindComponent( + NumberRangeFieldComponent, + 'fields', +) diff --git a/examples/vue/field-groups/src/forms/AgeRangeForm.vue b/examples/vue/field-groups/src/forms/AgeRangeForm.vue new file mode 100644 index 0000000000..497e8edc85 --- /dev/null +++ b/examples/vue/field-groups/src/forms/AgeRangeForm.vue @@ -0,0 +1,19 @@ + + + diff --git a/examples/vue/field-groups/src/forms/NestedFieldGroupsForm.vue b/examples/vue/field-groups/src/forms/NestedFieldGroupsForm.vue new file mode 100644 index 0000000000..5e1a2392d3 --- /dev/null +++ b/examples/vue/field-groups/src/forms/NestedFieldGroupsForm.vue @@ -0,0 +1,19 @@ + + + diff --git a/examples/vue/field-groups/src/forms/PricingFilterForm.vue b/examples/vue/field-groups/src/forms/PricingFilterForm.vue new file mode 100644 index 0000000000..eb52fb957a --- /dev/null +++ b/examples/vue/field-groups/src/forms/PricingFilterForm.vue @@ -0,0 +1,19 @@ + + + diff --git a/examples/vue/field-groups/src/forms/SwappableDateRangesForm.vue b/examples/vue/field-groups/src/forms/SwappableDateRangesForm.vue new file mode 100644 index 0000000000..69df1cda66 --- /dev/null +++ b/examples/vue/field-groups/src/forms/SwappableDateRangesForm.vue @@ -0,0 +1,31 @@ + + + diff --git a/examples/vue/field-groups/src/main.ts b/examples/vue/field-groups/src/main.ts new file mode 100644 index 0000000000..01433bca2a --- /dev/null +++ b/examples/vue/field-groups/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/examples/vue/field-groups/tsconfig.json b/examples/vue/field-groups/tsconfig.json new file mode 100644 index 0000000000..a42826ad1b --- /dev/null +++ b/examples/vue/field-groups/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] +} diff --git a/examples/vue/field-groups/vite.config.ts b/examples/vue/field-groups/vite.config.ts new file mode 100644 index 0000000000..544087a71c --- /dev/null +++ b/examples/vue/field-groups/vite.config.ts @@ -0,0 +1,6 @@ +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [vue()], +}) From 9cc23d6ca01f99d0e831413d2a937de8bed47974 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:01:16 +0100 Subject: [PATCH 5/7] feat: allow nested field groups for Vue --- .../vue-form/src/FieldGroup/withFields.lib.ts | 4 +-- .../src/FieldGroup/withFields.public.ts | 13 ++++++-- pnpm-lock.yaml | 31 +++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/vue-form/src/FieldGroup/withFields.lib.ts b/packages/vue-form/src/FieldGroup/withFields.lib.ts index d59e3eafbd..4d1ab56613 100644 --- a/packages/vue-form/src/FieldGroup/withFields.lib.ts +++ b/packages/vue-form/src/FieldGroup/withFields.lib.ts @@ -10,11 +10,12 @@ import type { InternalFieldGroupBindings, } from '@tanstack/form-core/internals' import type { AnyFieldGroupApi } from './FieldGroupApi.public' +import type { InternalVueFormApi } from '../VueForm/VueFormApi.lib' function attachVueFieldGroupComponents( group: InternalFieldGroupApi, - form: any, ): AnyFieldGroupApi { + const form = group.form as InternalVueFormApi const forwardField = (component: Component, name: string) => defineComponent( (_props, context) => { @@ -88,7 +89,6 @@ function withFieldsRuntime( context.attrs[fieldsPropName] as InternalFieldGroupBindings | undefined, }), - form, ) return () => { diff --git a/packages/vue-form/src/FieldGroup/withFields.public.ts b/packages/vue-form/src/FieldGroup/withFields.public.ts index 9be1b5a38c..7e1140e852 100644 --- a/packages/vue-form/src/FieldGroup/withFields.public.ts +++ b/packages/vue-form/src/FieldGroup/withFields.public.ts @@ -71,9 +71,14 @@ export type FieldGroupWithFieldsFn< component: Component & (new (props: TProps & PublicProps) => any), fieldsPropName: TFieldsPropName & FieldGroupFieldsPropName, -) => new ( +) => new < + TFormData, + TParentFieldComponents extends FieldGroupFieldComponentsOf, +>( props: Omit & { - form: FieldGroupForm, TFormData> + form: + | FieldGroupForm + | FieldGroupApi } & FieldGroupFieldBindingsProps< FieldGroupFieldsOf, TFormData, @@ -82,7 +87,9 @@ export type FieldGroupWithFieldsFn< PublicProps, ) => VueComponentInstance< Omit & { - form: FieldGroupForm, TFormData> + form: + | FieldGroupForm + | FieldGroupApi } & FieldGroupFieldBindingsInstanceProp< TFieldGroup, TFormData, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94fd383dd4..af25c3421a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2084,6 +2084,31 @@ importers: specifier: ^3.3.11 version: 3.3.11(typescript@6.0.3) + examples/vue/field-groups: + dependencies: + '@tanstack/vue-form': + specifier: ^2.0.0-alpha.3 + version: link:../../../packages/vue-form + vue: + specifier: ^3.6.0-rc.9 + version: 3.6.0-rc.9(typescript@6.0.3) + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.9 + version: 6.0.9(vite@8.3.0(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(less@4.8.1(supports-color@8.1.1))(sass@1.102.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.6.0-rc.9(typescript@6.0.3)) + typescript: + specifier: ^6.0.2 + version: 6.0.3 + vite: + specifier: ^8.2.1 + version: 8.3.0(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(less@4.8.1(supports-color@8.1.1))(sass@1.102.0)(terser@5.49.0)(yaml@2.9.0) + vue-tsc: + specifier: ^3.3.11 + version: 3.3.11(typescript@6.0.3) + examples/vue/large-form: dependencies: '@tanstack/vue-form': @@ -18934,6 +18959,12 @@ snapshots: vite: 8.3.0(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(less@4.8.1(supports-color@7.2.0))(sass@1.102.0)(terser@5.49.0)(yaml@2.9.0) vue: 3.6.0-rc.9(typescript@6.0.3) + '@vitejs/plugin-vue@6.0.9(vite@8.3.0(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(less@4.8.1(supports-color@8.1.1))(sass@1.102.0)(terser@5.49.0)(yaml@2.9.0))(vue@3.6.0-rc.9(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.0(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(less@4.8.1(supports-color@8.1.1))(sass@1.102.0)(terser@5.49.0)(yaml@2.9.0) + vue: 3.6.0-rc.9(typescript@6.0.3) + '@vitest/browser-playwright@4.1.10(playwright@1.62.1)(vite@8.3.0(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(less@4.8.1(supports-color@8.1.1))(sass@1.102.0)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@vitest/browser': 4.1.10(vite@8.3.0(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(less@4.8.1(supports-color@8.1.1))(sass@1.102.0)(terser@5.49.0)(yaml@2.9.0))(vitest@4.1.10) From 6c9a64bc75e0f6e837524007deb1a10de634cab9 Mon Sep 17 00:00:00 2001 From: LeCarbonator <18158911+LeCarbonator@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:35:18 +0100 Subject: [PATCH 6/7] feat: allow nested field groups in Svelte --- examples/svelte/field-groups/src/App.svelte | 17 ++++++++++++ .../src/fieldGroups/NumberRangeFields.svelte | 26 +++++++++++++++++++ .../src/fieldGroups/numberRange.ts | 14 ++++++++++ packages/svelte-form/src/Components.public.ts | 3 ++- packages/svelte-form/src/Field.svelte | 5 ++-- .../src/FieldGroup/FieldGroupApi.public.ts | 6 ++++- .../src/FieldGroup/withFields.lib.ts | 4 +-- .../src/FieldGroup/withFields.public.ts | 9 +++++-- .../src/fieldComponentsType.lib.ts | 10 +++++++ 9 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 examples/svelte/field-groups/src/fieldGroups/NumberRangeFields.svelte create mode 100644 examples/svelte/field-groups/src/fieldGroups/numberRange.ts create mode 100644 packages/svelte-form/src/fieldComponentsType.lib.ts diff --git a/examples/svelte/field-groups/src/App.svelte b/examples/svelte/field-groups/src/App.svelte index b06ec8c20e..aaa17777fe 100644 --- a/examples/svelte/field-groups/src/App.svelte +++ b/examples/svelte/field-groups/src/App.svelte @@ -1,6 +1,7 @@ + +
+ {label} + + + +
diff --git a/examples/svelte/field-groups/src/fieldGroups/numberRange.ts b/examples/svelte/field-groups/src/fieldGroups/numberRange.ts new file mode 100644 index 0000000000..3b629a9aa9 --- /dev/null +++ b/examples/svelte/field-groups/src/fieldGroups/numberRange.ts @@ -0,0 +1,14 @@ +import { defineFieldGroup } from '@tanstack/svelte-form' +import NumberRangeFields from './NumberRangeFields.svelte' + +const numberRangeFieldGroup = defineFieldGroup(({ strict }) => ({ + minimum: strict(), + maximum: strict(), +})) + +export const numberRangeFields = numberRangeFieldGroup.fields + +export const NumberRangeField = numberRangeFieldGroup.bindComponent( + NumberRangeFields, + 'fields', +) diff --git a/packages/svelte-form/src/Components.public.ts b/packages/svelte-form/src/Components.public.ts index 765e5e6c3a..8602747d52 100644 --- a/packages/svelte-form/src/Components.public.ts +++ b/packages/svelte-form/src/Components.public.ts @@ -20,6 +20,7 @@ import type { Snippet, SvelteComponent, } from 'svelte' +import type { SvelteFieldComponentsType } from './fieldComponentsType.lib' export type WithoutFunction = { [K in keyof T as T[K] extends Function ? never : K]: T[K] @@ -348,7 +349,7 @@ export interface SvelteTanStackFormComponents< never, never >, -> { +> extends SvelteFieldComponentsType { Field: SvelteFormFieldComponent ArrayField: SvelteFormArrayFieldComponent< TFormData, diff --git a/packages/svelte-form/src/Field.svelte b/packages/svelte-form/src/Field.svelte index 40ed687fb3..d49816f210 100644 --- a/packages/svelte-form/src/Field.svelte +++ b/packages/svelte-form/src/Field.svelte @@ -1,5 +1,6 @@