Setting up a new Phoenix 1.5 project! (With Phoenix LiveView!)


elixir phoenix tutorials

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.
  • In part 3, we will allow users to change their email addresses.

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

  1. mix phx.gen.live New live view generators in Phoenix 1.5. The live view equivalent of mix phx.gen.html
  2. Accounts The name of our domain context
  3. User The module name for the schema we are creating
  4. users The name of the database field attached to the schema
  5. 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.