Phoenix 1.5 is out! Phoenix Live view is now part of the Phoenix Framework! For a quick introduction to what Phoenix Live View is about, checkout this video by Chris McCord, the creator of Phoenix. This series will dive a bit more into setting up a new Phoenix app.
Check out the source repo
- Part 1 will be about generating scaffolding code, and explaining a bit about how live view works.
- In part 2 we will implement magic links, ala slack, and we will hide the app behind the login.
As in the video, we initialize the project with --live
mix archive.install hex phx_new 1.5.1 # Install the 1.5.1 phx_new generator
mix phx.new feenix --live && cd feenix # Create and enter the project
git init && git add -A && git commit -m "init" # create init commit
Users Users Users
Users are the center piece of any application. Our application will make use of ~Magic Links~ ala Slack, so no need for a password field. Let's generate our Accounts context and User schema.
mix phx.gen.live Accounts User users email:string username:string
Let's look at this command piece by piece
-
mix phx.gen.live
New live view generators in Phoenix 1.5. The live view equivalent ofmix phx.gen.html
-
Accounts
The name of our domain context -
User
The module name for the schema we are creating -
users
The name of the database field attached to the schema -
email:sttring username:string
The fields and their types
Add the live routes to your browser scope in lib/feenix_web/router.ex
:
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id/edit", UserLive.Index, :edit
live "/users/:id", UserLive.Show, :show
live "/users/:id/show/edit", UserLive.Show, :edit
Next, let's go into the CreateUsers
migration and unique indexes for email and username to the users table. The changeset function should now look like
# priv/repo/migrations/XXXXXXXXXXX_create_users.exs
def change do
create table(:users) do
add :email, :string
add :username, :string
timestamps()
end
create unique_index(:users, :email)
create unique_index(:users, :username)
end
This will both create an index in the database (for fast searching) and create a unique constraint.
Next, let's update the changeset to enforce the new unique constraint. The new changeset should look like
# lib/feenix/acconts/user.ex
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :username])
|> validate_required([:email, :username])
|> unique_constraint(:email,
name: "users_email_index",
message: "Account already exists. Please log in."
)
|> unique_constraint(:username,
name: "users_username_index",
message: "Username already in use. Please use another."
)
end
And lastly, let's just fix the input in our new/edit form
# lib/feenix_web/live/user_live/form_component.html.leex
<%= email_input f, :email %> # change text_input to email_input
Now that we have the users set up, let's commit
git add -A && git commit -m "Set up user accounts"
Looking around
Let's check out our new application. and compare it to what we would have gotten if we used phx.gen.html
as our scaffolding generator.
First thing to notice is that there are no controllers. Instead there is a live/user_live
folder which contains both the LiveViews and templates. Also of note, is that there are only 2 LiveViews, Index
and Show
(as well as a FormComponent
). Let's start with the index template.
Live Templates
# lib/feenix_web/live/user_live/index.html.leex
<h1>Listing Users</h1>
<%= if @live_action in [:new, :edit] do %>
<%= live_modal @socket, FeenixWeb.UserLive.FormComponent,
id: @user.id || :new,
title: @page_title,
action: @live_action,
user: @user,
return_to: Routes.user_index_path(@socket, :index) %>
<% end %>
<table>
# ommited for space
<span><%= live_redirect "Show", to: Routes.user_show_path(@socket, :show, user) %></span>
<span><%= live_patch "Edit", to: Routes.user_index_path(@socket, :edit, user) %></span>
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: user.id, data: [confirm: "Are you sure?"] %></span>
# ommited for space
</table>
<span><%= live_patch "New User", to: Routes.user_index_path(@socket, :new) %></span>
The first interesting thing on the page is the if @live_action
check, within which is a function live_modal
. After that, the familar table. Each row in the table has a link to "Show" "Edit" and "Delete". But take a look at them a bit more closely. The "Edit" link is not a link at all. It is live_patch
. And the href goes to the user_index_path
with the :edit
action. This is the @live_action
above. So clicking on that link will trigger the modal, since it will set @live_actiton
to :edit
Next the "Delete" link navigates to "#"
. It has an attribute phx_click: "delete"
. We will see how this works once we check out the live view.
Finally, the "New User" link at the bottom is also a live_patch
like "Edit" above, however this time the action is :new
.
Next, let's look at the Index LiveView proper
LiveViews
# lib/feenix_web/live/user_live/index.ex
defmodule FeenixWeb.UserLive.Index do
use FeenixWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :users, fetch_users())}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit User")
|> assign(:user, Accounts.get_user!(id))
end
def handle_event("delete", %{"id" => id}, socket) do
user = Accounts.get_user!(id)
{:ok, _} = Accounts.delete_user(user)
{:noreply, assign(socket, :users, fetch_users())}
end
# rest ommited for space
end
The mount
function sets up page initially. In this case, you can see that the socket is being assigned :users
with users fetched from the database
handle_params
is triggered by clicking on links with the live_patch
function. The @live_action
of :edit
, :new
, or :index
is set automatically, and the appropriate assigns are applied to the socket.
Finally, handle_event
is what was triggered with the phx_click
attribute. The attribute value is the first arguement to handle_event
.
The "show" template and live view is largely similar.
Live Components
Components are a mechanism to compartmentalize state, markup, and events in LiveView.
To see how this is being used in our app, first let's check out our live helpers at lib/feenix_web/live/live_helpers.ex
# lib/feenix_web/live/live_helpers.ex
def live_modal(socket, component, opts) do
path = Keyword.fetch!(opts, :return_to)
modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
live_component(socket, FeenixWeb.ModalComponent, modal_opts)
end
This explains the live_modal
functions in our templates from earlier. We see here that live_modal
is just a helper to invoke live_component
for our ModalComponent.
Check out the ModalComponent, we see some an inlined live view template, which itself calls live_component
with @component
. @component
here is our FormCoponent.
Components compartmentalize state, and we see for our FormComponent
, it looks like a live view all on its own. There is a key difference though, since the unlike the LiveView, a Component, is not a separate proess (it is part of its parent LiveView process), it does not have a handle_info
callback. Instead, from the parent LiveView, we can use send_update
to update the Component state.
The update
callback on Component is also invoked after mount
.
Closing thoughts
One of my favorite things about Elixir and Phoenix is that there isn't a lot of magic that happens. And the magic that does happen is at just the right point of abstraction. As a developer, you define templates which get populated but the socket assigns. If the assigns change, the rendered page changes. It's really that simple.
If you have any corrections to this post, or anything that you feel like should have been included, please feel free to reach out to me at joseph@joseph-lozano.com
Edit:
So, by adding our unique contraints, we actually broke a few of the tests that came with our generator. Let's fix them.
The root of the issue is that we are creating a fixture user as part of the setup. Let's just create another set of attributes @save_attrs %{email: "other@email.com", username: "some username"}
Then, update the failing test to use the new attrs.
# test/feenix_web/live/user_live_test.exs:45
{:ok, _, html} =
index_live
|> form("#user-form", user: @save_attrs)
|> render_submit()
|> follow_redirect(conn, Routes.user_index_path(conn, :index))
In future posts we will make use of TDD to avoid this issue in the future.
Originally posted at https://joseph-lozano.com
Top comments (1)
thanks for your post :)
Maybe after this step
mix phx.new feenix --live && cd feenix
you could add, edit this file
+++ b/config/dev.exs
@@ -8 +8 @@ config :feenix, Feenix.Repo,
and
mix ecto.create
Because, for someone, this could be the first contact with Phoenix.
Thanks again.