Validate params with a Peri schema and render Phoenix/LiveView forms without
Ecto. Peri.Phoenix.to_form/3 decodes and validates params, wraps the result
in a Peri.Form struct, and converts it into a Phoenix.HTML.Form through
the Phoenix.HTML.FormData protocol, so Phoenix.Component.form/1 and the
<.input> component work out of the box.
This integration is compiled only when the optional phoenix_html dependency
is present:
# mix.exs
{:phoenix_html, "~> 4.1"}Basic Usage
@schema %{
name: {:required, :string},
email: {:required, {:string, {:regex, ~r/@/}}},
age: {:coerce, :string, {:integer, {:gte, 18}}}
}
# Valid params: the form carries the validated data and no errors
Peri.Phoenix.to_form(@schema, %{"name" => "Jane", "email" => "jane@x.com", "age" => "27"})
# Invalid params: errors in changeset format, action set to :validate
form = Peri.Phoenix.to_form(@schema, %{"age" => "17"})
form.errors
# [name: {"is required, expected type of :string", []},
# email: {"is required, expected type of :string", []},
# age: {"should be greater then or equal to 18", []}]Because params flow through Peri.decode/3, the {:coerce, :string, target}
directive converts string params into typed data: "27" validates as the
integer 27 and constraint failures like "17" against {:gte, 18} are
reported on the typed value.
Options
to_form/3 accepts the shared Phoenix.HTML.FormData options plus :mode:
:as- form name, defaults to"peri":id- form id, defaults to the name:action- form action (defaults to:validatewhen validation fails):mode- validation mode forwarded toPeri.decode/3(:strictby default,:permissivekeeps unknown fields)
LiveView Round Trip
defmodule MyAppWeb.SignupLive do
use MyAppWeb, :live_view
@schema %{
name: {:required, :string},
email: {:required, {:string, {:regex, ~r/@/}}},
age: {:coerce, :string, {:integer, {:gte, 18}}}
}
def mount(_params, _session, socket) do
{:ok, assign(socket, form: Peri.Phoenix.to_form(@schema))}
end
def handle_event("validate", %{"peri" => params}, socket) do
{:noreply, assign(socket, form: Peri.Phoenix.to_form(@schema, params))}
end
def handle_event("save", %{"peri" => params}, socket) do
case Peri.decode(@schema, params) do
{:ok, data} ->
# persist the typed data, redirect, ...
{:noreply, socket}
{:error, _errors} ->
{:noreply, assign(socket, form: Peri.Phoenix.to_form(@schema, params))}
end
end
def render(assigns) do
~H"""
<.form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} label="Email" />
<.input field={@form[:age]} type="number" label="Age" />
<button>Save</button>
</.form>
"""
end
endThe update path also accepts the form's source struct directly:
Peri.Phoenix.to_form(form.source, new_params) revalidates against the
stored schema.
HTML5 validations come from the schema: {:required, type} fields render
required, and {:string, {:min, n}} / {:string, {:max, n}} render
minlength / maxlength.
Nested Schemas and inputs_for
Nested map schemas work with Phoenix.HTML.FormData.to_form/4 (what
inputs_for calls under the hood), and nested errors surface in the nested
form:
@schema %{
name: {:required, :string},
address: %{
street: {:required, :string},
city: :string
}
}
form = Peri.Phoenix.to_form(@schema, %{"name" => "Jane", "address" => %{}})
[address_form] = Phoenix.HTML.FormData.to_form(form.source, form, :address, [])
address_form[:street].errors
# [{"is required, expected type of :string", []}]List-of-map schemas ({:list, %{...}}) expand to one form per entry, keyed
by index (peri[tags][0], peri[tags][1], ...), reading values from params
first and falling back to validated data.
Caveats
- No changeset parity.
Peri.Formcarries validated data, raw params, and errors; it does not track changes, touch state, or apply/rollback semantics. If you need those, use the Ecto integration instead. - List element errors are not indexed. Peri currently reports errors
inside list elements at the field level (e.g.
[:tags, :label]) rather than per index ([:tags, 0, :label]), so per-entry errors for list-of-map schemas cannot be attributed to a specific form entry. Nested map errors are fully supported. - Failed validation discards data. When validation fails the form keeps only the raw params (which is what inputs re-render from), not a partially validated result.