# Conditional Fields

A field's meaning can depend on a sibling. An attribute editor's "default value"
means something different depending on the selected "type": free text for
`string`, a `<select>` populated from an `items` array for `enum`. Kin Form
doesn't need a separate mechanism for this - it's one field, discriminated by
another field's current value, combining
[Linked Fields](/form/guide/linked-fields) (`dependents`),
[Listeners](/form/guide/listeners) (`onValueChanged`), and
[Per-node Validation](/form/guide/per-node-validation).

```ts
type Attribute = {
  name: string;
  type: "string" | "enum";
  defaultValue: string;
  items: { code: string; value: string }[];
};
```

## One field, several widgets

Plain conditional rendering picks the widget; the underlying field stays the
same `FieldApi` across every branch:

<CodeGroup>

<CodeGroupItem label="React">

```tsx
function DefaultValueField(
  { typeApi, defaultValueApi, itemsApi }: {
    typeApi: FieldApi<Attribute["type"], Attribute>;
    defaultValueApi: FieldApi<Attribute["defaultValue"], Attribute>;
    itemsApi: FieldApi<Attribute["items"], Attribute>;
  },
) {
  const type = useWatch(typeApi, (f) => f.value);
  const items = useWatch(itemsApi, (f) => f.value);

  return type === "enum"
    ? (
      <SelectField api={defaultValueApi}>
        <option value="">Select an option…</option>
        {items.map((item) => (
          <option key={item.code} value={item.code}>
            {item.value || item.code}
          </option>
        ))}
      </SelectField>
    )
    : <TextField api={defaultValueApi} />;
}
```

</CodeGroupItem>

<CodeGroupItem label="Lit">

```ts
function renderDefaultValue(
  typeApi: FieldApi<Attribute["type"], Attribute>,
  defaultValueApi: FieldApi<Attribute["defaultValue"], Attribute>,
  itemsApi: FieldApi<Attribute["items"], Attribute>,
): unknown {
  return watch(
    typeApi.parent!,
    () => [typeApi.value, itemsApi.value] as const,
    (_group, [type, items]) =>
      type === "enum"
        ? html`
          <select-field .api=${defaultValueApi}>
            <option value="">Select an option…</option>
            ${items.map((item) =>
              html`<option value=${item.code}>${
                item.value || item.code
              }</option>`
            )}
          </select-field>
        `
        : html`<text-field .api=${defaultValueApi}></text-field>`,
  );
}
```

</CodeGroupItem>

</CodeGroup>

Reusing one `FieldApi` across branches keeps the submitted shape one plain
`defaultValue`, instead of a separate field per type. `type`/`defaultValue`
share `string` here, so no cast is needed; a `type` with more variants (say,
adding `number`/`boolean`) still shares one field, at the cost of a cast where a
branch's widget needs a narrower value type than `defaultValue`'s own - see the
live example below for that case.

## Validating per selected type

A per-node validator reads the sibling's current value directly and branches on
it, the same way any [cross-field validator](/form/guide/linked-fields) does:

```ts
form.field("defaultValue", {
  // Fixed once, at creation - not swapped out per type on every render.
  validators: (field) => {
    switch (field.parent!.value.type) {
      case "string":
        return field.value ? null : "Default value is required";
      case "enum":
        return field.value ? null : "Pick a default option";
    }
  },
});
```

## Resetting and revalidating on switch

`onValueChanged` clears the stale `defaultValue`/`items` the moment `type`
changes; `dependents` makes that reset take effect immediately instead of
waiting for the user to touch `defaultValue` themselves:

```ts
form.field("type", {
  // Revalidates defaultValue right after the reset below, so a freshly
  // switched "enum" with nothing picked is already known invalid.
  dependents: ["defaultValue"],
  onValueChanged: (field) => {
    const defaultValueField = field.parent!.field("defaultValue");
    defaultValueField.value = "";
    defaultValueField.touched = false; // don't show the old type's error

    // Same reason: an old enum option list shouldn't resurface later.
    const itemsField = field.parent!.field("items");
    itemsField.value = [];
    itemsField.touched = false;
  },
});
```

## Fields that exist in only one branch

A field registered for a hidden branch still counts. Its validators keep
running, so a `required()` on it blocks submit while the user can't even see the
field. Nothing unregisters a field when its widget unmounts, so do it when the
switch happens. Create the field inline where it's rendered, so it only exists
while shown, and drop it in `onValueChanged`:

```ts
const typeField = form.field("type", {
  onValueChanged: () => {
    // Drops the company field, validator included, and clears its value.
    form.unregisterField("company", { resetValue: true });
  },
});

// Where the "business" branch renders it:
form.field("company", { validators: required("Company is required") });
```

`resetValue` restores the value to its initial value. Without it,
`unregisterField` leaves the value alone. It does nothing when no field is
registered there, so it's safe to call on a switch that never showed the field.
Switching back registers a fresh field, untouched and with no stale error.

## Hidden branches don't lose data on their own

Nothing clears a field just because its widget unmounted - that's why `items`
needed the explicit reset above; left out, switching away from `"enum"` and back
would show the old options again. Whether hidden data survives a switch is the
app's call, made with that one `onValueChanged` line. See
[vs. React Hook Form](/form/comparison/react-hook-form#conditional-fields) and
[vs. TanStack Form](/form/comparison/tanstack-form#conditional-fields) for how
this compares to each.

The live example adds `number`/`boolean` on top of `string`/`enum` shown here:

<PlaygroundButton variant="text" label="Try it live on StackBlitz">
  <PlaygroundLink
    name="react"
    href="https://stackblitz.com/github/kintools-dev/form/tree/main/examples/react?file=src/examples/conditional-fields/App.tsx"
  />
  <PlaygroundLink
    name="lit"
    href="https://stackblitz.com/github/kintools-dev/form/tree/main/examples/lit?file=src/examples/conditional-fields/App.ts"
  />
</PlaygroundButton>
