(),
+}))
+
+interface NumberRangeFieldProps {
+ fields: typeof numberRange.fields
+ label: string
+}
+
+function NumberRangeFieldComponent({ fields, label }: NumberRangeFieldProps) {
+ return (
+
+ )
+}
+
+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/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 (
+
+ )
+}
+
+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/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 (
+
+ )
+}
+
+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/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 @@
+
+
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/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 @@
+
+
+
+
+
Field Group example
+
+
+
+ Nested Field Groups
+
+
+
+ Swappable Date Ranges Form
+
+
+
+
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 @@
+
+
+
+
+ {{ field.errors.map((error) => error.message).join('\n') }}
+
+
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 @@
+
+
+
+
+
+ {{ JSON.stringify(filters, null, 2) }}
+
+
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()],
+})
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/lit-form/src/with-fields.ts b/packages/lit-form/src/with-fields.ts
index 51f0893ebf..1f1068c4a9 100644
--- a/packages/lit-form/src/with-fields.ts
+++ b/packages/lit-form/src/with-fields.ts
@@ -19,7 +19,11 @@ import type {
} from '@tanstack/form-core'
import type { ReadonlyAtom } from '@tanstack/lit-store'
import type { Part, PartInfo } from 'lit/directive.js'
-import type { InternalFieldGroupBindings } from '@tanstack/form-core/internals'
+import type {
+ InternalFieldGroupBindings,
+ InternalFieldGroupOptions,
+} from '@tanstack/form-core/internals'
+import type { AnyLitFormApi } from './get-form-type.js'
import type {
LitFieldMethods,
LitSubscribeMethod,
@@ -65,6 +69,19 @@ type AnyLitForm = TanStackFormController<
any
>
+class InternalLitFieldGroupApi extends InternalFieldGroupApi {
+ readonly formController: AnyLitFormApi
+ constructor(
+ options: InternalFieldGroupOptions,
+ formController: AnyLitFormApi,
+ ) {
+ super(options)
+ this.formController = formController
+ }
+}
+
+type InternalLitFieldGroupParent = AnyLitForm | InternalLitFieldGroupApi
+
export type FieldGroupWithFieldsFn<
TFieldGroup extends {
readonly [fieldGroupFieldsSymbol]: FieldGroupFields
@@ -77,7 +94,7 @@ export type FieldGroupWithFieldsFn<
fieldsPropName: TFieldsPropName,
) => (
props: Omit & {
- form: AnyLitForm
+ form: AnyLitForm | LitFieldGroupApi
} & FieldGroupFieldBindingsProps<
FieldGroupFieldsOf,
TFormData,
@@ -97,9 +114,10 @@ export type DefineFieldGroupFn = (
) => FieldGroupDefinition
function attachLitFieldGroupMethods(
- group: InternalFieldGroupApi,
- form: AnyLitForm,
+ group: InternalLitFieldGroupApi,
): LitFieldGroupApi {
+ const form = group.formController
+
const resolveOptions = (options: { name: string }) =>
group._getFormFieldOptions(options, (base, overrides) => ({
...base,
@@ -120,7 +138,7 @@ function attachLitFieldGroupMethods(
}
class ReusableFieldGroupDirective extends Directive {
- private form?: AnyLitForm
+ private form?: InternalLitFieldGroupParent
private fieldNames?: Array
private bindings = createAtom(
undefined,
@@ -145,20 +163,24 @@ class ReusableFieldGroupDirective extends Directive {
if (this.form !== form || this.fieldNames !== fieldNames || !this.api) {
this.form = form
this.fieldNames = fieldNames
+ const isFieldGroup = form instanceof InternalLitFieldGroupApi
+ const controller = isFieldGroup ? form.formController : form
this.api = attachLitFieldGroupMethods(
- new InternalFieldGroupApi({
- form: form.api as never,
- fieldNames,
- getBindings: () => this.bindings.get(),
- }),
- form,
+ new InternalLitFieldGroupApi(
+ {
+ form: isFieldGroup ? form : (form.api as never),
+ fieldNames,
+ getBindings: () => this.bindings.get(),
+ },
+ controller,
+ ),
)
}
return render({ ...props, [propName]: this.api })
}
render(
- form: AnyLitForm,
+ form: InternalLitFieldGroupParent,
bindings: InternalFieldGroupBindings | undefined,
fieldNames: Array,
render: (props: Record) => unknown,
@@ -189,7 +211,7 @@ const withFieldsRuntime = (
throw new Error('TanStack Form: Field groups must receive a `form` prop.')
}
return reusableFieldGroupDirective(
- form as AnyLitForm,
+ form as InternalLitFieldGroupParent,
bindings as InternalFieldGroupBindings | undefined,
fieldNames,
render,
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,
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,
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,
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 @@