Forms
Validations
Read time: 5 minutes
We learned the basics of forms but we all know that a large part of the problem forms solve is related to validating data! Let's find out how LiveView handles these cases now.
#The phx-change
binding
Just like phx-submit
, the phx-change
binding works on forms but it is triggered every time any data in a form is modified. Let's get to practice: create and run a phx_change.exs
file:
Mix.install([
{:liveview_playground, "~> 0.1.5"}
])
defmodule CoreComponents do
use LiveviewPlaygroundWeb, :html
attr :field, Phoenix.HTML.FormField, required: true
attr :type, :string, default: "text"
attr :rest, :global, include: ~w(placeholder type)
def input(assigns) do
~H"""
<input type="text" id={@field.id} name={@field.name} {@rest} />
<div :for={msg <- @field.errors} class="text-red-500 py-2"><%= msg %></div>
"""
end
end
defmodule PageLive do
use LiveviewPlaygroundWeb, :live_view
import CoreComponents
@initial_state %{
"name" => "",
"description" => ""
}
def mount(_params, _session, socket) do
form = to_form(@initial_state, as: :product)
{:ok, assign(socket, form: form)}
end
def handle_event("validate_product", %{"product" => product_params}, socket) do
errors = []
errors =
if product_params["name"] == "" do
Keyword.put(errors, :name, "cannot be empty")
else
errors
end
errors =
if product_params["description"] == "" do
Keyword.put(errors, :description, "cannot be empty")
else
errors
end
form = to_form(product_params, as: :product, errors: errors)
{: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"])
Before you use your new LiveView, let's understand what's going on.
#phx-change
in our render/1
We added the phx-change="validate_product"
binding to our <.form>
component so the "validate_product"
event will be triggered whenever any input is modified. Nothing else in our render/1
has been modified.
#The <.input>
component
For errors to be displayed we need to define how they show appear in our code. Inside our Phoenix.Form.FormField
the errors
property contains a list of errors in string format. A div
with a :for={msg <- @field.errors}
loop is enough. Since we are using a component both of our fields automatically receive error validation!
#The handle_event("validate_product", %{"product" => product_params}, socket)
Our handle_event/3
follows the same format as the phx-submit
event. To add error validation simply create a Keyword list in the format [name: "cannot be empty", description: "cannot be empty"]
. Each field can have more than one validation error. Let's see how the validation that name
contains something was done:
errors = []
errors =
if product_params["name"] == "" do
Keyword.put(errors, :name, "cannot be empty")
else
errors
end
Our keyword list of errors starts empty. If the value of product_params["name"]
is ""
we use Keyword.put/3
to add the error. The same is repeated for description
.
At the end of the function we recreate the form
this time passing the list of errors to to_form/2
: form = to_form(product_params, as: :product, errors: errors)
.
#Hands-on
Now open LiveView in your browser. Write anything in the name field and immediately see that the description field says it cannot be empty. We are currently not checking whether the modified field was the same field being validated!
Also our LiveView has another issue. Leave the name field and write anything in the description field. The name disappeared?! What's going on here? Let's understand this now!
#How validation works
When you reassign the assign form
in your "validate_product
" event LiveView understands that all components that depend on it need an update. Furthermore, we updated the current value of the form fields but we do not teach the component to use this updated value.
def input(assigns) do
~H"""
<input type="text" id={@field.id} name={@field.name} value={@field.value} {@rest} />
<div :for={msg <- @field.errors} class="text-red-500 py-2"><%= msg %></div>
"""
end
At this point we already know what we need to know about this component. Let's use the real <.input>
component created by Phoenix! Create and run form_with_core_components.exs
:
Mix.install([
{:liveview_playground, "~> 0.1.7"}
])
defmodule PageLive do
use LiveviewPlaygroundWeb, :live_view
import LiveviewPlaygroundWeb.CoreComponents
@initial_state %{
"name" => "",
"description" => ""
}
def mount(_params, _session, socket) do
form = to_form(@initial_state, as: :product)
{:ok, assign(socket, form: form)}
end
def handle_event("validate_product", %{"product" => product_params}, socket) do
errors = []
errors =
if product_params["name"] == "" do
Keyword.put(errors, :name, "cannot be empty")
else
errors
end
errors =
if product_params["description"] == "" do
Keyword.put(errors, :description, "cannot be empty")
else
errors
end
form = to_form(product_params, as: :product, errors: errors)
{: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"])
We've swaped our own definition of <.input>
for the automatically generated definition from LiveviewPlaygroundWeb.CoreComponents
. I hope that with the content so far you have understood the power behind this component even if you never have to edit it.
You may also be wondering: why does the <.form>
component already comes with Phoenix and the <.input>
component is generated in CoreComponentes? The answer is simpler than it seems. While <.form>
works more with managing certain form features and does not have styles, CoreComponents components always have styles so it makes sense for them to come with a default style and you can edit them as you please, it's all in your hands.
#Recap!
-
phx-change
is a binding that runs every time the form changes. It triggers ahandle_event/3
similar to that ofphx-submit
. -
You can use
to_form/3
to add validation errors to your form by passing it in the options. -
It is the responsibility of the
<.input>
component to render errors and to render the current value of the form field if it is modified at the server. -
Fortunately you don't need to create the
<.input>
component we will use CoreComponents from now on, which would be normal in real Phoenix projects. -
Remember: in lessons we will do
import SeuProjetoWeb.CoreComponents
however in real Phoenix projects this comes automatically when you douse YourProjectWeb, :live_view
.
Feedback
Got any feedback about this page? Let us know!