TanStack Form is the ultimate solution for handling forms in web applications, providing a powerful and flexible approach to form management. Designed with first-class TypeScript support, headless UI components, and a framework-agnostic design, it streamlines form handling and ensures a seamless experience across various front-end frameworks.
Most web frameworks do not offer a comprehensive solution for form handling, leaving developers to create their own custom implementations or rely on less-capable libraries. This often results in a lack of consistency, poor performance, and increased development time. TanStack Form aims to address these challenges by providing an all-in-one solution for managing forms that is both powerful and easy to use.
With TanStack Form, developers can tackle common form-related challenges such as:
By providing a complete solution for these challenges, TanStack Form empowers developers to build robust and user-friendly forms with ease.
In the example below, you can see TanStack Form in action with the React framework adapter:
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
import { useForm } from '@tanstack/react-form'
import { FieldInfo } from './FieldInfo.tsx'
export default function App() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})
return (
<div>
<h1>Simple Form Example</h1>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<div>
{/* A type-safe field component*/}
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return (
value.includes('error') && 'No "error" allowed in first name'
)
},
}}
children={(field) => {
// Avoid hasty abstractions. Render props are great!
return (
<>
<label htmlFor={field.name}>First Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)
}}
/>
</div>
<div>
<form.Field
name="lastName"
children={(field) => (
<>
<label htmlFor={field.name}>Last Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)}
/>
</div>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<>
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
<button
type="reset"
onClick={(e) => {
// Avoid unexpected resets of form elements (especially <select> elements)
e.preventDefault()
form.reset()
}}
>
Reset
</button>
</>
)}
/>
</form>
</div>
)
}
const rootElement = document.getElementById('root')!
createRoot(rootElement).render(
<React.StrictMode>
<App />
<TanStackDevtools
config={{ hideUntilHover: true }}
plugins={[formDevtoolsPlugin()]}
/>
</React.StrictMode>,
)
import * as React from 'react'
import { createRoot } from 'react-dom/client'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { formDevtoolsPlugin } from '@tanstack/react-form-devtools'
import { useForm } from '@tanstack/react-form'
import { FieldInfo } from './FieldInfo.tsx'
export default function App() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})
return (
<div>
<h1>Simple Form Example</h1>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<div>
{/* A type-safe field component*/}
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return (
value.includes('error') && 'No "error" allowed in first name'
)
},
}}
children={(field) => {
// Avoid hasty abstractions. Render props are great!
return (
<>
<label htmlFor={field.name}>First Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)
}}
/>
</div>
<div>
<form.Field
name="lastName"
children={(field) => (
<>
<label htmlFor={field.name}>Last Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
)}
/>
</div>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<>
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
<button
type="reset"
onClick={(e) => {
// Avoid unexpected resets of form elements (especially <select> elements)
e.preventDefault()
form.reset()
}}
>
Reset
</button>
</>
)}
/>
</form>
</div>
)
}
const rootElement = document.getElementById('root')!
createRoot(rootElement).render(
<React.StrictMode>
<App />
<TanStackDevtools
config={{ hideUntilHover: true }}
plugins={[formDevtoolsPlugin()]}
/>
</React.StrictMode>,
)
import type { AnyFieldApi } from '@tanstack/react-form'
export function FieldInfo({ field }: { field: AnyFieldApi }) {
return (
<>
{field.state.meta.isTouched && !field.state.meta.isValid ? (
<em>{field.state.meta.errors.join(',')}</em>
) : null}
{field.state.meta.isValidating ? 'Validating...' : null}
</>
)
}
import type { AnyFieldApi } from '@tanstack/react-form'
export function FieldInfo({ field }: { field: AnyFieldApi }) {
return (
<>
{field.state.meta.isTouched && !field.state.meta.isValid ? (
<em>{field.state.meta.errors.join(',')}</em>
) : null}
{field.state.meta.isValidating ? 'Validating...' : null}
</>
)
}
In the example below, you can see TanStack Form in action with the Vue framework adapter:
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import FieldInfo from './FieldInfo.vue'
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
alert(JSON.stringify(value))
},
})
async function onChangeFirstName({ value }: { value: string }) {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes(`error`) && `No 'error' allowed in first name`
}
</script>
<template>
<form
@submit="
(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}
"
>
<div>
<form.Field
name="firstName"
:validators="{
onChange: ({ value }) =>
!value
? `A first name is required`
: value.length < 3
? `First name must be at least 3 characters`
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: onChangeFirstName,
}"
>
<template v-slot="{ field, state }">
<label :htmlFor="field.name">First Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
@input="
(e) => field.handleChange((e.target as HTMLInputElement).value)
"
@blur="field.handleBlur"
/>
<FieldInfo :state="state" />
</template>
</form.Field>
</div>
<div>
<form.Field name="lastName">
<template v-slot="{ field, state }">
<label :htmlFor="field.name">Last Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
@input="
(e) => field.handleChange((e.target as HTMLInputElement).value)
"
@blur="field.handleBlur"
/>
<FieldInfo :state="state" />
</template>
</form.Field>
</div>
<form.Subscribe>
<template v-slot="{ canSubmit, isSubmitting }">
<button type="submit" :disabled="!canSubmit">
{{ isSubmitting ? '...' : 'Submit' }}
</button>
</template>
</form.Subscribe>
</form>
</template>
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import FieldInfo from './FieldInfo.vue'
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
alert(JSON.stringify(value))
},
})
async function onChangeFirstName({ value }: { value: string }) {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes(`error`) && `No 'error' allowed in first name`
}
</script>
<template>
<form
@submit="
(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}
"
>
<div>
<form.Field
name="firstName"
:validators="{
onChange: ({ value }) =>
!value
? `A first name is required`
: value.length < 3
? `First name must be at least 3 characters`
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: onChangeFirstName,
}"
>
<template v-slot="{ field, state }">
<label :htmlFor="field.name">First Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
@input="
(e) => field.handleChange((e.target as HTMLInputElement).value)
"
@blur="field.handleBlur"
/>
<FieldInfo :state="state" />
</template>
</form.Field>
</div>
<div>
<form.Field name="lastName">
<template v-slot="{ field, state }">
<label :htmlFor="field.name">Last Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
@input="
(e) => field.handleChange((e.target as HTMLInputElement).value)
"
@blur="field.handleBlur"
/>
<FieldInfo :state="state" />
</template>
</form.Field>
</div>
<form.Subscribe>
<template v-slot="{ canSubmit, isSubmitting }">
<button type="submit" :disabled="!canSubmit">
{{ isSubmitting ? '...' : 'Submit' }}
</button>
</template>
</form.Subscribe>
</form>
</template>
<script setup lang="ts">
import { AnyFieldApi } from '@tanstack/vue-form'
const props = defineProps<{
state: AnyFieldApi['state']
}>()
</script>
<template>
<template v-if="props.state.meta.isTouched">
<em v-for="error of props.state.meta.errors">{{ error }}</em>
{{ props.state.meta.isValidating ? 'Validating...' : null }}
</template>
</template>
<script setup lang="ts">
import { AnyFieldApi } from '@tanstack/vue-form'
const props = defineProps<{
state: AnyFieldApi['state']
}>()
</script>
<template>
<template v-if="props.state.meta.isTouched">
<em v-for="error of props.state.meta.errors">{{ error }}</em>
{{ props.state.meta.isValidating ? 'Validating...' : null }}
</template>
</template>
In the example below, you can see TanStack Form in action with the Angular framework adapter:
import { Component } from '@angular/core'
import { TanStackField, injectForm, injectStore } from '@tanstack/angular-form'
import type {
FieldValidateAsyncFn,
FieldValidateFn,
} from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<form (submit)="handleSubmit($event)">
<div>
<ng-container
[tanstackField]="form"
name="firstName"
[validators]="{
onChange: firstNameValidator,
onChangeAsyncDebounceMs: 500,
onChangeAsync: firstNameAsyncValidator,
}"
#firstName="field"
>
<label [for]="firstName.api.name">First Name:</label>
<input
[id]="firstName.api.name"
[name]="firstName.api.name"
[value]="firstName.api.state.value"
(blur)="firstName.api.handleBlur()"
(input)="firstName.api.handleChange($any($event).target.value)"
/>
@if (firstName.api.state.meta.isTouched) {
@for (error of firstName.api.state.meta.errors; track $index) {
<div style="color: red">
{{ error }}
</div>
}
}
@if (firstName.api.state.meta.isValidating) {
<p>Validating...</p>
}
</ng-container>
</div>
<div>
<ng-container [tanstackField]="form" name="lastName" #lastName="field">
<label [for]="lastName.api.name">Last Name:</label>
<input
[id]="lastName.api.name"
[name]="lastName.api.name"
[value]="lastName.api.state.value"
(blur)="lastName.api.handleBlur()"
(input)="lastName.api.handleChange($any($event).target.value)"
/>
</ng-container>
</div>
<button type="submit" [disabled]="!canSubmit()">
{{ isSubmitting() ? '...' : 'Submit' }}
</button>
<button type="reset" (click)="form.reset()">Reset</button>
</form>
`,
})
export class AppComponent {
firstNameValidator: FieldValidateFn<any, string, any> = ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined
firstNameAsyncValidator: FieldValidateAsyncFn<any, string, any> = async ({
value,
}) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
}
form = injectForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit({ value }) {
// Do something with form data
console.log(value)
},
})
canSubmit = injectStore(this.form, (state) => state.canSubmit)
isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
handleSubmit(event: SubmitEvent) {
event.preventDefault()
event.stopPropagation()
this.form.handleSubmit()
}
}
import { Component } from '@angular/core'
import { TanStackField, injectForm, injectStore } from '@tanstack/angular-form'
import type {
FieldValidateAsyncFn,
FieldValidateFn,
} from '@tanstack/angular-form'
@Component({
selector: 'app-root',
standalone: true,
imports: [TanStackField],
template: `
<form (submit)="handleSubmit($event)">
<div>
<ng-container
[tanstackField]="form"
name="firstName"
[validators]="{
onChange: firstNameValidator,
onChangeAsyncDebounceMs: 500,
onChangeAsync: firstNameAsyncValidator,
}"
#firstName="field"
>
<label [for]="firstName.api.name">First Name:</label>
<input
[id]="firstName.api.name"
[name]="firstName.api.name"
[value]="firstName.api.state.value"
(blur)="firstName.api.handleBlur()"
(input)="firstName.api.handleChange($any($event).target.value)"
/>
@if (firstName.api.state.meta.isTouched) {
@for (error of firstName.api.state.meta.errors; track $index) {
<div style="color: red">
{{ error }}
</div>
}
}
@if (firstName.api.state.meta.isValidating) {
<p>Validating...</p>
}
</ng-container>
</div>
<div>
<ng-container [tanstackField]="form" name="lastName" #lastName="field">
<label [for]="lastName.api.name">Last Name:</label>
<input
[id]="lastName.api.name"
[name]="lastName.api.name"
[value]="lastName.api.state.value"
(blur)="lastName.api.handleBlur()"
(input)="lastName.api.handleChange($any($event).target.value)"
/>
</ng-container>
</div>
<button type="submit" [disabled]="!canSubmit()">
{{ isSubmitting() ? '...' : 'Submit' }}
</button>
<button type="reset" (click)="form.reset()">Reset</button>
</form>
`,
})
export class AppComponent {
firstNameValidator: FieldValidateFn<any, string, any> = ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined
firstNameAsyncValidator: FieldValidateAsyncFn<any, string, any> = async ({
value,
}) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
}
form = injectForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit({ value }) {
// Do something with form data
console.log(value)
},
})
canSubmit = injectStore(this.form, (state) => state.canSubmit)
isSubmitting = injectStore(this.form, (state) => state.isSubmitting)
handleSubmit(event: SubmitEvent) {
event.preventDefault()
event.stopPropagation()
this.form.handleSubmit()
}
}
In the example below, you can see TanStack Form in action with the Solid framework adapter:
/* @refresh reload */
import { render } from 'solid-js/web'
import { createForm } from '@tanstack/solid-form'
import { FieldInfo } from './FieldInfo.tsx';
function App() {
const form = createForm(() => ({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
}))
return (
<div>
<h1>Simple Form Example</h1>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<div>
{/* A type-safe field component*/}
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return (
value.includes('error') && 'No "error" allowed in first name'
)
},
}}
children={(field) => {
// Avoid hasty abstractions. Render props are great!
return (
<>
<label for={field().name}>First Name:</label>
<input
id={field().name}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.target.value)}
/>
<FieldInfo field={field()} />
</>
)
}}
/>
</div>
<div>
<form.Field
name="lastName"
children={(field) => (
<>
<label for={field().name}>Last Name:</label>
<input
id={field().name}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.target.value)}
/>
<FieldInfo field={field()} />
</>
)}
/>
</div>
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
children={(state) => {
return (
<button type="submit" disabled={!state().canSubmit}>
{state().isSubmitting ? '...' : 'Submit'}
</button>
)
}}
/>
</form>
</div>
)
}
const root = document.getElementById('root')
render(() => <App />, root!)
/* @refresh reload */
import { render } from 'solid-js/web'
import { createForm } from '@tanstack/solid-form'
import { FieldInfo } from './FieldInfo.tsx';
function App() {
const form = createForm(() => ({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
}))
return (
<div>
<h1>Simple Form Example</h1>
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<div>
{/* A type-safe field component*/}
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return (
value.includes('error') && 'No "error" allowed in first name'
)
},
}}
children={(field) => {
// Avoid hasty abstractions. Render props are great!
return (
<>
<label for={field().name}>First Name:</label>
<input
id={field().name}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.target.value)}
/>
<FieldInfo field={field()} />
</>
)
}}
/>
</div>
<div>
<form.Field
name="lastName"
children={(field) => (
<>
<label for={field().name}>Last Name:</label>
<input
id={field().name}
name={field().name}
value={field().state.value}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.target.value)}
/>
<FieldInfo field={field()} />
</>
)}
/>
</div>
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
children={(state) => {
return (
<button type="submit" disabled={!state().canSubmit}>
{state().isSubmitting ? '...' : 'Submit'}
</button>
)
}}
/>
</form>
</div>
)
}
const root = document.getElementById('root')
render(() => <App />, root!)
import type { AnyFieldApi } from '@tanstack/solid-form'
export interface FieldInfoProps {
field: AnyFieldApi
}
export function FieldInfo(props: FieldInfoProps) {
return (
<>
{props.field.state.meta.isTouched && !props.field.state.meta.isValid ? (
<em>{props.field.state.meta.errors.join(',')}</em>
) : null}
{props.field.state.meta.isValidating ? 'Validating...' : null}
</>
)
}
import type { AnyFieldApi } from '@tanstack/solid-form'
export interface FieldInfoProps {
field: AnyFieldApi
}
export function FieldInfo(props: FieldInfoProps) {
return (
<>
{props.field.state.meta.isTouched && !props.field.state.meta.isValid ? (
<em>{props.field.state.meta.errors.join(',')}</em>
) : null}
{props.field.state.meta.isValidating ? 'Validating...' : null}
</>
)
}
In the example below, you can see TanStack Form in action with the Svelte framework adapter:
<script lang="ts">
import { createForm } from '@tanstack/svelte-form'
import FieldInfo from './FieldInfo.svelte'
const form = createForm(() => ({
defaultValues: {
firstName: '',
lastName: '',
employed: false,
jobTitle: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
alert(JSON.stringify(value))
},
}))
</script>
<form
id="form"
onsubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<h1>TanStack Form - Svelte Demo</h1>
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
value.length < 3 ? 'Not long enough' : undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
},
}}
>
{#snippet children(field)}
<div>
<label for={field.name}>First Name</label>
<input
id={field.name}
type="text"
placeholder="First Name"
value={field.state.value}
onblur={() => field.handleBlur()}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
<FieldInfo {field} />
</div>
{/snippet}
</form.Field>
<form.Field
name="lastName"
validators={{
onChange: ({ value }) =>
value.length < 3 ? 'Not long enough' : undefined,
}}
>
{#snippet children(field)}
<div>
<label for={field.name}>Last Name</label>
<input
id={field.name}
type="text"
placeholder="Last Name"
value={field.state.value}
onblur={() => field.handleBlur()}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
<FieldInfo {field} />
</div>
{/snippet}
</form.Field>
<form.Field name="employed">
{#snippet children(field)}
<div>
<label for={field.name}>Employed?</label>
<input
oninput={() => field.handleChange(!field.state.value)}
checked={field.state.value}
onblur={() => field.handleBlur()}
id={field.name}
type="checkbox"
/>
</div>
{#if field.state.value}
<form.Field
name="jobTitle"
validators={{
onChange: ({ value }) =>
value.length === 0 ? 'If you have a job, you need a title' : null,
}}
>
{#snippet children(field)}
<div>
<label for={field.name}>Job Title</label>
<input
type="text"
id={field.name}
placeholder="Job Title"
value={field.state.value}
onblur={field.handleBlur}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
<FieldInfo {field} />
</div>
{/snippet}
</form.Field>
{/if}
{/snippet}
</form.Field>
<div>
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
>
{#snippet children({ canSubmit, isSubmitting })}
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? 'Submitting' : 'Submit'}
</button>
{/snippet}
</form.Subscribe>
<button
type="button"
id="reset"
onclick={() => {
form.reset()
}}
>
Reset
</button>
</div>
</form>
<script lang="ts">
import { createForm } from '@tanstack/svelte-form'
import FieldInfo from './FieldInfo.svelte'
const form = createForm(() => ({
defaultValues: {
firstName: '',
lastName: '',
employed: false,
jobTitle: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
alert(JSON.stringify(value))
},
}))
</script>
<form
id="form"
onsubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<h1>TanStack Form - Svelte Demo</h1>
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
value.length < 3 ? 'Not long enough' : undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
},
}}
>
{#snippet children(field)}
<div>
<label for={field.name}>First Name</label>
<input
id={field.name}
type="text"
placeholder="First Name"
value={field.state.value}
onblur={() => field.handleBlur()}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
<FieldInfo {field} />
</div>
{/snippet}
</form.Field>
<form.Field
name="lastName"
validators={{
onChange: ({ value }) =>
value.length < 3 ? 'Not long enough' : undefined,
}}
>
{#snippet children(field)}
<div>
<label for={field.name}>Last Name</label>
<input
id={field.name}
type="text"
placeholder="Last Name"
value={field.state.value}
onblur={() => field.handleBlur()}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
<FieldInfo {field} />
</div>
{/snippet}
</form.Field>
<form.Field name="employed">
{#snippet children(field)}
<div>
<label for={field.name}>Employed?</label>
<input
oninput={() => field.handleChange(!field.state.value)}
checked={field.state.value}
onblur={() => field.handleBlur()}
id={field.name}
type="checkbox"
/>
</div>
{#if field.state.value}
<form.Field
name="jobTitle"
validators={{
onChange: ({ value }) =>
value.length === 0 ? 'If you have a job, you need a title' : null,
}}
>
{#snippet children(field)}
<div>
<label for={field.name}>Job Title</label>
<input
type="text"
id={field.name}
placeholder="Job Title"
value={field.state.value}
onblur={field.handleBlur}
oninput={(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}
/>
<FieldInfo {field} />
</div>
{/snippet}
</form.Field>
{/if}
{/snippet}
</form.Field>
<div>
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
>
{#snippet children({ canSubmit, isSubmitting })}
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? 'Submitting' : 'Submit'}
</button>
{/snippet}
</form.Subscribe>
<button
type="button"
id="reset"
onclick={() => {
form.reset()
}}
>
Reset
</button>
</div>
</form>
<script lang="ts">
import type { AnyFieldApi } from '@tanstack/svelte-form'
let { field }: { field: AnyFieldApi } = $props()
</script>
{#if field.state.meta.isTouched}
{#each field.state.meta.errors as error}
<em>{error}</em>
{/each}
{field.state.meta.isValidating ? 'Validating...' : ''}
{/if}
<script lang="ts">
import type { AnyFieldApi } from '@tanstack/svelte-form'
let { field }: { field: AnyFieldApi } = $props()
</script>
{#if field.state.meta.isTouched}
{#each field.state.meta.errors as error}
<em>{error}</em>
{/each}
{field.state.meta.isValidating ? 'Validating...' : ''}
{/if}
In the example below, you can see TanStack Form in action with the Lit framework adapter:
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
import { repeat } from 'lit/directives/repeat.js'
@customElement('tanstack-form-demo')
export class TanStackFormDemo extends LitElement {
#form = new TanStackFormController(this, {
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit({ value }) {
// Do something with form data
console.log(value)
},
})
render() {
return html`
<form
@submit=${(e: Event) => {
e.preventDefault()
e.stopPropagation()
this.#form.api.handleSubmit()
}}
>
${this.#form.field(
{
name: `firstName`,
validators: {
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return (
value.includes('error') &&
'No "error" allowed in first name'
)
},
},
},
(field) => {
return html` <div>
<label for="${field.name}">First Name:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}"
/>
${field.state.meta.isTouched && !field.state.meta.isValid
? html`${repeat(
field.state.meta.errors,
(__, idx) => idx,
(error) => {
return html`<div style="color: red;">${error}</div>`
},
)}`
: nothing}
${field.state.meta.isValidating
? html`<p>Validating...</p>`
: nothing}
</div>`
},
)}
</div>
<div>
${this.#form.field(
{
name: `lastName`,
},
(field) => {
return html` <div>
<label for="${field.name}">Last Name:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}"
/>
</div>`
},
)}
</div>
<button type="submit" ?disabled=${this.#form.api.state.isSubmitting}>
${this.#form.api.state.isSubmitting ? '...' : 'Submit'}
</button>
<button
type="button"
@click=${() => {
this.#form.api.reset()
}}
>
Reset
</button>
</form>
`
}
}
import { LitElement, html, nothing } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
import { repeat } from 'lit/directives/repeat.js'
@customElement('tanstack-form-demo')
export class TanStackFormDemo extends LitElement {
#form = new TanStackFormController(this, {
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit({ value }) {
// Do something with form data
console.log(value)
},
})
render() {
return html`
<form
@submit=${(e: Event) => {
e.preventDefault()
e.stopPropagation()
this.#form.api.handleSubmit()
}}
>
${this.#form.field(
{
name: `firstName`,
validators: {
onChange: ({ value }) =>
!value
? 'A first name is required'
: value.length < 3
? 'First name must be at least 3 characters'
: undefined,
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return (
value.includes('error') &&
'No "error" allowed in first name'
)
},
},
},
(field) => {
return html` <div>
<label for="${field.name}">First Name:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}"
/>
${field.state.meta.isTouched && !field.state.meta.isValid
? html`${repeat(
field.state.meta.errors,
(__, idx) => idx,
(error) => {
return html`<div style="color: red;">${error}</div>`
},
)}`
: nothing}
${field.state.meta.isValidating
? html`<p>Validating...</p>`
: nothing}
</div>`
},
)}
</div>
<div>
${this.#form.field(
{
name: `lastName`,
},
(field) => {
return html` <div>
<label for="${field.name}">Last Name:</label>
<input
id="${field.name}"
name="${field.name}"
.value="${field.state.value}"
@blur="${() => field.handleBlur()}"
@input="${(e: Event) => {
const target = e.target as HTMLInputElement
field.handleChange(target.value)
}}"
/>
</div>`
},
)}
</div>
<button type="submit" ?disabled=${this.#form.api.state.isSubmitting}>
${this.#form.api.state.isSubmitting ? '...' : 'Submit'}
</button>
<button
type="button"
@click=${() => {
this.#form.api.reset()
}}
>
Reset
</button>
</form>
`
}
}
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.
