Skip to content
19 changes: 19 additions & 0 deletions packages/svelte-form/src/AppField.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- Take "form" as an prop, pass it to context, and render children -->
<script lang="ts">
import { Snippet } from 'svelte'
import InnerAppField from './InnerAppField.svelte'

interface Props {
form: any
fieldComponents: any
children: Snippet
fieldProps: any
}
const { children, form, fieldComponents, fieldProps }: Props = $props()
</script>

<form.Field {...fieldProps}>
{#snippet children(field: any)}
<InnerAppField field={field} children={children} fieldComponents={fieldComponents}/>
{/snippet}
</form.Field>
16 changes: 16 additions & 0 deletions packages/svelte-form/src/AppForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- Take "form" as an prop, pass it to context, and render children -->
<script lang="ts">
import { setContext, Snippet } from 'svelte'
import { formContextKey } from './context-keys.js'

interface Props {
form: any
children: Snippet
}

const { children, form }: Props = $props()

setContext(formContextKey, form)
</script>

{@render children?.()}
17 changes: 17 additions & 0 deletions packages/svelte-form/src/InnerAppField.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Take "form" as an prop, pass it to context, and render children -->
<script lang="ts">
import { setContext, Snippet } from 'svelte'
import { fieldContextKey } from './context-keys.js'

interface Props {
field: any
fieldComponents: any
children: Snippet<[any]>
}

const { children, field, fieldComponents }: Props = $props()

setContext(fieldContextKey, field)
</script>

{@render children?.(Object.assign(field, fieldComponents))}
2 changes: 2 additions & 0 deletions packages/svelte-form/src/context-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const fieldContextKey = '__tanstack_field_context_key'
export const formContextKey = '__tanstack_form_context_key'
49 changes: 47 additions & 2 deletions packages/svelte-form/src/createForm.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,51 @@ export interface SvelteFormApi<
WithoutFunction<Component>
}

/**
* An extended version of the `FormApi` class that includes Svelte-specific functionalities from `SvelteFormApi`
*/
export type SvelteFormExtendedApi<
TFormData,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnBlur extends undefined | FormValidateOrFn<TFormData>,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
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<TParentData>,
Expand Down Expand Up @@ -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
Expand Down
273 changes: 273 additions & 0 deletions packages/svelte-form/src/createFormRune.svelte.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
* arg?: T
* }
*
* function test<T>(arg?: Partial<Args<T>>): T {
* return 0 as any;
* }
*
* const a = test({});
*
* Then `T` will default to `unknown`.
*
* However, if we change `test` to be:
*
* @example
*
* function test<T extends undefined>(arg?: Partial<Args<T>>): 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<T> = [unknown] extends [T] ? any : T
type UnwrapDefaultOrAny<DefaultT, T> = [DefaultT] extends [T]
? [T] extends [DefaultT]
? any
: T
: T

export function createFormRuneContexts() {
function useFieldContext<TData>() {
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<string, never>,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any,
any
>
}

return { useFieldContext, useFormContext }
}

interface CreateFormRuneProps<
TFieldComponents extends Record<string, Component<any, any>>,
TFormComponents extends Record<string, Component<any, any>>,
> {
fieldComponents: TFieldComponents
formComponents: TFormComponents
}

/**
* @private
*/
type AppFieldExtendedSvelteFormApi<
TFormData,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnBlur extends undefined | FormValidateOrFn<TFormData>,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
TSubmitMeta,
TFieldComponents extends Record<string, Component<any, any>>,
TFormComponents extends Record<string, Component<any, any>>,
> = SvelteFormExtendedApi<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta
> &
NoInfer<TFormComponents> & {
AppField: FieldComponent<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer,
TSubmitMeta,
NoInfer<TFieldComponents>
>
AppForm: Component<{ children: Snippet }>
}

export function createFormRune<
const TComponents extends Record<string, Component<any, any>>,
const TFormComponents extends Record<string, Component<any, any>>,
>({
fieldComponents,
formComponents,
}: CreateFormRuneProps<TComponents, TFormComponents>) {
function createAppForm<
TFormData,
TOnMount extends undefined | FormValidateOrFn<TFormData>,
TOnChange extends undefined | FormValidateOrFn<TFormData>,
TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnBlur extends undefined | FormValidateOrFn<TFormData>,
TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnDynamic extends undefined | FormValidateOrFn<TFormData>,
TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
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,
}
}
Loading