Code
Links
Our first Live View
In this lesson we’ll introduce LiveView and get a sense of how powerful it can be. In the next lesson we’ll see better how LiveView works under the hood.
We start by defining our first LiveView module in lib/poeticoins_web/live/crypto_dashboard_live.ex
, called PoeticoinsWeb.CryptoDashboardLive
.
In this module we need to implement two callbacks: mount/3
and render/1
. We are not going too much into detail, we’ll see better in the next lesson how these callbacks are part of the LiveView’s life-cycle. By now we just need to know that:
- a LiveView is a process and each connected user is served by a separate LiveView process.
mount/3
is the LiveView entry-point. Here we initialize the state when a user connects.render/1
is responsible for returning the rendered content.
We first implement the mount(_params, _session, socket)
callback, ignoring the first two arguments and using only the third one, which is a %Phoenix.LiveView.Socket{}
. We can add key/value pairs to the socket
using the assign/3
function. We can then refer to these pairs in the view’s template.
#lib/poeticoins_web/live/crypto_dashboard_live.ex
defmodule PoeticoinsWeb.CryptoDashboardLive do
use PoeticoinsWeb, :live_view
alias Poeticoins.Product
@impl true
def mount(_params, _session, socket) do
product = Product.new("coinbase", "BTC-USD")
trade = Poeticoins.get_last_trade(product)
socket = assign(socket, :trade, trade)
{:ok, socket}
end
end
In the mount/3
callback we get the last trade just for the Coinbase BTC-USD product, then we assign/3
this trade to the socket
using the :trade
key.
We can now implement the render/1
callback. At the moment we’ll just render the last Coinbase BTC-USD trade details.
In the render/1
callback we can use the ~L
sigil to inline LiveView templates. At the beginning, especially when playing with small views, it’s pretty useful to have everything in one module. We can obviously have a LiveView template in a separate file.
#lib/poeticoins_web/live/crypto_dashboard_live.ex
def render(assigns) do
~L"""
<h2>
<%= @trade.product.exchange_name %> -
<%= @trade.product.currency_pair %>
</h2>
<p>
<%= @trade.traded_at %> -
<%= @trade.price %> -
<%= @trade.volume %>
</p>
"""
end
Like a classic EEx template, we refer to the assigned trade using @trade
.
At the moment we don’t handle the fact that the trade could be nil
. The product is quite traded and the Historical
should have a trade almost immediately after the application is started.
The view is ready, we just need to update the router.
defmodule PoeticoinsWeb.Router do
...
scope "/", PoeticoinsWeb do
pipe_through :browser
live "/", CryptoDashboardLive
end
end
We add a live
route, which points to the PoeticoinsWeb.CryptoDashboardLive
LiveView.
Once started the server (mix phx.server
), we see the most recent trade information rendered on the browser.
Obviously it seems a similar result to what we’ve got with a normal controller, it doesn’t update. Let’s go back to our CryptoDashboardLive
module and see how to handle new trades.
First, to get new trades we need to subscribe the LiveView process to the product’s PubSub topic, calling Poeticoins.subscribe_to_trades/1
. We do it when a user connects, in mount/3
.
#lib/poeticoins_web/live/crypto_dashboard_live.ex
def mount(_params, _session, socket) do
product = Product.new("coinbase", "BTC-USD")
trade = Poeticoins.get_last_trade(product)
if socket.connected? do
Poeticoins.subscribe_to_trades(product)
end
socket = assign(socket, :trade, trade)
{:ok, socket}
end
In the next lesson we’ll see why we need to check that socket.connected?
.
Remember that this LiveView is a process, it will receive new trade messages from PubSub. To handle these messages we need to implement the handle_info/2
callback.
#lib/poeticoins_web/live/crypto_dashboard_live.ex
def handle_info({:new_trade, trade}, socket) do
socket = assign(socket, :trade, trade)
{:noreply, socket}
end
We match the :new_trade
message, getting the new trade, and we update the socket assigning the new :trade
.
Every time we assign or update values in the socket, the view is re-rendered and the changes are sent to the browser. Then, the LiveView’s JavaScript running on the browser applies those changes.
It’s time to restart the server and refresh the page!
We see that the view updates automatically! We see the new trades on the browser and we didn’t have to deal with any JavaScript.
This is fantastic! In this way we can just focus on our data, and every time we change the data, the rendered changes are automatically sent to the browser.
Render trades for all the products
To render the trades for all the available products, we need to make few changes. First we refactor mount/3
.
#lib/poeticoins_web/live/crypto_dashboard_live.ex
def mount(_params, _session, socket) do
# list of products
products = Poeticoins.available_products()
# trade list to a map %{Product.t => Trade.t}
trades =
products
|> Poeticoins.get_last_trades()
|> Enum.reject(&is_nil(&1))
|> Enum.map(&{&1.product, &1})
|> Enum.into(%{})
if socket.connected? do
Enum.each(products, &Poeticoins.subscribe_to_trades(&1))
end
socket = assign(socket, trades: trades, products: products)
{:ok, socket}
end
We use Poeticoins.available_products/0
to get a list of all the products.
Then we convert the trade list, returned by Poeticoins.get_last_trades(products)
, to a map %{Product.t => Trade.t}
where the key is a product and the value is the most recent trade for that product. This will make updates much easier.
Then we subscribe the LiveView process to receive {:new_trade, trade}
messages for all the products.
This time we assign/2
to the socket both :trades
and :products
.
We can now rewrite our template in render/1
.
#lib/poeticoins_web/live/crypto_dashboard_live.ex
def render(assigns) do
~L"""
<table>
<thead>
<th>Traded at</th>
<th>Exchange</th>
<th>Currency</th>
<th>Price</th>
<th>Volume</th>
</thead>
<tbody>
<%= for product <- @products, trade = @trades[product], not is_nil(trade) do%>
<tr>
<td><%= trade.traded_at %></td>
<td><%= trade.product.exchange_name %></td>
<td><%= trade.product.currency_pair %></td>
<td><%= trade.price %></td>
<td><%= trade.volume %></td>
</tr>
<% end %>
</tbody>
</table>
"""
end
This time we render a table, similar to what we had in the ProductController
. Instead of enumerating the @trades
directly, we enumerate the @products
to maintain the same order. For each product
we get a trade
from the @trades
map. If the @trades
map doesn’t have the product
key yet, trade
will be nil, so we need to filter it out with not is_nil(trade)
.
We now need to refactor the handle_info({:new_trade, trade}, socket)
callback. Every time we receive a new trade, we need to update the trades
map in the socket.
#lib/poeticoins_web/live/crypto_dashboard_live.ex
def handle_info({:new_trade, trade}, socket) do
socket = update(socket, :trades, fn trades ->
Map.put(trades, trade.product, trade)
end)
{:noreply, socket}
end
This time, instead of using assign/3
, we use update/3
which works pretty similarly to Map.update/4
. As the third argument we pass a function that returns the updated value, in this case we update the map with the new trade.
And this is all we need to render a table, with all the products, that gets updated automatically every time the app receives a new trade.