https://fly.io/phoenix-files/phx-trigger-action/
App performance optimization Open main menu
Content
Blog Phoenix Files Laravel Bytes
Content
Blog Phoenix Files Laravel Bytes
Docs Community Status Pricing Sign In Get Started RSS Feed
Reading time * 7 min Share this post on Twitter Share this post on
Hacker News Share this post on Reddit
Triggering a Phoenix Controller Action From a Form in a LiveView
Author
Berenice Medel
Name
Berenice Medel
Twitter
@bemesa21
Author
Chris Nicoll
Name
Chris Nicoll
Twitter
@beepcat
[phx-trigge]
This is a post about getting a form in a LiveView to invoke a Phoenix
controller action, on your terms. If you want to deploy a Phoenix
LiveView app right now, then check out how to get started. You can be
up and running in minutes.
Have you ever wanted to use LiveViews for a site's authentication?
Among many other implementation details, you need to save some data
to identify the logged-in user. This can be a token or some unique
identifier, and it needs to persist even as the user navigates around
your app and different LiveViews get created and destroyed.
The obvious solution is to store this token or unique identifier in
the session. You can create a Phoenix controller with a :create
action that generates a token, then saves it in the session using
functions of the Plug.Conn module:
defmodule MyAppWeb.SessionController do
use MyAppWeb, :controller
def create(conn, %{"user" => user}) do
token = Accounts.generate_user_session_token(user)
conn
|> put_session(:user_token, token)
|> redirect(to: signed_in_path(conn))
end
end
You continue building your authentication system and decide that once
a user signs up, using a form in a LiveView, they should be
automatically logged in. This means saving the session data from
within the LiveView--and only after the new user is finished signing
up and you're happy for them to have access to the app.
Problem
The LiveView lifecycle starts as an HTTP request, but then a
WebSocket connection is established with the server, and all
communication between your LiveView and the server takes place over
that connection.
Why is this important? Because session data is stored in cookies, and
cookies are only exchanged during an HTTP request/response. So
writing data in session can't be done directly from a LiveView.
Can we call the controller's :create action from our LiveView form,
and have it write the data for us? And can we make sure that happens
only once the new user's registration process is complete: their data
validated and saved?
Solution
We can make an HTTP route call to a controller when the form is
submitted by adding the :action attribute to our forms, specifying
the URL we want to use.
And the :phx-trigger-action attribute allows us to make form
submission conditional on some criteria.
In this case, we want to trigger the form submit, and log the new
user in, after saving their registration data in the database without
errors; if this doesn't happen, the action should not trigger, and
instead we need to keep our LiveView connected and display any
generated errors.
Let's see how to do it.
Let's start by defining, in our LiveView, the form that we'll use to
fill out the user's data:
def render(assigns) do
~H"""
Register
<.form
id="registration_form"
:let={f}
for={@changeset}
as={:user}
>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
<%= label f, :password %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
<%= submit "Register" %>
"""
end
This form uses a changeset to build the necessary inputs. In this
case, just a couple of inputs to save the user's email and password.
We define the changeset and add it to the LiveView assigns:
def mount(_params, _session, socket) do
changeset = Accounts.change_user_registration(%User{})
{:ok, assign(socket, changeset: changeset)}
end
We also add a couple of callbacks: validate to validate the data that
the user enters into the form, and show us live errors if needed, and
save to persist the user's information into the database.
def handle_event("validate", %{"user" => user_params}, socket) do
changeset = Accounts.change_user_registration(%User{}, user_params)
{:noreply, assign(socket, changeset: changeset}
end
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.register_user(user_params) do
{:ok, user} ->
changeset = Accounts.change_user_registration(user)
{:noreply, assign(socket, changeset: changeset)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
We add three more attributes to our form: :phx-submit, :phx-change
and :action. The first two invoke the callbacks we defined above, and
:action executes our controller's :create action using the URL users/
log_in/.
Spoiler alert! Verified routes coming soon
<.form
id="registration_form"
:let={f}
for={@changeset}
as={:user}
phx-submit="save"
phx-change="validate"
action={~p"/users/log_in/"} #{Routes.session_path(@socket, :create)}
>
With this, we get the :create action to run once the form is
submitted; however, the action will run happily even if there was an
error saving the user data. We don't want that!
This is where the :phx-trigger-action attribute comes into play.
Let's use it to submit the form only if the user has been
successfully saved to the database.
First we add the phx-trigger-action attribute to the form:
<.form
id="registration_form"
:let={f}
for={@changeset}
as={:user}
phx-submit="save"
phx-change="validate"
action={~p"/users/log_in/"} #{Routes.session_path(@socket, :create)}
phx-trigger-action={@trigger_submit}
>
You can probably see where this is going: phx-trigger-action takes a
boolean value, so when @trigger_submit is true, the form will get
submitted and the action defined in our action attribute will be
triggered. Let's add trigger_submit to the LiveView assigns:
def mount(_params, _session, socket) do
changeset = Accounts.change_user_registration(%User{})
{:ok, assign(socket, changeset: changeset, trigger_submit: false)}
end
We change trigger_submit to true only if the user has been saved
correctly:
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.register_user(user_params) do
{:ok, user} ->
changeset = Accounts.change_user_registration(user)
socket = assign(socket, changeset: changeset, trigger_submit: true)
{:noreply, socket}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
Now the :create action is only executed once the user is saved
correctly. In case of error, the LiveView shows the registration
errors to the user.
Fly.io [?] Elixir
Fly.io is a great way to run your Phoenix LiveView app close to your
users. It's really easy to get started. You can be running in
minutes.
Deploy a Phoenix app today! -
[cta-dog]
Possible Errors and How to Fix Them
Let's prevent two common errors that can trip us up when using the
phx-trigger-action option.
Form Parameters Are Empty When Phx-trigger-action Is Triggered
The first error is very specific to our use case and is related to
our form fields: When the form is submitted and the form's parameters
reach the controller, the parameter that stores the user's password
is empty, even though we're sure we've entered a value.
This is related to the password type input and its behavior. All we
have to do is explicitly give the input a value by adding the value
option like so:
<%= password_input f, :password,
required: true,
value: input_value(f, :password)
%>
With this simple step, the value of our password input will be sent
in the parameters of the form!
The Controller Route Is Not Found, Even Though It Is Defined in the
Router
The second mystifying error is this: phx-trigger-action tries to
execute the controller action we specified, but the route cannot be
found on the router, even when it is the correct one.
[debug] ** (Phoenix.Router.NoRouteError)
no route found for PUT /users/log_in (MyAppWeb.Router)
In this case, it's related to how our changeset is interpreted when
the form and its attributes are being built.
In our example, we insert the user into the database just before
submitting the form, so our changeset contains the data of a record
that already exists. Phoenix thinks that we're trying to modify that
record; that's when the form is built using the put method instead of
the post method.
The solution is simple; we just have to add the option method="post"
to our form's definition.
<.form
id="registration_form"
:let={f}
for={@changeset}
as={:user}
phx-submit="save"
phx-change="validate"
action={~p"/users/log_in/"} #{Routes.session_path(@socket, :create)}
phx-trigger-action={@trigger_submit}
method="post"
>
Discussion
The phx-trigger-action option is ideal when you need to do final
validations just before submitting form data via an HTTP request from
a LiveView.
It's also so simple to use that you'd think nothing could go wrong.
However, as we've seen, headaches can arise from the underlying form
behavior, and they can be tricky to debug. We've highlighted two such
problems to help you use the phx-trigger-action option painlessly.
Last updated *
Aug 16, 2022
Share this post on Twitter Share this post on Hacker News Share this
post on Reddit
Author
Berenice Medel
Name
Berenice Medel
Twitter
@bemesa21
Author
Chris Nicoll
Name
Chris Nicoll
Twitter
@beepcat
Previous post |
Making Tabs Mobile Friendly
Previous post |
Making Tabs Mobile Friendly
App performance optimization
Company
About Pricing Jobs
Content
Blog Phoenix Files Laravel Bytes
Resources
Docs Support Status
Contact
GitHub Twitter Community
Legal
Security Privacy policy Terms of service
Copyright (c) 2022 Fly.io