Form

import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage, useFormField } from '@hanzo/ui'

Default

One Input with all five pieces around it; `rules` says the field is required, so pressing Save with it empty puts the message under the field and marks the control invalid.

Shown in the sidebar and on every invite.

export function Default() {
const form = useForm({ defaultValues: { name: "" } })
const [saved, setSaved] = useState("")
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(({ name }) => setSaved(name))}>
<YStack gap="$4" width="100%" maxW={320}>
<FormField control={form.control} name="name" rules={{ required: "A workspace needs a name." }} render={({ field }) => ( <FormItem> <FormLabel>Workspace name</FormLabel> <FormControl> <Input placeholder="acme" {...field} /> </FormControl> <FormDescription> Shown in the sidebar and on every invite. </FormDescription> <FormMessage /> </FormItem> )} />
<XStack gap="$3" items="center">
<Button type="submit">Save</Button>
{saved ? (
<Paragraph color="$color11">Saved as {saved}</Paragraph>
) : null}
</XStack>
</YStack>
</form>
</Form>
)
}

Validation

`rules` carry each field's checks, `validate` can read the other fields, and `mode: 'onBlur'` runs them as you leave a field instead of waiting for the button.

Twelve characters or more; a sentence works well.

export function Validation() {
const form = useForm({
mode: "onBlur",
defaultValues: { password: "", confirm: "" },
})
const [changed, setChanged] = useState(false)
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(() => { form.reset() setChanged(true) })} >
<YStack gap="$4" width="100%" maxW={320}>
<FormField control={form.control} name="password" rules={{ required: "Enter a new password.", minLength: { value: 12, message: "Use at least 12 characters." }, }} render={({ field }) => ( <FormItem> <FormLabel>New password</FormLabel> <FormControl> <Input type="password" {...field} /> </FormControl> <FormDescription> Twelve characters or more; a sentence works well. </FormDescription> <FormMessage /> </FormItem> )} />
<FormField control={form.control} name="confirm" rules={{ validate: (value, { password }) => value === password || "The two passwords differ.", }} render={({ field }) => ( <FormItem> <FormLabel>Confirm password</FormLabel> <FormControl> <Input type="password" reveal={false} {...field} /> </FormControl> <FormMessage /> </FormItem> )} />
<XStack gap="$3" items="center">
<Button type="submit">Change password</Button>
{changed ? (
<Paragraph color="$color11">Password changed.</Paragraph>
) : null}
</XStack>
</YStack>
</form>
</Form>
)
}

Controls

A Select takes `value` and `onValueChange` from the field and a Switch or Checkbox takes `checked` and `onCheckedChange`; `FormControl` wraps whatever takes focus, which for a Select is its trigger.

Where the database and its backups live.

export function Controls() {
const regions: Record<string, string> = {
nyc1: "New York",
fra1: "Frankfurt",
sgp1: "Singapore",
}
const form = useForm({
defaultValues: { region: "fra1", backups: true, billing: false },
})
const [created, setCreated] = useState("")
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(({ region, backups }) => setCreated( `${regions[region]}${backups ? ", backed up nightly" : ""}` ) )} >
<YStack gap="$4" width="100%" maxW={320}>
<FormField control={form.control} name="region" render={({ field }) => ( <FormItem> <FormLabel>Region</FormLabel> <Select value={field.value} onValueChange={field.onChange} renderValue={(v) => regions[v]} > <FormControl> <SelectTrigger> <SelectValue /> </SelectTrigger> </FormControl> <SelectContent> {Object.entries(regions).map(([value, label]) => ( <SelectItem key={value} value={value}> {label} </SelectItem> ))} </SelectContent> </Select> <FormDescription> Where the database and its backups live. </FormDescription> </FormItem> )} />
<FormField control={form.control} name="backups" render={({ field }) => ( <FormItem> <XStack items="center" justify="space-between"> <FormLabel>Nightly backups</FormLabel> <FormControl> <Switch checked={field.value} onCheckedChange={field.onChange} /> </FormControl> </XStack> </FormItem> )} />
<FormField control={form.control} name="billing" rules={{ required: "Confirm the charge to continue." }} render={({ field }) => ( <FormItem> <XStack gap="$2" items="center"> <FormControl> <Checkbox checked={field.value} onCheckedChange={(next) => field.onChange(next === true)} /> </FormControl> <FormLabel>Bill $15 a month to this workspace</FormLabel> </XStack> <FormMessage /> </FormItem> )} />
<XStack gap="$3" items="center">
<Button type="submit">Create database</Button>
{created ? (
<Paragraph color="$color11">Created in {created}.</Paragraph>
) : null}
</XStack>
</YStack>
</form>
</Form>
)
}

Own piece

Anything inside a field can call `useFormField()` for that field's ids and state; `Unsaved` is that call and a Text shown while `isDirty`, and Save stays off until some field differs from what was loaded.

export function OwnPiece() {
const form = useForm({
defaultValues: {
name: "Ada Lovelace",
bio: "Wrote the first program for a machine that did not exist yet.",
},
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit((values) => form.reset(values))}>
<YStack gap="$4" width="100%" maxW={320}>
<FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <XStack gap="$2" items="center"> <FormLabel>Display name</FormLabel> <Unsaved /> </XStack> <FormControl> <Input {...field} /> </FormControl> </FormItem> )} />
<FormField control={form.control} name="bio" render={({ field }) => ( <FormItem> <XStack gap="$2" items="center"> <FormLabel>Bio</FormLabel> <Unsaved /> </XStack> <FormControl> <Textarea rows={2} {...field} /> </FormControl> </FormItem> )} />
<XStack>
<Button type="submit" disabled={!form.formState.isDirty}>
Save
</Button>
</XStack>
</YStack>
</form>
</Form>
)
}

Types

This module declares no types of its own.

Exports

FormFormFieldFormItemFormLabelFormControlFormDescriptionFormMessageuseFormField