# Controlled fields

> This document is the Markdown version of [formisch.dev/honox/guides/controlled-fields/](https://formisch.dev/honox/guides/controlled-fields/). For the complete documentation index, see [llms.txt](https://formisch.dev/llms.txt).

By default, all form fields are uncontrolled because that's the default behavior of the browser. For a simple login or contact form this is quite sufficient.

## Why controlled?

As soon as your forms become more complex, for example you set initial values or change the values of a form field via [`setInput`](/methods/api/setInput.md), it becomes necessary that you control your fields yourself. For example, depending on which HTML form field you use, you may need to set the `value`, `checked` or `selected` attributes. Since HonoX renders your islands on the server, this also ensures that initial values are already included in the server-rendered HTML.

### Text input example

For a text input field you simply add the `value` attribute and pass the field's input value:

```tsx
<Field of={loginForm} path={['firstName']}>
  {(field) => <input {...field.props} type="text" value={field.input} />}
</Field>
```

### Exception for files

The HTML `<input type="file" />` element is an exception because it cannot be controlled. However, you have the possibility to control the UI around it. For inspiration you can use the code of our [`FileInput`](https://github.com/open-circle/formisch/blob/main/playgrounds/honox/app/components/FileInput.tsx) component from our [playground](/playground/special/).

## Numbers and dates

Fields defined with a `v.number()` or `v.date()` schema need extra steps to be controlled, because the `<input />` element natively understands only strings as value.

### Number input example

An `<input type="number" />` only reads and writes strings. So if you spread `field.props` onto it, `field.input` holds a string like `"123"`, even though it is typed as `number`. To store a real number, override the change handler with one that converts the value via `valueAsNumber` and forwards it to `field.onChange`.

You also have to handle `NaN`. While typing a floating point number, the value can briefly be `NaN`, for example right after typing `1.`. If you don't catch this for the displayed value, the input is cleared. It is best to encapsulate both in a separate component as described in the [input components](/honox/guides/input-components.md) guide.

```tsx
import type { FieldElementProps } from '@formisch/honox';
import { useMemo, useRef } from 'hono/jsx';

interface NumberInputProps extends Omit<FieldElementProps, 'onChange'> {
  label?: string;
  placeholder?: string;
  input: number | undefined;
  errors: [string, ...string[]] | null;
  required?: boolean;
  onChange: (value: number | undefined) => void;
}

export function NumberInput({
  label,
  input,
  errors,
  name,
  onChange,
  ...inputProps
}: NumberInputProps) {
  const prevValue = useRef<number | undefined>(undefined);

  // Get value, keeping previous value if NaN
  const value = useMemo(() => {
    if (typeof input === 'number' && !Number.isNaN(input)) {
      prevValue.current = input;
      return input;
    }
    return prevValue.current;
  }, [input]);

  return (
    <div>
      {label && <label for={name}>{label}</label>}
      <input
        {...inputProps}
        name={name}
        id={name}
        type="number"
        value={value ?? ''}
        // Convert string to number before storing
        onChange={(event) => {
          const element = event.currentTarget as HTMLInputElement;
          onChange(element.value === '' ? undefined : element.valueAsNumber);
        }}
        aria-invalid={!!errors}
        aria-errormessage={`${name}-error`}
      />
      {errors && <div id={`${name}-error`}>{errors[0]}</div>}
    </div>
  );
}
```

Pass `field.onChange` after spreading `field.props` so it overrides the native handler.

```tsx
<Field of={form} path={['age']}>
  {(field) => (
    <NumberInput
      {...field.props}
      label="Age"
      input={field.input}
      errors={field.errors}
      onChange={field.onChange}
    />
  )}
</Field>
```

### Date input example

The same applies to dates. An `<input type="date" />` works with `yyyy-mm-dd` strings, while you usually want to store a `Date`. So you convert in both directions: format the `Date` for display, and parse the entered string back with `valueAsDate` before storing it.

```tsx
import type { FieldElementProps } from '@formisch/honox';
import { useMemo } from 'hono/jsx';

interface DateInputProps extends Omit<FieldElementProps, 'onChange'> {
  label?: string;
  placeholder?: string;
  input: Date | undefined;
  errors: [string, ...string[]] | null;
  required?: boolean;
  onChange: (value: Date | undefined) => void;
}

export function DateInput({
  label,
  input,
  errors,
  name,
  onChange,
  ...inputProps
}: DateInputProps) {
  // Transform date to string
  const value = useMemo(
    () =>
      input && !Number.isNaN(input.getTime())
        ? input.toISOString().split('T', 1)[0]
        : '',
    [input]
  );

  return (
    <div>
      {label && <label for={name}>{label}</label>}
      <input
        {...inputProps}
        name={name}
        id={name}
        type="date"
        value={value}
        // Convert string to date before storing
        onChange={(event) =>
          onChange(
            (event.currentTarget as HTMLInputElement).valueAsDate ?? undefined
          )
        }
        aria-invalid={!!errors}
        aria-errormessage={`${name}-error`}
      />
      {errors && <div id={`${name}-error`}>{errors[0]}</div>}
    </div>
  );
}
```

As with the number input, pass `field.onChange` after the spread to override the native handler.

```tsx
<Field of={form} path={['birthday']}>
  {(field) => (
    <DateInput
      {...field.props}
      label="Birthday"
      input={field.input}
      errors={field.errors}
      onChange={field.onChange}
    />
  )}
</Field>
```

## Custom inputs and component libraries

Some component libraries don't expose the underlying native HTML element, which means you cannot spread `field.props` onto them. For these cases, use `field.onChange` to set the value programmatically.

```tsx
import { DatePicker } from 'some-component-library';

<Field of={form} path={['date']}>
  {(field) => (
    <DatePicker
      value={field.input}
      onChange={(newDate) => field.onChange(newDate)}
    />
  )}
</Field>;
```

This is useful for:

- **Component libraries** that wrap native elements without exposing them
- **Complex custom inputs** like date pickers, rich text editors, or color pickers

The `field.onChange` method updates the field value and triggers validation, just like a native input would.

## Next steps

Now that you understand controlled fields, you can explore more advanced topics like [nested fields](/honox/guides/nested-fields.md) and [field arrays](/honox/guides/field-arrays.md) to handle complex form structures.
