Forms Cheatsheet

View Source

Quick reference for form controls and patterns in Sutra UI.

Basic Form

Simple Form with Changeset

<.simple_form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:name]} label="Name" />
  <.input field={@form[:email]} type="email" label="Email" />
  <:actions>
    <.button type="submit" phx-disable-with="Saving...">
      Save
    </.button>
  </:actions>
</.simple_form>

Manual Form

<form phx-submit="save">
  <.field>
    <:label>Name</:label>
    <.input type="text" name="name" value={@name} />
  </.field>
  <.button type="submit">Save</.button>
</form>

Text Inputs

Basic Input

<.input type="text" name="name" value={@name} />
<.input type="text" name="name" placeholder="Enter name" />
<.input type="text" name="name" disabled />

With Field Wrapper

<.field>
  <:label>Full Name</:label>
  <.input type="text" name="name" />
  <:description>Enter your legal name</:description>
</.field>

Input with Errors

<.field>
  <:label>Email</:label>
  <.input 
    type="email" 
    name="email"
    value={@email}
    aria-invalid={@errors != []}
  />
  <:error :for={error <- @errors}>{error}</:error>
</.field>

Input Types Reference

TypeExample
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

Basic Textarea

<.textarea name="bio" />
<.textarea name="bio" rows="6" />
<.textarea name="bio" placeholder="Tell us about yourself..." />

With Character Count

<.field>
  <:label>Bio</:label>
  <.textarea name="bio" maxlength="500" />
  <:description>{String.length(@bio)}/500 characters</:description>
</.field>

Input Groups

With Prefix

<.input_group>
  <:prefix>https://</:prefix>
  <.input type="text" name="domain" placeholder="example.com" />
</.input_group>

With Suffix

<.input_group>
  <.input type="number" name="weight" />
  <:suffix>kg</:suffix>
</.input_group>

With Icons

<.input_group>
  <:prefix>
    <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" placeholder="Search..." />
</.input_group>

<.input_group>
  <:prefix>$</:prefix>
  <.input type="number" name="price" />
  <:suffix>.00</:suffix>
</.input_group>

Selection Controls

Checkbox

<.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

<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

<.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

<.radio_group name="plan" value={@selected_plan}>
  <:radio value="free">Free</:radio>
  <:radio value="pro">Pro - $10/mo</:radio>
  <:radio value="enterprise">Enterprise - Contact us</:radio>
</.radio_group>

Radio with Descriptions

<.radio_group name="shipping">
  <:radio value="standard">
    <span class="font-medium">Standard</span>
    <span class="text-muted-foreground">3-5 business days</span>
  </:radio>
  <:radio value="express">
    <span class="font-medium">Express</span>
    <span class="text-muted-foreground">1-2 business days</span>
  </:radio>
</.radio_group>

Select Controls

Basic Select

<.select 
  id="country"
  name="country"
  placeholder="Select a country"
  options={[
    {"United States", "us"},
    {"Canada", "ca"},
    {"Mexico", "mx"}
  ]}
/>

Select with Groups

<.select 
  id="timezone"
  name="timezone"
  placeholder="Select timezone"
  options={[
    {"Americas": [
      {"New York", "America/New_York"},
      {"Los Angeles", "America/Los_Angeles"}
    ]},
    {"Europe": [
      {"London", "Europe/London"},
      {"Paris", "Europe/Paris"}
    ]}
  ]}
/>

Select with Default Value

<.select 
  id="status"
  name="status"
  value={@status}
  options={[
    {"Active", "active"},
    {"Inactive", "inactive"},
    {"Pending", "pending"}
  ]}
/>
<.live_select
  id="user-select"
  name="user_id"
  placeholder="Search users..."
  search_event="search_users"
  value={@selected_user}
/>

In your LiveView:

def handle_event("search_users", %{"query" => query}, socket) do
  users = Accounts.search_users(query)
  options = Enum.map(users, &{&1.name, &1.id})
  {:noreply, push_event(socket, "live_select:options", %{options: options})}
end

Sliders

Basic Slider

<.slider id="volume" name="volume" min="0" max="100" value="50" />

Slider with Steps

<.slider id="rating" name="rating" min="1" max="5" step="1" value="3" />

Range Slider (Dual Handle)

<.range_slider
  id="price-range"
  name="price"
  min="0"
  max="1000"
  min_value="200"
  max_value="800"
/>

Slider with Labels

<.field>
  <:label>Volume: {@ volume}%</:label>
  <.slider 
    id="volume" 
    name="volume" 
    min="0" 
    max="100" 
    value={@volume}
    phx-change="update_volume"
  />
</.field>

Form Patterns

Inline Validation

# 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
<.simple_form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:email]} type="email" label="Email" />
  <:actions>
    <.button type="submit">Save</.button>
  </:actions>
</.simple_form>

Form with Loading State

<.simple_form for={@form} phx-submit="save">
  <.input field={@form[:name]} label="Name" />
  <:actions>
    <.button type="submit" loading={@saving} phx-disable-with="Saving...">
      Save
    </.button>
  </:actions>
</.simple_form>

Multi-Step Form

<.tabs id="form-steps" default_value={@step}>
  <:tab value="1" disabled={@step < 1}>Step 1</:tab>
  <:tab value="2" disabled={@step < 2}>Step 2</:tab>
  <:tab value="3" disabled={@step < 3}>Review</:tab>
  
  <:panel value="1">
    <.simple_form for={@form} phx-submit="next_step">
      <.input field={@form[:name]} label="Name" />
      <:actions>
        <.button type="submit">Next</.button>
      </:actions>
    </.simple_form>
  </:panel>
  <!-- Additional panels... -->
</.tabs>

Filter Forms

Filter Bar

<.filter_bar>
  <.input type="search" name="q" value={@query} placeholder="Search..." />
  <.select 
    id="status" 
    name="status" 
    value={@status}
    options={[{"All", ""}, {"Active", "active"}, {"Inactive", "inactive"}]}
  />
  <.select
    id="sort"
    name="sort"
    value={@sort}
    options={[{"Newest", "newest"}, {"Oldest", "oldest"}, {"Name", "name"}]}
  />
  <.button type="submit">Apply</.button>
  <.button type="button" variant="outline" phx-click="reset_filters">
    Reset
  </.button>
</.filter_bar>

Date Range Filter

<.filter_bar>
  <.input type="date" name="start_date" value={@start_date} />
  <span class="text-muted-foreground">to</span>
  <.input type="date" name="end_date" value={@end_date} />
  <.button type="submit">Filter</.button>
</.filter_bar>

Error Handling

Field with Error

<.field>
  <:label>Email</:label>
  <.input 
    field={@form[:email]} 
    type="email"
    phx-debounce="blur"
  />
  <:error :for={error <- @form[:email].errors}>
    {translate_error(error)}
  </:error>
</.field>

Form-Level Errors

<.simple_form for={@form} phx-submit="save">
  <.alert :if={@form.errors[:base]} variant="destructive">
    {translate_error(@form.errors[:base])}
  </.alert>
  
  <.input field={@form[:email]} label="Email" />
  <:actions>
    <.button type="submit">Save</.button>
  </:actions>
</.simple_form>

Async Validation

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
<.field>
  <:label>Email</:label>
  <.input 
    type="email" 
    name="email"
    phx-blur="validate_email"
    phx-debounce="500"
  />
  <:error :if={@email_error}>{@email_error}</:error>
</.field>