Simple Phoenix LIveView App: Show & Edit Pages

This is episode #8 of a Phoenix LiveView series. Last episode we converted the new users page and form into live views. We also had a chance to see what LiveView form validations look like.

In this episode, we'll build the live views and templates for the show and edit pages, fix a few bugs and finish converting users from traditional CRUD to LiveView. After this episode, we won't need the user controller at all.

Note: This episode is using Phoenix LiveView, version 0.5.1. If you've been following along with the series you'll be fine but if you've already upgraded to a newer version you'll hit some issues. We'll be upgrading LiveView next episode.

The templates

The first thing we'll do is update the link tags in our show.html.eex and edit.html.eex templates to use LiveView. That means changing link to live_link and changing path to live_path and updating the extension.

In edit.html.leex

<%= live_link "Back", to: Routes.live_path(@socket, UserLive.Index) %>

In show.html.leex

<%= live_link "Edit", to: Routes.live_path(@socket, UserLive.Edit, @user) %>
<%= live_link "Back", to: Routes.live_path(@socket, UserLive.Index) %>

Create the LiveViews

First, create user_live/show.ex:

defmodule ReactorWeb.UserLive.Show do
  use Phoenix.LiveView
  alias Reactor.Accounts
  alias ReactorWeb.{UserLive, UserView}
  alias ReactorWeb.Router.Helpers, as: Routes
  alias Phoenix.LiveView.Socket

  def render(assigns), do: UserView.render("show.html", assigns)
  def mount(_session, socket), do: {:ok, socket}

  def handle_params(%{"id" => id}, _url, socket) do
    if connected?(socket), do: Accounts.subscribe(id)
    {:noreply, socket |> assign(id: id) |> fetch()}
  end

  def fetch(%Socket{assigns: %{id: id}} = socket) do
    assign(socket, user: Accounts.get_user!(id))
  end

  def handle_info({Accounts, [:user, :updated], _}, socket) do
    {:noreply, fetch(socket)}
  end

  def handle_info({Accounts, [:user, :deleted], _}, socket) do
    {:stop,
     socket
     # |> put_flash(:error, "This user has been deleted.")
     |> redirect(to: Routes.live_path(socket, UserLive.Index))}
  end
end

This is very similar to the LiveView we created last episode. We've got a render function that just passes the template file and the assigns into UserView.render, and we've got a mount function that returns our initial socket. As before, we're using the generated functions in our Accounts context to do our basic CRUD as well as the subscribe function we added to it.

The major new piece for this file is the handle_info function, which matches the assigns we got from the router, and gets the ID of the user being shown.

Our LiveView for edit.ex is a bit more complex:

defmodule ReactorWeb.UserLive.Edit do
  use Phoenix.LiveView
  alias Phoenix.LiveView.Socket
  alias Reactor.Accounts
  alias Accounts.User
  alias ReactorWeb.{UserLive, UserView}
  alias ReactorWeb.Router.Helpers, as: Routes
  alias Phoenix.LiveView.Socket

  def render(assigns), do: UserView.render("edit.html", assigns)
  def mount(_session, socket), do: {:ok, socket}

  def handle_params(%{"id" => id}, _url, socket) do
    if connected?(socket), do: Accounts.subscribe(id)
    {:noreply, socket |> assign(id: id) |> fetch()}
  end

  defp fetch(%Socket{assigns: %{id: id}} = socket) do
    user = Accounts.get_user!(id)
    assign(socket, user: user, changeset: Accounts.change_user(user))
  end

  def handle_event("validate", %{"user" => params}, socket) do
    cset =
      socket.assigns.user
      |> Accounts.change_user(params)
      |> Map.put(:action, :update)

    {:noreply, assign(socket, changeset: cset)}
  end

  def handle_event("save", %{"user" => params}, socket) do
    case Accounts.update_user(socket.assigns.user, params) do
      {:ok, user} ->
        {:stop,
         socket
         #  |> put_flash(:info, "user updated")
         |> redirect(to: Routes.live_path(socket, UserLive.Show, user))}

      {:error, %Ecto.Changeset{} = cset} ->
        {:noreply, assign(socket, changeset: cset)}
    end
  end

  def handle_info({Accounts, [:user, :updated], _}, socket) do
    {:noreply, fetch(socket)}
  end

  def handle_info({Accounts, [:user, :deleted], _}, socket) do
    {:stop,
     socket
     # |> put_flash(:error, "This user has been deleted.")
     |> redirect(to: Routes.live_path(socket, UserLive.Index))}
  end
end

All the pieces are familiar though. Like new.ex, we need to pass a changeset to the template so form actions can make changes to our DB. We'll also need the same event handlers (but have them call update rather than create). Like show.ex, we need to add handle_info pages to reflect any external changes to the user being shown on the page.

Notifying Subscribers (Account Context)

Note that the Accounts.notify_subscribers/2 function broadcasts the module, the event which has just occurred and the "result" (AKA data needed by the LiveView) to all clients currently subscribed, and then it passes the exact same {:ok, result} or {:error, reason} tuple passed in as its first argument.

This is why we can drop the function into our CRUD pipelines without changing the data being passed through.

  defp notify_subscribers({:ok, result}, event) do
    Phoenix.PubSub.broadcast(Reactor.PubSub, @topic, {__MODULE__, event, result})

    Phoenix.PubSub.broadcast(
      Reactor.PubSub,
      @topic <> "#{result.id}",
      {__MODULE__, event, result}
    )

    {:ok, result}
  end

  defp notify_subscribers({:error, reason}, _), do: {:error, reason

(Source code available for premium members)

Next time, we'll update LiveView to version 0.8.1 and also get the /foo page updated.

Back to index

No Comments