Files
simple-react-app-kit/packages/ui/docs/en-US/examples/composition.mdx
T
2026-09-21 17:27:45 +08:00

99 lines
3.4 KiB
Plaintext

---
title: Compose a profile form
description: Combine Field, Input, Select, and Button into an accessible form while keeping state in the application.
order: 30
toc:
- id: complete-example
title: Complete example
- id: validation
title: Validation state
- id: controlled-select
title: Controlled Select
- id: styling
title: Style overrides
---
## Complete example {#complete-example}
```tsx
import * as React from "react"
import { Button } from "@workspace/ui/components/button"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@workspace/ui/components/field"
import { Input } from "@workspace/ui/components/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@workspace/ui/components/select"
export function ProfileForm() {
const [role, setRole] = React.useState("editor")
const [error, setError] = React.useState("")
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const data = new FormData(event.currentTarget)
setError(data.get("displayName") ? "" : "Enter a display name")
}
return (
<form onSubmit={submit} className="max-w-md space-y-6">
<FieldGroup>
<Field data-invalid={Boolean(error)}>
<FieldLabel htmlFor="display-name">Display name</FieldLabel>
<Input
id="display-name"
name="displayName"
aria-invalid={Boolean(error)}
aria-describedby="display-name-error"
/>
<FieldDescription>
This name is visible to other members.
</FieldDescription>
<FieldError id="display-name-error">{error}</FieldError>
</Field>
<Field>
<FieldLabel htmlFor="role">Role</FieldLabel>
<Select
value={role}
onValueChange={(value) => setRole(value ?? "editor")}
>
<SelectTrigger id="role">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Administrator</SelectItem>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<Button type="submit">Save profile</Button>
</form>
)
}
```
## Validation state {#validation}
The UI package does not select a validation library. The application produces errors and connects the same state to `Field data-invalid`, the control's `aria-invalid`, and `FieldError`. Multiple schema errors can be passed through `FieldError errors={errors}`; the component deduplicates them before rendering a list.
## Controlled Select {#controlled-select}
The local Select is built on Base UI and uses `value` with `onValueChange`. `SelectTrigger` renders its own arrow, so it only needs `SelectValue`; do not add another chevron. The popup aligns to the trigger and adopts its width by default. Adjust `side`, `align`, `sideOffset`, and `alignItemWithTrigger` on `SelectContent` when required.
## Style overrides {#styling}
Use the existing `variant`, `size`, and structural parts first, then apply page layout through `className`. Components merge classes with `cn`, allowing consumer Tailwind classes to override default utilities. Promote overrides repeated across pages into a new variant or composite component instead of copying long class strings.