First contact with Phoenix LiveView

Course Index page

Code


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.

LiveView rendered page

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!

New trades automatically shown on the browser

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.

LiveView trades