Domphy

Listeners & Side Effects

onChangeListenTo — linked field validation

When field B's validation depends on field A's value, use onChangeListenTo to re-run B's validators whenever A changes:

const form = createForm<{ password: string; confirmPassword: string }>({
  defaultValues: { password: "", confirmPassword: "" },
  onSubmit: ({ value }) => submit(value),
})

const passwordField = form.field<string>("password", {})

const confirmField = form.field<string>("confirmPassword", {
  validators: {
    onChange: ({ value, fieldApi }) => {
      const password = fieldApi.form.getFieldValue("password")
      return value === password ? undefined : "Passwords do not match"
    },
  },
  onChangeListenTo: ["password"],   // re-validate when "password" changes
})

Without onChangeListenTo, the confirmPassword validator only runs when the user types in confirmPassword — not when they update password.

Form-level onChange listener

React to any value change at the form level:

const form = createForm<PriceInput>({
  defaultValues: { quantity: 1, unitPrice: 10, total: 10 },
  onSubmit: ({ value }) => checkout(value),
  onChange: ({ value }) => {
    // Compute derived fields
    const total = value.quantity * value.unitPrice
    form.form.setFieldValue("total", total, { touch: false })
  },
})

Watching field values with effect

For side effects outside the form (e.g. showing a live preview, making an API call), use @domphy/core's effect:

import { effect } from "@domphy/core"
import { toState } from "@domphy/core"

const form = createForm<SearchInput>({
  defaultValues: { query: "", filters: [] },
  onSubmit: () => {},
})

const queryField = form.field<string>("query", {})
const results = toState<SearchResult[]>([])

// Trigger search whenever query changes (with debounce)
effect(() => {
  const query = queryField.value()   // reactive read
  if (!query) { results.set([]); return }

  let cancelled = false
  const timer = setTimeout(async () => {
    const data = await searchApi(query)
    if (!cancelled) results.set(data)
  }, 300)

  return () => {
    cancelled = true
    clearTimeout(timer)
  }
})

onBlurListenTo — cross-field blur validation

Re-run a field's blur validators when another field blurs:

const emailField = form.field<string>("email", {
  validators: {
    onBlur: ({ value }) => validateEmail(value),
  },
  onBlurListenTo: ["username"],   // also re-validate when "username" blurs
})

Dependent fields — show/hide based on another field

import { toState } from "@domphy/core"

const form = createForm<ShippingInput>({
  defaultValues: { method: "standard", expediteReason: "" },
  onSubmit: ({ value }) => submit(value),
})

const methodField = form.field<string>("method", {})
const reasonField = form.field<string>("expediteReason", {
  validators: {
    onSubmit: ({ value, fieldApi }) => {
      if (fieldApi.form.getFieldValue("method") === "express" && !value) {
        return "Please provide a reason for express shipping"
      }
    },
  },
  onChangeListenTo: ["method"],
})

const ShippingForm = {
  form: [
    ShippingMethodSelect,
    // Only show reason field when "express" is selected
    {
      div: ReasonInput,
      hidden: (l) => methodField.value(l) !== "express",
    },
  ],
  onSubmit: (e) => { e.preventDefault(); form.handleSubmit() },
}

Computed derived values

Keep derived values in sync with form state using computed:

import { computed } from "@domphy/core"

const quantityField = form.field<number>("quantity", {})
const priceField = form.field<number>("price", {})

const total = computed((l) => {
  const qty = quantityField.value(l) ?? 0
  const price = priceField.value(l) ?? 0
  return qty * price
})

const TotalDisplay = {
  div: (l) => `Total: $${total.get(l).toFixed(2)}`,
}

Subscription to raw FormApi

For complete control, subscribe to the underlying FormApi state:

form.form.subscribe(
  (state) => state.values,           // selector — only re-fires when values change
  (values) => {
    console.log("Form values changed:", values)
    autosave(values)
  }
)

Field-level subscription

const emailApi = emailField.api

emailApi.store.subscribe(() => {
  const { value, meta } = emailApi.state
  console.log(`email: ${value}, errors: ${meta.errors}`)
})