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>
);
}
);