Controlled fields

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, 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.

Text input example

For a text input field you simply add the value attribute and pass the value of the field or in case of undefined an empty string:

<Field of={loginForm} path={['firstName']}>
  {(field) => (
    <input
      {...field.props}
      type="text"
      // Pass value or empty string
      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 component from our playground.

Numbers and dates

To make the fields of numbers and dates controlled, further steps are required, because the <input /> element natively understands only strings as value.

Number input example

Since not every input into an <input type="number" /> field is a valid number, for example when typing floating numbers, the value may be NaN in between. You have to catch this case, otherwise the whole input will be removed when NaN is passed. It is best to encapsulate this logic in a separate component as described in the input components guide.

import { createMemo, JSX, splitProps } from 'solid-js';

type NumberInputProps = {
  name: string;
  type: 'number';
  label?: string;
  placeholder?: string;
  input: number | undefined;
  errors: [string, ...string[]] | null;
  required?: boolean;
  ref: (element: HTMLInputElement) => void;
  onFocus: JSX.EventHandler<HTMLInputElement, FocusEvent>;
  onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
  onChange: JSX.EventHandler<HTMLInputElement, Event>;
  onBlur: JSX.EventHandler<HTMLInputElement, FocusEvent>;
};

export function NumberInput(props: NumberInputProps) {
  const [, inputProps] = splitProps(props, ['input', 'label', 'errors']);

  // Update memo if value is not `NaN`
  const getValue = createMemo<number | undefined>(
    (prevValue) => (!Number.isNaN(props.input) ? props.input : prevValue),
    undefined
  );

  return (
    <div>
      {props.label && <label for={props.name}>{props.label}</label>}
      <input
        {...inputProps}
        id={props.name}
        value={getValue() ?? ''}
        aria-invalid={!!props.errors}
        aria-errormessage={`${props.name}-error`}
      />
      {props.errors && <div id={`${props.name}-error`}>{props.errors[0]}</div>}
    </div>
  );
}

Date input example

A date or a number representing a date must be converted to a string before it can be passed to an <input type="date" /> field. Since it is a calculated value, you can use createMemo for this.

import { createMemo, JSX, splitProps } from 'solid-js';

type DateInputProps = {
  name: string;
  type: 'date';
  label?: string;
  placeholder?: string;
  input: Date | number | undefined;
  errors: [string, ...string[]] | null;
  required?: boolean;
  ref: (element: HTMLInputElement) => void;
  onFocus: JSX.EventHandler<HTMLInputElement, FocusEvent>;
  onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
  onChange: JSX.EventHandler<HTMLInputElement, Event>;
  onBlur: JSX.EventHandler<HTMLInputElement, FocusEvent>;
};

export function DateInput(props: DateInputProps) {
  const [, inputProps] = splitProps(props, ['input', 'label', 'errors']);

  // Transform date or number to string
  const getValue = createMemo(() =>
    props.input &&
    !Number.isNaN(
      typeof props.input === 'number' ? props.input : props.input.getTime()
    )
      ? new Date(props.input).toISOString().split('T', 1)[0]
      : ''
  );

  return (
    <div>
      {props.label && <label for={props.name}>{props.name}</label>}
      <input
        {...inputProps}
        id={props.name}
        value={getValue()}
        aria-invalid={!!props.errors}
        aria-errormessage={`${props.name}-error`}
      />
      {props.errors && <div id={`${props.name}-error`}>{props.errors[0]}</div>}
    </div>
  );
}

Next steps

Now that you understand controlled fields, you can explore more advanced topics like nested fields and field arrays to handle complex form structures.

Contributors

Thanks to all the contributors who helped make this page better!

  • GitHub profile picture of @fabian-hiller

Partners

Thanks to our partners who support the project ideally and financially.

Sponsors

Thanks to our GitHub sponsors who support the project financially.

  • GitHub profile picture of @vasilii-kovalev
  • GitHub profile picture of @saturnonearth
  • GitHub profile picture of @ruiaraujo012
  • GitHub profile picture of @hyunbinseo
  • GitHub profile picture of @nickytonline
  • GitHub profile picture of @KubaJastrz
  • GitHub profile picture of @andrewmd5
  • GitHub profile picture of @Thanaen
  • GitHub profile picture of @caegdeveloper
  • GitHub profile picture of @bmoyroud
  • GitHub profile picture of @dslatkin