Forms
View SourceCorex form components accept field={@form[:name]} from Phoenix.Component.to_form/2. Set the form id in to_form/2 and wrap fields in <.form for={@form}>.
The examples below use Corex.Checkbox and Corex.Select. The same patterns apply to other form components; see each module's Form section in Hexdocs.
Example schema
defmodule MyApp.Form.Preferences do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :terms, :boolean, default: false
field :country, Ecto.Enum, values: [:fra, :deu, :bel]
end
def changeset(preferences, attrs \\ %{}) do
preferences
|> cast(attrs, [:terms, :country])
|> validate_required([:terms, :country])
|> validate_acceptance(:terms)
end
def changeset_validate(preferences, attrs \\ %{}) do
preferences
|> cast(attrs, [:terms, :country])
|> validate_required([:terms, :country], message: "can't be blank")
|> validate_acceptance(:terms, message: "must be accepted to continue")
end
endController
Build the form from a changeset, render with <.form>, and post to a controller action.
def preferences_page(conn, _params) do
form =
%MyApp.Form.Preferences{}
|> MyApp.Form.Preferences.changeset(%{})
|> Phoenix.Component.to_form(as: :preferences, id: "preferences-form")
render(conn, :preferences, form: form)
end
def preferences_create(conn, %{"preferences" => params}) do
case MyApp.Form.Preferences.changeset(%MyApp.Form.Preferences{}, params) do
%Ecto.Changeset{valid?: true} = changeset ->
data = Ecto.Changeset.apply_changes(changeset)
conn
|> put_flash(:info, "Saved: terms=#{data.terms}, country=#{data.country}")
|> redirect(to: "/account")
changeset ->
changeset = Map.put(changeset, :action, :insert)
form =
Phoenix.Component.to_form(changeset, as: :preferences, id: "preferences-form")
render(conn, :preferences, form: form)
end
end<.form :let={f} for={@form} action="/account/preferences" method="post">
<.checkbox field={f[:terms]} class="checkbox">
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.select
field={f[:country]}
class="select"
items={Corex.List.new([
%{label: "France", value: "fra"},
%{label: "Belgium", value: "bel"},
%{label: "Germany", value: "deu"}
])}
>
<:label>Country</:label>
<:trigger><.heroicon name="hero-chevron-down" /></:trigger>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.select>
<.action type="submit" class="button button--accent">Submit</.action>
</.form>LiveView
Use an Ecto changeset as the source of truth. Add phx-change so validation runs as the user edits. On <.select>, pass controlled when the form is driven by LiveView (see Corex.Select).
def mount(_params, _session, socket) do
form =
%MyApp.Form.Preferences{}
|> MyApp.Form.Preferences.changeset_validate(%{})
|> to_form(action: :validate, as: :preferences, id: "preferences-form")
{:ok, assign(socket, :form, form)}
end
def handle_event("validate", %{"preferences" => params}, socket) do
form =
%MyApp.Form.Preferences{}
|> MyApp.Form.Preferences.changeset_validate(params)
|> to_form(action: :validate, as: :preferences, id: "preferences-form")
{:noreply, assign(socket, :form, form)}
end
def handle_event("save", %{"preferences" => params}, socket) do
case MyApp.Form.Preferences.changeset(%MyApp.Form.Preferences{}, params) do
%Ecto.Changeset{valid?: true} = changeset ->
data = Ecto.Changeset.apply_changes(changeset)
{:noreply, put_flash(socket, :info, "Saved: country=#{data.country}")}
changeset ->
{:noreply,
assign(socket, :form, to_form(changeset, action: :validate, as: :preferences, id: "preferences-form"))}
end
end<.form for={@form} id="preferences-form" phx-change="validate" phx-submit="save">
<.checkbox
field={@form[:terms]}
class="checkbox"
invalid={Corex.FormField.invalid?(@form[:terms])}
>
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.select
field={@form[:country]}
class="select"
controlled
invalid={Corex.FormField.invalid?(@form[:country])}
items={Corex.List.new([
%{label: "France", value: "fra"},
%{label: "Belgium", value: "bel"},
%{label: "Germany", value: "deu"}
])}
>
<:label>Country</:label>
<:trigger><.heroicon name="hero-chevron-down" /></:trigger>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.select>
<.action type="submit" class="button button--accent">Save</.action>
</.form>Native form (plain HTML)
Use name on the component when you are not using to_form/2. Checkbox values follow Phoenix's checkbox param convention.
<form action="/register" method="post">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
<.checkbox name="user[accept_terms]" class="checkbox">
<:label>Accept terms</:label>
</.checkbox>
<.select
name="user[country]"
class="select"
items={Corex.List.new([
%{label: "France", value: "fra"},
%{label: "Belgium", value: "bel"},
%{label: "Germany", value: "deu"}
])}
>
<:label>Country</:label>
<:trigger><.heroicon name="hero-chevron-down" /></:trigger>
</.select>
<.action type="submit" class="button button--accent">Submit</.action>
</form>def register_create(conn, %{"user" => %{"accept_terms" => terms, "country" => country}}) do
checked = Phoenix.HTML.Form.normalize_value("checkbox", terms)
conn
|> put_flash(:info, "Submitted: terms=#{inspect(checked)}, country=#{country}")
|> redirect(to: "/register")
endEcto validation (strict messages)
Use a dedicated changeset_validate/2 with stricter messages for controller re-render or LiveView phx-change. The HEEx is the same as in Controller or LiveView; only the changeset function differs.
def preferences_validate_page(conn, _params) do
changeset =
MyApp.Form.Preferences.changeset_validate(%MyApp.Form.Preferences{}, %{})
form =
Phoenix.Component.to_form(changeset, as: :preferences_validate, id: "preferences-validate-form")
render(conn, :preferences_validate, form: form)
end
def preferences_validate_create(conn, %{"preferences_validate" => params}) do
case MyApp.Form.Preferences.changeset_validate(%MyApp.Form.Preferences{}, params) do
%Ecto.Changeset{valid?: true} = changeset ->
data = Ecto.Changeset.apply_changes(changeset)
conn
|> put_flash(:info, "Saved: country=#{data.country}")
|> redirect(to: "/account")
changeset ->
form =
Phoenix.Component.to_form(
Map.put(changeset, :action, :insert),
as: :preferences_validate,
id: "preferences-validate-form"
)
render(conn, :preferences_validate, form: form)
end
endIn LiveView, call changeset_validate/2 inside handle_event("validate", ...) the same way as in the LiveView section above.
Error messages and invalid styling
Corex.FormField wires id, name, form, and errors into components that accept field={...}. It does not set invalid from changeset errors automatically.
- Messages render through the
:errorslot when the field has errors and was used (used_input?/1). - Alert borders (
data-invalid) are opt-in: passinvalidwhen you want visible invalid styling.
<.checkbox field={@form[:terms]} class="checkbox" invalid={Corex.FormField.invalid?(@form[:terms])}>
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.select
field={@form[:country]}
class="select"
controlled
invalid={Corex.FormField.invalid?(@form[:country])}
items={Corex.List.new([
%{label: "France", value: "fra"},
%{label: "Belgium", value: "bel"},
%{label: "Germany", value: "deu"}
])}
>
<:label>Country</:label>
<:trigger><.heroicon name="hero-chevron-down" /></:trigger>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.select>Use Corex.FormField.invalid?/1 on LiveView forms with phx-change so borders appear after the user interacts with a field, not on the initial empty render. Static demos without a changeset can pass invalid directly on the component.
Custom error presentation
Keep invalid off the control if you only want a custom affordance (for example a tooltip) without data-invalid on the host.
<.select field={@form[:country]} class="select relative" controlled>
<:label>Country</:label>
<:error :let={msg} class="absolute top-0 end-0">
<.tooltip class="tooltip tooltip--sm" positioning={%Corex.Positioning{placement: "top-end"}}>
<:trigger>
<.heroicon name="hero-exclamation-circle" class="icon text-ink-alert" />
</:trigger>
<:content>{msg}</:content>
</.tooltip>
</:error>
</.select>The :error slot still receives translated messages from the changeset; only the presentation changes.
Component reference
Hover a Component link for the Hexdocs summary card. Form links jump to that module's Form section.