Skip to main content
The TanStackFormController class is a Reactive Controller that manages forms in Lit applications using TanStack Form.

Import

import { TanStackFormController } from '@tanstack/lit-form'

Constructor

class TanStackFormController<
  TParentData,
  TFormOnMount extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChange extends undefined | FormValidateOrFn<TParentData>,
  TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnBlur extends undefined | FormValidateOrFn<TParentData>,
  TFormOnBlurAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnSubmit extends undefined | FormValidateOrFn<TParentData>,
  TFormOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnDynamic extends undefined | FormValidateOrFn<TParentData>,
  TFormOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TParentData>,
  TFormOnServer extends undefined | FormAsyncValidateOrFn<TParentData>,
  TSubmitMeta,
> implements ReactiveController {
  constructor(
    host: ReactiveControllerHost,
    config?: FormOptions<
      TParentData,
      TFormOnMount,
      TFormOnChange,
      TFormOnChangeAsync,
      TFormOnBlur,
      TFormOnBlurAsync,
      TFormOnSubmit,
      TFormOnSubmitAsync,
      TFormOnDynamic,
      TFormOnDynamicAsync,
      TFormOnServer,
      TSubmitMeta
    >,
  )
}

Constructor Parameters

host
ReactiveControllerHost
required
The Lit element that will host this controller. Typically this when instantiating in a Lit element.
config
FormOptions<TFormData>
Optional configuration object for the form.
config.defaultValues
TFormData
Initial values for the form fields.
config.onSubmit
(values: FormSubmitData<TFormData>) => void | Promise<void>
Callback function called when the form is submitted.
config.validators
FormValidators<TFormData>
Validation functions for the form.
config.validators.onChange
FormValidateOrFn<TFormData>
Validator that runs on every change.
config.validators.onChangeAsync
FormAsyncValidateOrFn<TFormData>
Async validator that runs on change with debouncing.
config.validators.onBlur
FormValidateOrFn<TFormData>
Validator that runs when the form loses focus.
config.validators.onMount
FormValidateOrFn<TFormData>
Validator that runs when the form is mounted.

Properties

api
FormApi<TFormData>
The form API instance.
api.handleSubmit
() => void
Function to trigger form submission.
api.reset
() => void
Function to reset the form to its initial state.
api.state
FormState<TFormData>
Current state of the form including values, errors, and validation status.
api.store
Store<FormState<TFormData>>
The underlying store for the form state.

Methods

field
<TName, TData, ...>(fieldConfig, render) => Directive
Creates a field directive for rendering form fields.
fieldConfig
FieldOptions<TParentData, TName, TData>
required
Configuration for the field.
fieldConfig.name
TName
required
The field name as a path string.
fieldConfig.defaultValue
TData
The default value for the field.
fieldConfig.validators
FieldValidators<TParentData, TName, TData>
Validation functions for the field.
render
(field: FieldApi<...>) => unknown
required
A render callback that receives the field API and returns Lit template content.
hostConnected
() => void
Lifecycle method called when the host element is connected. Subscribes to form state updates.
hostDisconnected
() => void
Lifecycle method called when the host element is disconnected. Unsubscribes from form state updates.

Usage Example

Basic Form

import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'

interface FormData {
  firstName: string
  lastName: string
}

@customElement('my-form')
export class MyForm extends LitElement {
  #form = new TanStackFormController<FormData>(this, {
    defaultValues: {
      firstName: '',
      lastName: '',
    },
    onSubmit({ value }) {
      console.log('Form submitted:', 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,
            },
          },
          (field) => 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.errors.length > 0
                ? html`<div style="color: red;">${field.state.meta.errors[0]}</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>
    `
  }
}

Form with Async Validation

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('async-form')
export class AsyncForm extends LitElement {
  #form = new TanStackFormController(this, {
    defaultValues: {
      firstName: '',
    },
    onSubmit({ value }) {
      console.log(value)
    },
  })

  render() {
    return html`
      <form @submit=${(e: Event) => {
        e.preventDefault()
        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) => 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) => html`<div style="color: red;">${error}</div>`,
                  )}`
                : nothing}
              ${field.state.meta.isValidating
                ? html`<p>Validating...</p>`
                : nothing}
            </div>
          `,
        )}
        <button type="submit">Submit</button>
      </form>
    `
  }
}

Array Fields

import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { TanStackFormController } from '@tanstack/lit-form'
import { repeat } from 'lit/directives/repeat.js'

interface Item {
  name: string
}

@customElement('array-form')
export class ArrayForm extends LitElement {
  #form = new TanStackFormController<{ items: Item[] }>(this, {
    defaultValues: {
      items: [],
    },
    onSubmit({ value }) {
      console.log(value)
    },
  })

  render() {
    return html`
      <form>
        ${this.#form.field(
          { name: 'items' },
          (field) => html`
            <div>
              ${repeat(
                field.state.value,
                (__, idx) => idx,
                (__, idx) => html`
                  <div>
                    ${this.#form.field(
                      { name: `items[${idx}].name` },
                      (subField) => html`
                        <input
                          .value="${subField.state.value}"
                          @input="${(e: Event) => {
                            const target = e.target as HTMLInputElement
                            subField.handleChange(target.value)
                          }}"
                        />
                      `,
                    )}
                    <button
                      @click=${() => field.removeValue(idx)}
                      type="button"
                    >
                      Remove
                    </button>
                  </div>
                `,
              )}
              <button
                @click=${() => field.pushValue({ name: '' })}
                type="button"
              >
                Add Item
              </button>
            </div>
          `,
        )}
      </form>
    `
  }
}

See Also

Build docs developers (and LLMs) love