The Lazy Programmer's Intro to LiveView: Chapter 5

lubien

Lubien

Posted on June 18, 2023

The Lazy Programmer's Intro to LiveView: Chapter 5

Go to Chapter 1

Listing our users

Before it's possible for users to create matches between themselves they must at least be able to know who else is on the platform. For this, we are going to build a user list page.

Creating Accounts.list_users

Now you should be aware how to add a new context function so let's speedrun through it. Head out to accounts.ex and add this function:

@doc """
List users.

## Examples

    iex> list_users()
    [%User{}]

"""
def list_users() do
  Repo.all(User)
end
Enter fullscreen mode Exit fullscreen mode

Unlike the previous function we created this time we are using Repo.all/2 to list all entries on the table corresponding to the User model. That should be enough for now. Let's add a new unit test to account_test.exs:

describe "list_users/0" do
  test "show all users on our system" do
    user = user_fixture()
    assert [^user] = Accounts.list_users()
  end
end
Enter fullscreen mode Exit fullscreen mode

The cool thing about the code above is that the assertion above just says 'create an user then listing users should return that exact same user' using the amazing pin operator (^). In case you never seen it in action, definitely take a look later. This should be enough for our context for now.

Creating your first new route

So far you got new routes for free using generators. Time to do it by hand. The first thing you want to check is your router.ex file under champions_web. There's a ton of new things here but to start simple, look for live_session :current_user, and we are going to put our new route there.

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
end
Enter fullscreen mode Exit fullscreen mode

Since we will be creating a LiveView we used the live/4 macro. The arguments we pass are the route path name, the LiveView file name and the live action. Since the LiveView we passed is called UserLive.Index that means we must define a module called ChampionsWeb.UserLive.Index. As for the live action it could be any atom but it's recommended to use :index, :show, :edit, :new, :create and :update whenever possible. We will take a look on them over the project's progress.

Creating the your first LivView

Create the lib/champions_web/live/user_live folder then make a file called index.ex. Let's start by creating your first LiveView ever.

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

  @impl true
  def render(assigns) do
    ~H"""
    <div>Hello LiveView</div>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

This is the minimal LiveView module you can create to show a Hello World. First of all, the module name is important: it should match what you defined inside router.ex to the letter prepended by ChampionsWeb. The second line contains the use macro to import useful things for LiveView modules defined under ChampionsWeb. If you're curious open champions_web.ex and look for def live_view do, we will talk about this at another time.

The real start of the module above really is the render/1 function. All LiveViews must have a render function that takes exactly one argument called assigns. Those functions often return a HEEx markup delimited by the H sigil (often wrote as ~H"""). This time it only returns a single div. Open http://localhost:4000/users to see this working.

Giving the page a title

LiveViews separate state management from view rendering. The rule of thumb is that your render function should only know how to render whatever it is in your assigns and the other LiveView functions, known as callbacks, are responsible of managing the state you need.

To get started let's meet your first LiveView callback: mount/3. This callback is responsible often for starting the initial state of your view and also is likely to fetch data from the server. Let's start with the simplest scenario.

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

This mount/3 callback receives parameters from the URL, the session state and the socket but ignore everything but the socket. All mount/3 must return a tuple with :ok as the first element and a socket as the second argument. Sockets maintain the state of your LiveView. We will talk more about that over time. The current state of that callback is the same as it didn't exist, it does nothing.

If you are on /users and look at your tab name you're going to see something generic including the name of your app. Let's improve that. Update your mount/3 callback to this:

@impl true
def mount(_params, _session, socket) do
  {:ok,
    socket
    |> assign(:page_title, "Listing Users")
  }
end
Enter fullscreen mode Exit fullscreen mode

Now instead of returning the socket as-is we used assign/3 to assign page_title to your socket state. Why page_title? This assign is used by root.html.heex to generate your HTML <title> tag. Looking at your tab name should show 'Listing Users · Phoenix Framework' now. You'll notice we will be adding page_title assign to most if not all pages from now on.

Using streams to list users

Recently LiveView added a new memory efficient way of handling collections of data: streams. For the sake of this chapter will overly simplify things and just say streams are used for lists of things. They behave a lot like assigns: you defined them on callbacks and your render function renders them. Let's rewrite out module:

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

  alias Champions.Accounts

  @impl true
  def mount(_params, _session, socket) do
    {:ok,
      socket
      |> assign(:page_title, "Listing Users")
      |> stream(:users, Accounts.list_users())
    }
  end

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

    <.table
      id="users"
      rows={@streams.users}
    >
      <:col :let={{_id, user}} label="Email"><%= user.email %></:col>
      <:col :let={{_id, user}} label="Points"><%= user.points %></:col>
    </.table>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

The first step to create a stream is, just like assigns, add it to the socket. With line 11 we create @streams.users containing the list of all users in our platform. Phoenix comes packed with a component called <.table> that is aware of streams and can render those just fine. All you need to do is pass two parameters to it: id and rows.

Inside the <.table> we can spot two <:col> slots, each representing a single column on our table. In short those part of the <.table> component as we used them to render each item from our stream. There's also the <.header> component but that's very self explanatory. Your /users page should look something like this:

A simple table listing one user with email lubien@example.com with 0 points.

Testing your LiveView

It brings me so much joy to say that testing LiveViews are not only easy but support for it comes by default. Since our LiveView file is lib/champions_web/live/user_live/index.ex our test file will live in test/champions_web/live/user_live_test.exs.

defmodule ChampionsWeb.UserLiveTest do
  use ChampionsWeb.ConnCase

  import Phoenix.LiveViewTest
  import Champions.AccountsFixtures

  defp create_user(_) do
    user = user_fixture()
    %{user: user}
  end

  describe "Index" do
    setup [:create_user]

    test "lists all users", %{conn: conn, user: user} do
      {:ok, _index_live, html} = live(conn, ~p"/users")

      assert html =~ "Listing Users"
      assert html =~ user.email
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The whole magic lies in live/2 from the Phoenix.LiveViewTest module. All you need to do is pass a conn, which already comes ready for tests that use ChampionsWeb.ConnCase and a route such as ~p"/users". The return value is a 3-tuple of :ok, a reference to the LiveView so we could do things like navigation or clicking buttons, and last but not least the rendered HTML so far. For this test all we need to do is check if the title and HTML contain what we expect such as the user email and the title on our render function.

Summary

  • Using Repo.all/2 its possible to query all items from an Ecto model.
  • To add new pages to Phoenix always start with the router.ex
  • Phoenix routes map to modules such as live "/users", UserLive.Index, :index mapping to ChampionsWeb.UserLive.Index
  • It's a common practice to call route actions as :index, :new, :create, :edit, :update and :delete but any other atom works such as :confirm_email.
  • To create a LiveView you start with creating the render function.
  • Render functions always take one argument: assigns.
  • LiveView manages the state via callback functions.
  • We can use the mount/3 callback to create the initial state of our LiveView.
  • Assigning something to page_title change the value of the <title> tag because that's used on root.html.heex
  • Assigns are just variables accessible on the LiveView render function using @assign_name.
  • Streams are how LiveView handles collections of data in a memory-efficient manner. Usually used for lists.
  • Streams are very like assigns: you defined and manage them on callbacks then use them on the render function as @streams.stream_name.
  • Phoenix comes with helper components such as <.header> and <.table>.
  • The <.table> component does all the stream's magic so we can learn that later.
  • Testing a LiveView can be quite easy with the live/2 helper.

Read chapter 6: Creating the user page

💖 💪 🙅 🚩
lubien
Lubien

Posted on June 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related