Controls
Formula includes wrapper components like <Input> which you’re expected to use in place of
their native equivalents such as <input>.
Each Formula control accepts a FormField prop called field, which points to the slice of
the form data that should be wired to the control.
import { Input, useForm } from "@michaelboyles/formula";
export function LoginForm() { const form = useForm({ initialValues: { username: "", password: "" } }); return ( <form onSubmit={form.submit}> Username: <Input field={form("username")} /> Password: <Input field={form("password")} type="password" /> </form> )}Built-in controls
Section titled “Built-in controls”| Control | Native control | Notes |
|---|---|---|
| Checkbox | <input type="checkbox"> | Suitable for boolean values |
| Input | <input> | Suitable for string values: text, password, etc. |
| FileInput | <input type="file"> | |
| IntegerInput | <input type="number"> | Numeric input that only permits integers |
| NumberInput | <input type="number"> | Numeric input that permits integers and decimals |
| RadioButton | <input type="radio"> | |
| Select | <select> | |
| TextArea | <textarea> |
Demystifying controls
Section titled “Demystifying controls”There’s nothing magical about Formula’s controls. In fact, you could write your code without them, it would just be more verbose and re-render more often.
Here’s an example of how you could write the same form as above without using <Input>:
import { useFieldData, useForm } from "@michaelboyles/formula";
export function LoginForm() { const form = useForm({ initialValues: { username: "", password: "" } }); const usernameField = form("username"); const username = useFieldData(usernameField); const passwordField = form("password"); const password = useFieldData(passwordField); return ( <form onSubmit={form.submit}> Username: <input value={username} onChange={e => { usernameField.setData(e.target.value); usernameField.setIsChanged(true); }} onBlur={() => usernameField.setIsBlurred(true)} /> Password: <input type="password" value={password} onChange={e => { passwordField.setData(e.target.value); passwordField.setIsChanged(true); }} onBlur={() => passwordField.setIsBlurred(true)} /> </form> )}There are a few issues with the code above. Firstly, the wiring for the change and blur listeners is repeated for both inputs.
Secondly, the use of useFieldData at the form level means that typing in either input
will re-render the entire form. That’s not likely to be a problem in this example, but the re-rendering could be isolated
to only a single input.
<Input> and the other built-in controls solve both of these problems, by moving the useFieldData hook further down
the tree, and automatically applying onChange and onBlur listeners.
Writing your own controls
Section titled “Writing your own controls”There will be cases where the functionality of the built-in controls is too limited. They’re provided for convenience and aren’t expected to cover every use case. You can always write your own controls.
A basic control will:
- Accept a
FormFieldprop. Unless there’s a good reason not to, name it ‘field’ for consistency with Formula’s controls - Call
useFieldDatato subscribe to updates to the value - Call
setDatawhen the value changes. If no options are provided (2nd parameter), this will automatically mark the field as having changed - Call
setIsBlurredwhen the field is blurred (optional: blur may not be relevant to all fields)
Here’s a simple example of a custom control:
import { type FormField, useFieldData, useForm } from "@michaelboyles/formula";
export type ToggleProps = { field: FormField<boolean>}export function Toggle({ field }: ToggleProps) { const isOn = useFieldData(field); return ( <button onClick={() => field.setData(prev => !prev)} > { isOn ? "ON" : "OFF" } </button> )}The list above is only the minimum. A more complex custom control may go further. For example, you might use
useFieldErrors to add an error indicator to the field.