Ecto Changesets are used to validate and manipulate data server-side. Most of the work with changesets is done using schemas, nonetheless there are cases where we would want to validate a set of data without using one, e.g.: form validation. Although Ecto documentation describes how to implement a schemaless changeset, it doesn't explain how to use it with Liveview.
To start off we'll need a data source. In our app's lib/app_web/live
directory create our liveview file, let's call it nickname_live.ex
, and implement mount and render callbacks of the liveview behaviour:
defmodule AppWeb.NicknameLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, form: to_form(%{}))}
end
def render(assigns) do
~H"""
<.simple_form for={@form} phx-change="validate" phx-submit="save" class="flex flex-col items-center">
<h1>Please provide your nickname :</h1>
<.input field={@form[:nickname]} label="nickname" />
<:actions>
<.button>Create</.button>
</:actions>
</.simple_form>
"""
end
So what's going on here? In mount
we add form
(it has to be the same as form's id in html) variable to the socket, it will hold whatever data we send from our form. to_form
function transforms a map or Ecto changeset to a form component. The render function is responsible for generating our html.heex template. Let's now implement some logic:
def handle_event("validate", params, socket) do
types = %{nickname: :string}
changeset =
{%{}, types}
|> Ecto.Changeset.cast(params, Map.keys(types))
|> Ecto.Changeset.validate_required(:nickname)
|> Ecto.Changeset.validate_length(:nickname, min: 3)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
Above code receives params and simply performs Ecto validation and returns all errors generated, finally we update the form
variable. Now when we run the server and try to type in the Nickname
field... we will get an error.
** (ArgumentError) cannot generate name for changeset where the data is not backed by a struct. You must either pass the :as option to form/form_for or use a struct-based changeset
As it turns out, to_form
function computes a name to nest the params based on schema name that is being used. As we don't use a schema we'll have to pass as:
parameter that will serve as aforementioned name. It basically creates a map that we can pattern match against. Our mount
and handle_event
functions will now look like this:
def mount(_params, _session, socket) do
{:ok, assign(socket, form: to_form(%{}, as: :nickname))}
end
def handle_event("validate", %{"nickname"=> params}, socket) do
types = %{nickname: :string}
changeset =
{%{}, types}
|> Ecto.Changeset.cast(params, Map.keys(types))
|> Ecto.Changeset.validate_required(:nickname)
|> Ecto.Changeset.validate_length(:nickname, min: 3)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset, as: :nickname))}
end
Et voilà, now the compiler doesn't complain anymore, and we can see errors when typing in the Nickname
field.