Input components
To make your code more readable, we recommend that you develop your own input components if you are not using a prebuilt UI library. There you can encapsulate logic to display error messages, for example.
If you're already a bit more experienced, you can use the input components we developed for our playground as a starting point. You can find the code in our GitHub repository here.
Why input components?
Currently, your fields might look something like this:
<Field name="email" validate={…}>
{(field, props) => (
<div>
<label for={field.name}>Email</label>
<input
{...props}
id={field.name}
value={field.value}
type="email"
required
/>
{field.error && <div>{field.error}</div>}
</div>
)}
</Field>
If CSS and a few more functionalities are added here, the code quickly becomes confusing. In addition, you have to rewrite the same code for almost every form field.
Our goal is to develop a TextInput component so that the code ends up looking like this:
<Field name="email" validate={…}>
{(field, props) => (
<TextInput
{...props}
type="email"
label="Email"
value={field.value}
error={field.error}
required
/>
)}
</Field>
Create an input component
In the first step, you create a new file for the TextInput component and, if you use TypeScript, define its properties.
import { ReadonlySignal } from '@preact/signals';
import { JSX, Ref } from 'preact';
type TextInputProps = {
name: string;
type: 'text' | 'email' | 'tel' | 'password' | 'url' | 'date';
label?: string;
placeholder?: string;
value: ReadonlySignal<string | undefined>;
error: ReadonlySignal<string>;
required?: boolean;
onInput: JSX.GenericEventHandler<HTMLInputElement>;
onChange: JSX.GenericEventHandler<HTMLInputElement>;
onBlur: JSX.FocusEventHandler<HTMLInputElement>;
};
Component function
In the next step, add the component function to the file and destructure the properties of the HTML <input /> element from the rest. It is important that you use forwardRef to pass the element reference.
import { ReadonlySignal } from '@preact/signals';
import { JSX, Ref } from 'preact';
import { forwardRef } from 'preact/compat';
type TextInputProps = { … };
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, value, error, ...props }, ref) => {
const { name, required } = props;
}
);
JSX code
After that, next you can add the JSX code to the return statement.
import { ReadonlySignal, useComputed } from '@preact/signals';
import { JSX, Ref } from 'preact';
import { forwardRef } from 'preact/compat';
type TextInputProps = { … };
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, value, error, ...props }, ref) => {
const { name, required } = props;
return (
<div>
{label && (
<label for={name}>
{label} {required && <span>*</span>}
</label>
)}
<input
{...props}
ref={ref}
id={name}
value={useComputed(() => value.value || '')}
aria-invalid={!!error.value}
aria-errormessage={`${name}-error`}
/>
{error.value && <div id={`${name}-error`}>{error}</div>}
</div>
);
}
);
Next steps
You can now build on this code and add CSS, for example. You can also follow the procedure to create other components such as Checkbox, Slider, Select and FileInput.
Final code
Below is an overview of the entire code of the TextInput component.
import { ReadonlySignal, useComputed } from '@preact/signals';
import { JSX, Ref } from 'preact';
import { forwardRef } from 'preact/compat';
type TextInputProps = {
name: string;
type: 'text' | 'email' | 'tel' | 'password' | 'url' | 'date';
label?: string;
placeholder?: string;
value: ReadonlySignal<string | undefined>;
error: string;
required?: boolean;
ref: Ref<HTMLInputElement>;
onInput: JSX.GenericEventHandler<HTMLInputElement>;
onChange: JSX.GenericEventHandler<HTMLInputElement>;
onBlur: JSX.FocusEventHandler<HTMLInputElement>;
};
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, value, error, ...props }, ref) => {
const { name, required } = props;
return (
<div>
{label && (
<label for={name}>
{label} {required && <span>*</span>}
</label>
)}
<input
{...props}
ref={ref}
id={name}
value={useComputed(() => value.value || '')}
aria-invalid={!!error.value}
aria-errormessage={`${name}-error`}
/>
{error.value && <div id={`${name}-error`}>{error}</div>}
</div>
);
}
);