Lubien
Posted on June 18, 2023
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
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
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
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
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
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
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
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:
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
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 toChampionsWeb.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 onroot.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.
Posted on June 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.