diff --git a/packages/svelte-form/src/AppField.svelte b/packages/svelte-form/src/AppField.svelte
new file mode 100644
index 000000000..41daccd13
--- /dev/null
+++ b/packages/svelte-form/src/AppField.svelte
@@ -0,0 +1,19 @@
+
+
+
+
+ {#snippet children(field: any)}
+
+ {/snippet}
+
diff --git a/packages/svelte-form/src/AppForm.svelte b/packages/svelte-form/src/AppForm.svelte
new file mode 100644
index 000000000..ce0bf3798
--- /dev/null
+++ b/packages/svelte-form/src/AppForm.svelte
@@ -0,0 +1,16 @@
+
+
+
+{@render children?.()}
diff --git a/packages/svelte-form/src/InnerAppField.svelte b/packages/svelte-form/src/InnerAppField.svelte
new file mode 100644
index 000000000..de0f34873
--- /dev/null
+++ b/packages/svelte-form/src/InnerAppField.svelte
@@ -0,0 +1,17 @@
+
+
+
+{@render children?.(Object.assign(field, fieldComponents))}
diff --git a/packages/svelte-form/src/context-keys.ts b/packages/svelte-form/src/context-keys.ts
new file mode 100644
index 000000000..7903532e9
--- /dev/null
+++ b/packages/svelte-form/src/context-keys.ts
@@ -0,0 +1,2 @@
+export const fieldContextKey = '__tanstack_field_context_key'
+export const formContextKey = '__tanstack_form_context_key'
diff --git a/packages/svelte-form/src/createForm.svelte.ts b/packages/svelte-form/src/createForm.svelte.ts
index 4de61273f..5bd92181c 100644
--- a/packages/svelte-form/src/createForm.svelte.ts
+++ b/packages/svelte-form/src/createForm.svelte.ts
@@ -179,6 +179,51 @@ export interface SvelteFormApi<
WithoutFunction
}
+/**
+ * An extended version of the `FormApi` class that includes Svelte-specific functionalities from `SvelteFormApi`
+ */
+export type SvelteFormExtendedApi<
+ 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
+> &
+ SvelteFormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta
+ >
+
export function createForm<
TParentData,
TFormOnMount extends undefined | FormValidateOrFn,
@@ -241,10 +286,10 @@ export function createForm<
// @ts-expect-error constructor definition exists only on a type level
extendedApi.Field = (internal, props) =>
- Field(internal, { ...props, form: api })
+ Field(internal, { ...props, form: api as never } as never)
extendedApi.createField = (props) =>
createField(() => {
- return { ...props(), form: api }
+ return { ...props(), form: api } as never
}) as never // Type cast because else "Error: Type instantiation is excessively deep and possibly infinite."
extendedApi.useStore = (selector) => useStore(api.store, selector)
// @ts-expect-error constructor definition exists only on a type level
diff --git a/packages/svelte-form/src/createFormRune.svelte.ts b/packages/svelte-form/src/createFormRune.svelte.ts
new file mode 100644
index 000000000..2946584db
--- /dev/null
+++ b/packages/svelte-form/src/createFormRune.svelte.ts
@@ -0,0 +1,273 @@
+import { getContext } from 'svelte'
+import { createForm } from './createForm.svelte'
+import AppFormSvelte from './AppForm.svelte'
+import AppFieldSvelte from './AppField.svelte'
+import { fieldContextKey, formContextKey } from './context-keys.js'
+import type {
+ AnyFieldApi,
+ AnyFormApi,
+ FieldApi,
+ FormAsyncValidateOrFn,
+ FormOptions,
+ FormValidateOrFn,
+} from '@tanstack/form-core'
+import type { FieldComponent } from './types.js'
+import type { SvelteFormExtendedApi } from './createForm.svelte'
+import type { Component, Snippet, SvelteComponent } from 'svelte'
+
+/**
+ * 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 occured.
+ */
+type UnwrapOrAny = [unknown] extends [T] ? any : T
+type UnwrapDefaultOrAny = [DefaultT] extends [T]
+ ? [T] extends [DefaultT]
+ ? any
+ : T
+ : T
+
+export function createFormRuneContexts() {
+ function useFieldContext() {
+ const field = getContext(fieldContextKey) as AnyFieldApi
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!field) {
+ throw new Error(
+ '`fieldContext` only works when within a `fieldComponent` passed to `createFormRune`',
+ )
+ }
+
+ 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
+ >
+ }
+
+ function useFormContext() {
+ const form = getContext(formContextKey) as AnyFormApi
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (!form) {
+ throw new Error(
+ '`formContext` only works when within a `formComponent` passed to `createFormRune`',
+ )
+ }
+
+ return form as SvelteFormExtendedApi<
+ // 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
+ >
+ }
+
+ return { useFieldContext, useFormContext }
+}
+
+interface CreateFormRuneProps<
+ TFieldComponents extends Record>,
+ TFormComponents extends Record>,
+> {
+ fieldComponents: TFieldComponents
+ formComponents: TFormComponents
+}
+
+/**
+ * @private
+ */
+type AppFieldExtendedSvelteFormApi<
+ 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>,
+> = SvelteFormExtendedApi<
+ 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: Component<{ children: Snippet }>
+ }
+
+export function createFormRune<
+ const TComponents extends Record>,
+ const TFormComponents extends Record>,
+>({
+ fieldComponents,
+ formComponents,
+}: CreateFormRuneProps) {
+ function createAppForm<
+ 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
+ >,
+ ): AppFieldExtendedSvelteFormApi<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta,
+ TComponents,
+ TFormComponents
+ > {
+ const form = createForm(props)
+
+ const AppForm = ((internal, props) => {
+ return AppFormSvelte(internal, { ...props, form })
+ }) as Component<{ children: Snippet }>
+
+ const AppField = ((internal, { children, ...fieldProps }) =>
+ AppFieldSvelte(internal, {
+ fieldProps,
+ form,
+ fieldComponents,
+ children,
+ } as never)) as FieldComponent<
+ TFormData,
+ TOnMount,
+ TOnChange,
+ TOnChangeAsync,
+ TOnBlur,
+ TOnBlurAsync,
+ TOnSubmit,
+ TOnSubmitAsync,
+ TOnDynamic,
+ TOnDynamicAsync,
+ TOnServer,
+ TSubmitMeta,
+ TComponents
+ >
+
+ const extendedForm = Object.assign(form, {
+ AppField,
+ AppForm,
+ ...formComponents,
+ })
+
+ return extendedForm
+ }
+
+ return {
+ createAppForm,
+ }
+}
diff --git a/packages/svelte-form/src/index.ts b/packages/svelte-form/src/index.ts
index a76cbb7f2..ed692af69 100644
--- a/packages/svelte-form/src/index.ts
+++ b/packages/svelte-form/src/index.ts
@@ -7,3 +7,8 @@ export { createForm, type SvelteFormApi } from './createForm.svelte.js'
export { default as Field, createField } from './Field.svelte'
export type { CreateField, FieldComponent } from './types.js'
+
+export {
+ createFormRune,
+ createFormRuneContexts,
+} from './createFormRune.svelte.js'
diff --git a/packages/svelte-form/src/types.ts b/packages/svelte-form/src/types.ts
index cfb9d587c..853834d0a 100644
--- a/packages/svelte-form/src/types.ts
+++ b/packages/svelte-form/src/types.ts
@@ -123,6 +123,7 @@ export type FieldComponent<
TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
TFormOnServer extends undefined | FormAsyncValidateOrFn,
TParentSubmitMeta,
+ ExtendedApi = {},
> =
// This giant type allows the type
// - to be used as a function (which they are now in Svelte 5)
@@ -178,7 +179,8 @@ export type FieldComponent<
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
- TParentSubmitMeta
+ TParentSubmitMeta,
+ ExtendedApi
>,
'form'
>,
@@ -235,7 +237,8 @@ export type FieldComponent<
TFormOnDynamic,
TFormOnDynamicAsync,
TFormOnServer,
- TParentSubmitMeta
+ TParentSubmitMeta,
+ ExtendedApi
>,
'form'
>
@@ -275,6 +278,7 @@ type FieldComponentProps<
TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn,
TFormOnServer extends undefined | FormAsyncValidateOrFn,
TParentSubmitMeta,
+ ExtendedApi = {},
> = {
children: Snippet<
[
@@ -302,7 +306,8 @@ type FieldComponentProps<
TFormOnDynamicAsync,
TFormOnServer,
TParentSubmitMeta
- >,
+ > &
+ ExtendedApi,
]
>
} & Omit<
diff --git a/packages/svelte-form/tests/large-components/context.ts b/packages/svelte-form/tests/large-components/context.ts
new file mode 100644
index 000000000..3e20b01d6
--- /dev/null
+++ b/packages/svelte-form/tests/large-components/context.ts
@@ -0,0 +1,3 @@
+import { createFormRuneContexts } from '../../src/index.js'
+
+export const { useFieldContext } = createFormRuneContexts()
diff --git a/packages/svelte-form/tests/large-components/rune.ts b/packages/svelte-form/tests/large-components/rune.ts
new file mode 100644
index 000000000..727526ae8
--- /dev/null
+++ b/packages/svelte-form/tests/large-components/rune.ts
@@ -0,0 +1,9 @@
+import { createFormRune } from '../../src/createFormRune.svelte.js'
+import TextField from './text-field.svelte'
+
+export const { createAppForm } = createFormRune({
+ fieldComponents: {
+ TextField,
+ },
+ formComponents: {},
+})
diff --git a/packages/svelte-form/tests/large-components/text-field.svelte b/packages/svelte-form/tests/large-components/text-field.svelte
new file mode 100644
index 000000000..267569203
--- /dev/null
+++ b/packages/svelte-form/tests/large-components/text-field.svelte
@@ -0,0 +1,16 @@
+
+
+
diff --git a/packages/svelte-form/tests/large.svelte b/packages/svelte-form/tests/large.svelte
new file mode 100644
index 000000000..30ee0f575
--- /dev/null
+++ b/packages/svelte-form/tests/large.svelte
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+ {#snippet children(field)}
+
+ {/snippet}
+
+
+
+{JSON.stringify(formState.current, null, 2)}
diff --git a/packages/svelte-form/tests/large.test.ts b/packages/svelte-form/tests/large.test.ts
new file mode 100644
index 000000000..9ccdc0a0c
--- /dev/null
+++ b/packages/svelte-form/tests/large.test.ts
@@ -0,0 +1,38 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { userEvent } from '@testing-library/user-event'
+import { mount, unmount } from 'svelte'
+import TestForm, { getSampleData } from './large.svelte'
+
+describe('Svelte Tests', () => {
+ let element: HTMLDivElement
+ let instance: any
+ beforeEach(async () => {
+ element = document.createElement('div')
+ document.body.appendChild(element)
+ instance = mount(TestForm, {
+ target: element,
+ })
+ })
+
+ afterEach(() => {
+ unmount(instance)
+ element.remove()
+ })
+
+ it('should have initial values', async () => {
+ expect(element.querySelector('#firstName')).toHaveValue(
+ getSampleData().firstName,
+ )
+ })
+
+ it('should mirror user input', async () => {
+ const firstName = element.querySelector('#firstName')!
+ const firstNameValue = 'Jobs'
+ await userEvent.type(firstName, firstNameValue)
+
+ const form = JSON.parse(element.querySelector('pre')!.textContent!)
+ expect(form.values.firstName).toBe(
+ getSampleData().firstName + firstNameValue,
+ )
+ })
+})