DEV Community

Lubien
Lubien

Posted on • Edited on

The Lazy Programmer's Intro to LiveView: Chapter 6

Go to Chapter 1

Creating the user page

Now that we have a way to list users in our application we should add a page that shows just one of them so later we can add features such as 'request match'. This chapter be very similar to the previous one with very few additions, you will be able to exercise more of the basics of LiveView that way.

Creating the route

live_session :current_user,
  on_mount: [{ChampionsWeb.UserAuth, :mount_current_user}] do
  live "/users/confirm/:token", UserConfirmationLive, :edit
  live "/users/confirm", UserConfirmationInstructionsLive, :new
  live "/users", UserLive.Index, :index
+ live "/users/:id", UserLive.Show, :show
end
Enter fullscreen mode Exit fullscreen mode

Back to the router.ex add a new route just beside your UserLive.Index. This time, since we are talking about a single user, we are going to call the module UserLive.Show using the :show action. If you pay attention to the URL you're going to see a variable there: :id. Phoenix calls those variables in the URL params.

Using params for the first time

Inside lib/champions_web/live/user_live/show.ex paste the following code:

defmodule ChampionsWeb.UserLive.Show do
  use ChampionsWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  @impl true
  def handle_params(_params, _session, socket) do
    {:noreply,
     socket
     |> assign(:page_title, "Show user")}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>I'm a user</div>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

This time we are looking at handle_params/3 a new LiveView callback. This callback is specifically meant to handle your LiveView params both when your view starts but also when those params change as we will be seeing in later chapters.

In short and omitting some cool details for now, that's the cycle of callbacks before your view is first rendered. Whenever you need to use params, you should prefer to use handle_params. One thing worth noting here: our mount/3 does absolutely nothing, it just returns {:ok, socket} so it's fine to delete it. How can we get the id from params then?

defmodule ChampionsWeb.UserLive.Show do
  use ChampionsWeb, :live_view

- @impl true
- def mount(_params, _session, socket) do
-   {:ok, socket}
- end

  @impl true
- def handle_params(_params, _session, socket) do
+ def handle_params(%{"id" => id}, _session, socket) do
    {:noreply,
     socket
-    |> assign(:page_title, "Show user")}
+    |> assign(:page_title, "Showing user #{id}")
+    |> assign(:id, id)}
  end

  @impl true
  def render(assigns) do
    ~H"""
-   <div>I'm a user</div>
+   <div>I'm the user <%= @id %></div>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

Instead of ignoring our params using the _ operator at the start of its name we changed the first argument of handle_params/3 to receive a Map that contains the key "id" and pattern matched it out. Now we have a variable id so we used that to update the page_title assign to be more specific and even created a new assign called id to be used on our render function as @id. So far we haven't actually fetched the user though.

defmodule ChampionsWeb.UserLive.Show do
  use ChampionsWeb, :live_view

+ alias Champions.Accounts

  @impl true
  def handle_params(%{"id" => id}, _session, socket) do
+   user = Accounts.get_user!(id)
    {:noreply,
     socket
-    |> assign(:page_title, "Showing user #{id}")
+    |> assign(:page_title, "Showing user #{user.email}")
-    |> assign(:id, id)}
+    |> assign(:user, user)}
  end

  @impl true
  def render(assigns) do
    ~H"""
-   <div>I'm the user <%= @id %></div>
+   <div>I'm the user <%= @user.email %></div>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

The first thing we needed to do was an alias to Champions.Accounts so we won't need to write Champions. when we use the Accounts context. After that we used Accounts.get_user!/1, a function created by Phoenix auth generator. We then updated our page_title to be more descriptive and changed from using an id assignment to an user assignment. Let's make this page slightly prettier. Change your render function to this:

@impl true
def render(assigns) do
  ~H"""
  <.header>
    User <%= @user.id %>
    <:subtitle>This is a player on this app.</:subtitle>
  </.header>

  <.list>
    <:item title="Email"><%= @user.email %></:item>
    <:item title="Points"><%= @user.points %></:item>
  </.list>

  <.back navigate={~p"/users"}>Back to users</.back>
  """
end
Enter fullscreen mode Exit fullscreen mode

You already know about the <.header> component albeit you're seeing how to use <:subtitle> inside it now but there are also two new components here for you and both come from Phoenix by default too. The first one is <.list> which is kinda like a horizontal table but instead of each row being an element on a list we use manually add each row using <:item>. Also, another fun component to use is <.back> which will simulate a back button press to the appointed path passed to it's navigate property.

Oh no, we just realized we have a way from going from /users/:id to /users but we don't have the other way around. Our users are in trouble. Let's go back to lib/champions_web/live/user_live/index.ex:

@impl true
def render(assigns) do
  ~H"""
  <.header>
    Listing Users
  </.header>

  <.table
    id="users"
    rows={@streams.users}
+   row_click={fn {_id, user} -> JS.navigate(~p"/users/#{user}") end}
  >
    <:col :let={{_id, user}} label="Email"><%= user.email %></:col>
    <:col :let={{_id, user}} label="Points"><%= user.points %></:col>
  </.table>
  """
end
Enter fullscreen mode Exit fullscreen mode

Phoenix got you covered. The <.table> component can receive an row_click attribute that receives a function that takes a single argument {id, element} and you can use JS.navigate/1 to change into the user page.

Adding tests!

Let's get back to test/champions_web/live/user_live_test.exs and add a new test suite:

describe "Show" do
  setup [:create_user]

  test "displays user", %{conn: conn, user: user} do
    {:ok, _show_live, html} = live(conn, ~p"/users/#{user}")

    assert html =~ "This is a player on this app"
    assert html =~ user.email
  end
end
Enter fullscreen mode Exit fullscreen mode

Nothing really magical here, we just go straight to the page and verify that the information that should be there is there.

Summary

  • Phoenix routes can receive variables using the :param_name syntax inside it's URL.
  • Params are passed to LiveViews inside both mount/3 and handle_params/3. They are run in that order before our render function.
  • Devs should prefer using params on handle_params/3 because it's aware of when params change in some specific cases we will dive in later.
  • Phoenix comes with a <.list> component for horizontal lists and <.back> for navigating back through pages.
  • The <.table> component can receive an row_click attribute to handle clicks on rows. We used that to navigate from the users list to the user page.

Read chapter 7: Conceding points to the winner

Top comments (0)