Forms
Simplifying everything with Ecto
Read time: 6 minutes
Now that you understand not only how forms work behind the scenes but also how to reason about the flow of forms and events, let's simplify everything!
#Introducing Ecto
Ecto is a library in Elixir that manages database access. Over time the community noticed that Ecto's validation pattern was quite powerful and abstractions to validate data without even considering the database were emerging. Today we will use one of them.
It's worth mentioning that in new Phoenix projects Ecto comes by default so understanding Ecto will not only help us today to refactor our form into a more manageable code but it will also teach you the fundamentals of Ecto so that you can use this library in future projects.
#Refactoring our previous form to Ecto
Let's get straight to the point. Create and run a file called ecto_form.exs
:
Mix.install([
{:liveview_playground, "~> 0.1.8"},
{:phoenix_ecto, "~> 4.5"},
{:ecto, "~> 3.11"}
])
defmodule Product do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :name, :string, default: ""
field :description, :string, default: ""
end
def changeset(product, params \\ %{}) do
product
|> cast(params, [:name, :description])
|> validate_required([:name, :description])
end
end
defmodule PageLive do
use LiveviewPlaygroundWeb, :live_view
import LiveviewPlaygroundWeb.CoreComponents
def mount(_params, _session, socket) do
form =
Product.changeset(%Product{})
|> to_form()
{:ok, assign(socket, form: form)}
end
def handle_event("validate_product", %{"product" => product_params}, socket) do
form =
Product.changeset(%Product{}, product_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
def handle_event("create_product", %{"product" => product_params}, socket) do
IO.inspect({"Form submitted!!", product_params})
{:noreply, socket}
end
def render(assigns) do
~H"""
<div class="bg-grey-100">
<.form
for={@form}
phx-change="validate_product"
phx-submit="create_product"
class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24"
>
<h1>Creating a product</h1>
<.input field={@form[:name]} placeholder="Name" />
<.input field={@form[:description]} placeholder="Description" />
<.button type="submit">Send</.button>
</.form>
</div>
"""
end
end
LiveviewPlayground.start(scripts: ["https://cdn.tailwindcss.com?plugins=forms"])
#Installing the necessary libraries
Mix.install([
{:liveview_playground, "~> 0.1.8"},
{:phoenix_ecto, "~> 4.5"},
{:ecto, "~> 3.11"}
])
In our Mix.Install/2
we added not only Ecto itself but also the phoenix_ecto
library that serves to make both work together. In real projects this would already be installed, don't worry.
#Understanding an Ecto Schema
defmodule Product do
use Ecto.Schema
import Ecto.Changeset
# ...
end
The magic starts here. We defined a module called Product
to represent the data in our form. The first thing we do is use Ecto.Schema
so that our module receives the DSL (Domain Specific Language) that lets us use macros like embedded_schema
and field
to define the format of our Product model. Think of this DSL as a simple way to define a Struct in Elixir.
We also imported Ecto.Changeset
. Changeset is a data structure that contains data about modifications to something. In this case our Changeset will contain data about modifications, errors and validations of our Product struct. Think of changesets as a validation step.
#Understanding an embedded_schema
defmodule Product do
# ...
embedded_schema do
field :name, :string, default: ""
field :description, :string, default: ""
end
# ...
end
In Ecto terms Embedded Schemas are data that live only in memory without being saved in a database. Using the above syntax and defining the struct fields with field/3
we can easily tell which data belongs to the Product struct. Essentially what this piece of code does is say that a Struct Product starts as %Product{name: "", description: ""}
with a very easy to understand code.
#The changeset/2
function
defmodule Product do
# ...
def changeset(product, params \\ %{}) do
product
|> cast(params, [:name, :description])
|> validate_required([:name, :description])
end
end
It is practically inevitable that you will see an Ecto.Schema with a changeset/2
function or even more than one. This function is under your full control and is used to define how we validate your data. In previous lessons validation took place within LiveView but this left our code messy and difficult to reuse. In Phoenix projects validations are almost always carried out at the level of an Ecto.Schema in that function.
We receive two arguments: the product and optionally parameters (note that if nothing is passed we use the default %{}
). With these two values in mind we use pipes to transform this value as follows:
-
We have a struct of
%Product{name: "", description: ""}
(since our form starts with an empty%Product{}
). -
Using the function
cast/4
we transform the%Product{}
into a%Ecto.Changeset{}
receiving theparams
and accepting only theparams
that are:name
or:description
. -
Using the function
validate_required/3
we receive the%Ecto.Changeset{}
and validate that:name
and:description
are present.
At the end of the function we will have a validated changeset. The Ecto.Changeset
module contains several useful validation functions and you can also create custom validations. At the moment we will continue with only validate_required/3
.
#Using changesets in our LiveView
defmodule PageLive do
# ...
def mount(_params, _session, socket) do
form =
Product.changeset(%Product{})
|> to_form()
{:ok, assign(socket, form: form)}
end
def handle_event("validate_product", %{"product" => product_params}, socket) do
form =
Product.changeset(%Product{}, product_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
# ...
end
Now that we have changesets the only refactoring necessary for our LiveView happened in the callbacks. Let's analyze them step by step.
#The new mount/3
def mount(_params, _session, socket) do
form =
Product.changeset(%Product{})
|> to_form()
{:ok, assign(socket, form: form)}
end
In our mount/3
we use the module's changeset/2
function without passing the second argument as we know that there is no modified data. We immediately pass the result to to_form/2
.
You may be wondering: don't we need an as: :product
? Phoenix forms are prepared to automatically convert the Ecto.Schema
name so that a Product
schema implicitly means as: :product
in to_form/2
. It's worth remembering that from the beginning we mentioned that this was the Phoenix standard and you can see how the framework takes this seriously to the point of simplifying it for you.
#The new handle_event/3
def handle_event("validate_product", %{"product" => product_params}, socket) do
form =
Product.changeset(%Product{}, product_params)
|> Map.put(:action, :validate)
|> to_form()
{:noreply, assign(socket, form: form)}
end
Very similar to mount/3
, our function also uses changeset to create Phoenix.HTML.Form
. We had two modifications:
-
We pass the
product_params
to the changeset so that the new data is validated. -
We use
Map.put/3
to define THAT the changeset is in validation mode. This is necessary so that our LiveView knows that the changeset has been validated and errors can be rendered.
#Recap!
- Ecto is a powerful library for managing powerfu database access.
- Phoenix projects use Ecto by default not only to work with databases but also to validate data.
-
We can use
Ecto.Schema
to easily create aStruct
. As we are not working with a database (yet) we useembedded_schema
andfields
to be able to create data only in memory. -
We can use
Ecto.Changeset
to easily validate user data going into your struct. -
Usually an
Ecto.Schema
has one or morechangeset/2
functions to define how to validate its data and are used asProduct.changeset(%Product{}, params)
. - Our LiveViews become cleaner when we separate the validation logic from our UI logic.
Feedback
Got any feedback about this page? Let us know!