Forms
Form component
Read time: 8 minutes
Forms are important in many Phoenix applications. They are also one of the biggest points of confusion for people starting on LiveView. During the next classes we will learn about forms in a bottom-up way. That means that we will start by implementing some things so we understand what Phoenix is solving with its built-in components.
If you think it's too complicated at first and the framework is too hard don't worry because in the end you'll see that all these things are solved with built-in components because Phoenix is a batteries-included framework.
#The simplest form of all
When you learn the basics of HTML I bet at some point you had to build a form that had some inputs and could be submitted. Let's start with that goal. Let's create a product creation form. Create and run first_form.exs
:
Mix.install([
{:liveview_playground, "~> 0.1.5"}
])
defmodule PageLive do
use LiveviewPlaygroundWeb, :live_view
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 phx-submit="create_product" class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24">
<h1>Creating a product</h1>
<input type="text" name="product[name]" placeholder="Name" />
<input type="text" name="product[description]" placeholder="Description" />
<button type="submit">Send</button>
</form>
</div>
"""
end
end
LiveviewPlayground.start(scripts: ["https://cdn.tailwindcss.com?plugins=forms"])
#Tailwind plugin forms
If you look at our scripts
area in LiveViewPlayground.start
you should notice that we added ?plugins=forms
to the CDN URL. This plugin just adds some default styles to HTML forms. In real Phoenix projects with Tailwind it is already pre-installed so you don't need to worry. Moving forward we will be using this plugin a lot.
#HEEx and the phx-submit
binding
Look at our HEEx code. Our form and input tags are just what you've seen when you studied HTML without any modifications. The new thing introduced here is the phx-submit
binding. Just like phx-click
this binding maps the submission of a form to a handle_event/3
in our LiveView.
#Mapping input attributes name
to maps
Another point that you may have found strange is that the name
attributes in our HEEx code use the product[name]
format. Although not mandatory this has been the Phoenix convention before LiveView even existed and the recommendation would be that you continue with it. Don't worry too much, we'll see later that this is all done automatically.
When we have a HTML form with inputs product[name]
and product[description]
this generates an equivalent map in the format %{"product" => %{"name" => "", "description" => ""}}
. This makes it easier for us to retrieve this value in our handle_event/3
. I believe it is always good to highlight this topic as it is something that I generally don't see explained in framework documentation.
#Receiving the form with handle_event/3
In our HEEx code we added to the form phx-submit="create_product"
so we must handle the event "create_product"
like handle_event("create_product", %{"product" => product_params}, socket)
. Note that the params
were matched using the format explained previously because Phoenix prefers to follow this convention.
Our handle_event/3
doesn't do anything important it just generates a message in your terminal and nothing more. Congratulations, you've created your first form in Phoenix LiveView!
#Getting to know the form component
We currently do not perform any type of validation with our form. To help us Phoenix has a data structure called Phoenix.HTML.Form
that simplifies form management in addition to providing us with a validation system.
#New data structures
When we convert a map in the format %{name: "", description: ""}
to Phoenix.HTML.Form
a variable in the format below is created:
%Phoenix.HTML.Form{
source: %{"description" => "", "name" => ""},
impl: Phoenix.HTML.FormData.Map,
id: "product",
name: "product",
data: %{},
action: nil,
hidden: [],
params: %{"description" => "", "name" => ""},
errors: [],
options: [],
index: nil
}
Assuming that your variable is form
, you can access the fields in a structure called Phoenix.HTML.FormField
that have the following format:
%Phoenix.HTML.FormField{
id: "product_name",
name: "product[name]",
errors: [],
field: :name,
form: %Phoenix.HTML.Form{...},
value: ""
}
Let's apply them to our code!
#The <.form>
component
Phoenix projects include a new component called <.form>
. The objective of this component is to generate basic HTML for forms in addition to offering advantages such as protection against CSRF (when necessary), extra error validation and method spoofing. The preference will always be to use this component instead of the <form>
tag.
Let's try. Create and run a file called first_form_component.exs
:
Mix.install([
{:liveview_playground, "~> 0.1.5"}
])
defmodule PageLive do
use LiveviewPlaygroundWeb, :live_view
@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("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-submit="create_product"
class="flex flex-col max-w-96 mx-auto bg-gray-100 p-24"
>
<h1>Creating a product</h1>
<input type="text" id={@form[:name].id} name={@form[:name].name} placeholder="Name" />
<input
type="text"
id={@form[:description].id}
name={@form[:description].name}
placeholder="Description"
/>
<button type="submit">Send</button>
</.form>
</div>
"""
end
end
LiveviewPlayground.start(scripts: ["https://cdn.tailwindcss.com?plugins=forms"])
#Using to_form/2
to generate forms
At the top of our module we created a module attribute called @initial_state
to help making our code more readable and make this state easily accessible in the future. Additionally we introduced a mount/3
that creates an assign called form
with the value of the function to_form/2
passing our @initial_state
and as an option as: :product
. The reason we put this option is so that our form fields follow the product[name]
format.
#Rendering our <.form>
Looking at the HEEx code we can notice that the first difference is that we stopped using the HTML tag <form>
and added the component <.form>
passing the assign for={@form}
. That's all this component needs!
Further below we modified our input
tags to receive form fields in the format @form[:name]
. Each of these represents a Phoenix.HTML.FormField
and we use the field's id
and name
properties in the attributes with the same names.
Now you must be thinking: "my code has become more verbose, what's the advantage?". The motivation is simpler than it seems: we can componentize our input
tags!
#The .input
component
Because you have structured your data in Phoenix.HTML.FormField
we can now easily build a component that reads this data and automatically adds necessary properties like name
and id
. Create and run a file called first_form_component.exs
:
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} />
"""
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("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-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"])
#Implementing <.input>
With barely any code we were able to create a component <.input field={@form[:name]}>
that automatically uses the necessary properties. Additionally we create an attr
that sets the type
to "text"
by default and a global attr
to receive any other necessary properties. If in the future we want to modify the styles of all inputs in our system we have a centralized place to do this.
Will I have to create my input components in every Phoenix project I do?
#Recap!
-
LiveView forms use the
phx-submit
binding to trigger ahandle_event/3
with the respective event name. -
Phoenix prefers to use the format
parent_name[child_name]
in itsinput
to make it easier to manage which form contains which data. This generates maps like%{"parent_name" => %{"child_name" => ""}}
inphx-submit
events. -
The preference for creating forms will always be to use the
<.form>
component instead of the<form>
tag. -
To prepare data in the
Phoenix.HTML.Form
format, theto_form/2
function converts the datato_form(%{name: ""}, as: :product)
into the appropriate format. -
We prefer to add
as: :name_of_parent
as an option toto_form/2
to follow the Phoenix convention ofname
attributes of ourinput
tags. -
Using forms in the
Phoenix.HTML.Form
format makes it easier to create components for inputs.
Feedback
Got any feedback about this page? Let us know!