Current section
Files
Jump to
Current section
Files
cheatsheets/forms.cheatmd
# Forms Cheatsheet
Quick reference for form controls and patterns in Sutra UI.
For the broader component list, use `components.cheatmd`. For design rules,
composition, and implementation constraints, see `SKILL.md`,
`skill/patterns.md`, and `usage_rules.md`.
Browse live form and component examples at [sutraui.gurujada.com](https://sutraui.gurujada.com).
## Basic Form
{: .col-2}
### LiveView Form with Changeset
```heex
<.form for={@form} class="form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} type="email" label="Email" />
<.button type="submit" phx-disable-with="Saving...">Save</.button>
</.form>
```
### Manual Form
```heex
<.form for={@form} phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.button type="submit">Save</.button>
</.form>
```
## Text Inputs
{: .col-2}
### Basic Input
```heex
<.input type="text" name="name" value={@name} />
<.input type="text" name="name" placeholder="Enter name" />
<.input type="text" name="name" disabled />
```
### With Label and Description
```heex
<.input
field={@form[:name]}
label="Full Name"
description="Enter your legal name"
/>
```
### Input with Errors
```heex
<.input
id="email"
name="email"
value={@email}
type="email"
label="Email"
errors={@errors}
/>
```
### Input Types Reference
| Type | Example |
|------|---------|
| `text` | `<.input type="text" />` |
| `email` | `<.input type="email" />` |
| `password` | `<.input type="password" />` |
| `number` | `<.input type="number" min="0" />` |
| `tel` | `<.input type="tel" />` |
| `url` | `<.input type="url" />` |
| `search` | `<.input type="search" />` |
| `date` | `<.input type="date" />` |
| `time` | `<.input type="time" />` |
| `datetime-local` | `<.input type="datetime-local" />` |
## Textarea
{: .col-2}
### Basic Textarea
```heex
<.textarea name="bio" />
<.textarea name="bio" rows="6" />
<.textarea name="bio" placeholder="Tell us about yourself..." />
```
### With Character Count
```heex
<.input
field={@form[:bio]}
type="textarea"
label="Bio"
description={"#{String.length(@bio || "")}/500 characters"}
maxlength="500"
/>
```
## Input Groups
{: .col-2}
### With Prefix
```heex
<.input_group>
<:prefix>https://</:prefix>
<input type="text" name="domain" class="input pl-21" placeholder="example.com" />
</.input_group>
```
### With Suffix
```heex
<.input_group>
<input type="number" name="weight" class="input pr-12" />
<:suffix>kg</:suffix>
</.input_group>
```
### With Icons
```heex
<.input_group>
<:prefix type="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</:prefix>
<input type="search" name="q" class="input pl-9" placeholder="Search..." />
</.input_group>
<.input_group>
<:prefix>$</:prefix>
<input type="number" name="price" class="input pl-7 pr-12" />
<:suffix>.00</:suffix>
</.input_group>
```
## Selection Controls
{: .col-2}
### Checkbox
```heex
<.checkbox id="terms" name="terms" />
<!-- With label -->
<div class="flex items-center gap-2">
<.checkbox id="terms" name="terms" />
<.label for="terms">I agree to the terms</.label>
</div>
<!-- Checked by default -->
<.checkbox id="subscribe" name="subscribe" checked />
```
### Checkbox Group Pattern
```heex
<fieldset>
<legend class="text-sm font-medium">Notifications</legend>
<div class="space-y-2 mt-2">
<div class="flex items-center gap-2">
<.checkbox id="email" name="notifications[]" value="email" />
<.label for="email">Email</.label>
</div>
<div class="flex items-center gap-2">
<.checkbox id="sms" name="notifications[]" value="sms" />
<.label for="sms">SMS</.label>
</div>
</div>
</fieldset>
```
### Switch
```heex
<.switch id="dark-mode" name="dark_mode" />
<!-- With label -->
<div class="flex items-center gap-2">
<.switch id="notifications" name="notifications" />
<.label for="notifications">Enable notifications</.label>
</div>
```
### Radio Group
```heex
<.radio_group name="plan" label="Plan">
<:radio value="free" label="Free" checked={@selected_plan == "free"} />
<:radio value="pro" label="Pro - $10/mo" checked={@selected_plan == "pro"} />
<:radio
value="enterprise"
label="Enterprise - Contact us"
checked={@selected_plan == "enterprise"}
/>
</.radio_group>
```
### Radio with Descriptions
```heex
<.radio_group name="shipping">
<:radio value="standard" label="Standard">
<span class="text-muted-foreground">3-5 business days</span>
</:radio>
<:radio value="express" label="Express">
<span class="text-muted-foreground">1-2 business days</span>
</:radio>
</.radio_group>
```
## Select Controls
{: .col-2}
### Basic Select
```heex
<.select id="country" name="country" value={@country}>
<:trigger>{@country_label || "Select a country"}</:trigger>
<.select_option value="us" label="United States" />
<.select_option value="ca" label="Canada" />
<.select_option value="mx" label="Mexico" />
</.select>
```
### Select with Groups
```heex
<.select id="timezone" name="timezone" value={@timezone} searchable>
<:trigger>{@timezone_label || "Select timezone"}</:trigger>
<.select_group label="Americas">
<.select_option value="America/New_York" label="New York" />
<.select_option value="America/Los_Angeles" label="Los Angeles" />
</.select_group>
<.select_separator />
<.select_group label="Europe">
<.select_option value="Europe/London" label="London" />
<.select_option value="Europe/Paris" label="Paris" />
</.select_group>
</.select>
```
### Select with Default Value
```heex
<.select id="status" name="status" value={@status} selected_label={@status_label}>
<.select_option value="active" label="Active" />
<.select_option value="inactive" label="Inactive" />
<.select_option value="pending" label="Pending" />
</.select>
```
### Live Select (Async Search)
```heex
<.form for={@form} class="form" phx-change="validate">
<.live_component
module={SutraUI.LiveSelect}
id="user-select"
field={@form[:user_id]}
placeholder="Search users..."
/>
</.form>
<%= case SutraUI.LiveSelect.decode(@form.params["user_id"]) do %>
<% nil -> %>
<% [] -> %>
<% user -> %>
Selected user id: {user}
<% end %>
```
In your LiveView:
```elixir
def handle_event("live_select_change", %{"text" => text, "id" => id}, socket) do
users = Accounts.search_users(text)
options = Enum.map(users, &{&1.name, &1.id})
send_update(SutraUI.LiveSelect, id: id, options: options)
{:noreply, socket}
end
```
## Sliders
{: .col-2}
### Basic Slider
```heex
<.slider id="volume" name="volume" min="0" max="100" value="50" />
```
### Slider with Steps
```heex
<.slider id="rating" name="rating" min="1" max="5" step="1" value="3" />
```
### Range Slider (Dual Handle)
```heex
<.range_slider
id="price-range"
name="price"
min="0"
max="1000"
value_min="200"
value_max="800"
/>
```
### Slider with Labels
```heex
<div class="space-y-2">
<div class="flex items-center justify-between">
<.label for="volume">Volume</.label>
<output for="volume">{@volume}%</output>
</div>
<.slider id="volume" name="volume" min="0" max="100" value={@volume} phx-change="update_volume" />
</div>
```
## Form Patterns
{: .col-2}
### Inline Validation
```elixir
# LiveView
def handle_event("validate", %{"user" => params}, socket) do
changeset =
%User{}
|> User.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
```
```heex
<.form for={@form} class="form" phx-change="validate" phx-submit="save">
<.input field={@form[:email]} type="email" label="Email" />
<.button type="submit">Save</.button>
</.form>
```
### Form with Loading State
```heex
<.form for={@form} class="form" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.button type="submit" loading={@saving} phx-disable-with="Saving...">Save</.button>
</.form>
```
### Multi-Step Form
```heex
<.form for={@form} class="form" phx-change="validate" phx-submit="next_step">
<.stepper_wizard id="profile-wizard" current={@step} errors={@step_errors}>
<:step id="profile" label="Profile">
<.input field={@form[:name]} label="Name" />
</:step>
<:step id="details" label="Details">
<.input field={@form[:email]} type="email" label="Email" />
</:step>
<:step id="review" label="Review">
Review your details before submitting.
</:step>
<:actions>
<.button type="button" variant="outline" phx-click="previous_step">Back</.button>
<.button type="submit">Continue</.button>
</:actions>
</.stepper_wizard>
</.form>
```
## Filter Forms
{: .col-2}
### Filter Bar
```heex
<.filter_bar on_change="filter" on_clear="reset_filters" show_clear={@has_filters}>
<:filter>
<.input type="search" name="q" value={@query} placeholder="Search..." />
</:filter>
<:filter>
<.input
type="select"
name="status"
value={@status}
options={[{"All", ""}, {"Active", "active"}, {"Inactive", "inactive"}]}
/>
</:filter>
<:filter>
<.input
type="select"
name="sort"
value={@sort}
options={[{"Newest", "newest"}, {"Oldest", "oldest"}, {"Name", "name"}]}
/>
</:filter>
</.filter_bar>
```
### Date Range Filter
```heex
<.filter_bar on_change="filter_dates" cols={2}>
<:filter>
<.input type="date" name="start_date" value={@start_date} />
</:filter>
<:filter>
<.input type="date" name="end_date" value={@end_date} />
</:filter>
</.filter_bar>
```
## Error Handling
{: .col-2}
### Input with Error
```heex
<.input
field={@form[:email]}
type="email"
label="Email"
phx-debounce="blur"
/>
```
### Form-Level Errors
```heex
<.form for={@form} class="form" phx-submit="save">
<.alert :if={@form.errors[:base]} variant="destructive">
<:title>{translate_error(@form.errors[:base])}</:title>
</.alert>
<.input field={@form[:email]} label="Email" />
<.button type="submit">Save</.button>
</.form>
```
### Async Validation
```elixir
def handle_event("validate_email", %{"email" => email}, socket) do
case Accounts.email_available?(email) do
true -> {:noreply, assign(socket, email_error: nil)}
false -> {:noreply, assign(socket, email_error: "Email already taken")}
end
end
```
```heex
<.input
id="email"
name="email"
value={@email}
type="email"
label="Email"
phx-blur="validate_email"
phx-debounce="500"
errors={if @email_error, do: [@email_error], else: []}
/>
```