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