Converting A Traditional Controller Driven App To Phoenix LiveView - Part 1

September 25, 2019 8 minutes read

This is part 1 of a 2 part post. In this one, we'll move from a traditional Phoenix controller-driven application to one that is live-updating with Phoenix LiveView. In part 2, we'll add some interaction with the ability to comment.

Versions used

Elixir 1.9.1-otp-22
Phoenix 1.4.10
Phoenix LiveView 0.2.0

I recently got back from ElixirConf 2019 and Phoenix LiveView was undoubtedly the star of the conference. There were many talks on how awesome it is, how to utilize it, and Chris McCord’s keynote was excellent. In truth, it is a very exciting technology and has the potential to change the way real-time web applications are built.

Shortly after Phoenix LiveView was first released, I played around with it just a little bit:

However, I decided to dive a little bit deeper for a talk I recently gave at Full Stack Talks in north San Diego County. This post is a modified written version of the talk I gave that night. If you’d rather watch the talk than read the text, you can view it here:

The setup

This is part 1 of a 2 part post. In this one, we’ll move from a traditional Phoenix controller-driven application to one that is live-updating. In part 2, we’ll add some interaction with the ability to comment. If you watch the video above, you’ll see that I went over both during the meetup in case you want to see what’s coming.

Our task is to convert a traditional controller-driven Phoenix app to one that uses Phoenix LiveView. The application we are working on is a price tracker for 500 stocks traded on the Nasdaq exchange. In the background, there is a process that fetches data from whatever pricing source the application is connected to. Currently, our users have to refresh their browser to get see this new information. We’d like to update the application to use Phoenix LiveView to push the new information to the user when it is received.

A few notes on the price updater

I mentioned above that there is a process that fetches data from a source. For this sample application, that is abstracted away. In reality, a GenServer is running on a timer. When the GenServer is initialized, it fetches basic company information from a database and binds them to a companies variable that it keeps in state. It also initializes a price attribute for the company. Every two seconds, it iterates through all 500 companies and generates a new price for the company. Two-thirds of the time, the price will increase by a random amount between 0 and 0.2 percent of the current price; one-third of the time it will decrease by a similar amount.

Furthermore, this GenServer acts as a PubSub. It allows processes to register themselves. Once a process is registered, it will receive an :update message whenever prices are updated.

The code that handles the :update_prices message on each timer message is below.

# lib/fullstack/fake_db.ex

def handle_info(:update_prices, state) do
  companies =
    state.companies
    |> Flow.from_enumerable()
    |> Flow.map(&Fullstack.update_latest_price_for_company/1)
    |> Enum.to_list()

  state.listeners
  |> Enum.each(fn pid ->
    :ok = Process.send(pid, :update, [])
  end)

  {:noreply, %{state | companies: companies}}
end

Router, Controller, and View Template

The router, controller, and view template are pretty simple and are typical for what you’d see in a simple application like this.

Router

A GET request to the root of our application (/) will end up being routed to FullstackWeb.PageController.index/2.

# lib/fullstack_web/router.ex
# ... typical router setup stuff ...

scope "/", FullstackWeb do
  pipe_through :browser

  get "/", PageController, :index
end

Controller

Once the request hits the controller, it is again pretty typical. Our function simply requests all the companies in the database (including with their current price information) and passes that list as a companies assign to the index.html template.

# lib/fullstack_web/controllers/page_controller.ex
defmodule FullstackWeb.PageController do
  use FullstackWeb, :controller

  def index(conn, _params) do
    companies = Fullstack.FakeDB.get_companies()
    render(conn, "index.html", companies: companies)
  end
end

View Template

Finally, the user’s request hits gets to the view template which returns the HTML that is sent to the user’s browser. The important part of the code is below, in which we render a row of data for each company in the @companies assign.

# lib/fullstack_web/templates/index.html.eex
# ... table setup code including thead ...

<%= for company <- @companies do %>
  <tr>
    <td><%= company.symbol %></td>
    <td><%= company.name %></td>
    <td class="right">
      <%= :erlang.float_to_binary(company.latest_price, [decimals: 2]) %>
    </td>
  </tr>
<% end %>

Say goodbye to the controller

In order to keep up with other traders, we need the new information pushed to us whenever it changes. Who has time to keep manually refreshing the browser?

The first step in converting our conventional controller-driven application is to get rid of the controller. We can render the same information—even statically—with LiveView.

Add the dependency

First, we need to make sure that phoenix_live_view is included in our list of dependencies. It is not included with the main Phoenix dependency so we need to specify that we need it in our application. Add {:phoenix_live_view, "~> 0.2.0"} to your mix.exs file.

Add four lines of javascript

The only javascript we will be touching in our application is the four lines straight out of the setup guide for LiveView. These lines simply set up the socket connections required for LiveView to do its magic.

// in assets/app.js
import {Socket} from "phoenix"
import LiveSocket from "phoenix_live_view"

let liveSocket = new LiveSocket("/live", Socket)
liveSocket.connect()

Set up the live socket in the Endpoint

Our Endpoint needs to know that we expect LiveView socket connections. Add this line.

# lib/fullstack_web/endpoint.ex

socket "live", Phoenix.LiveView.Socket

Add some salt

In order to securely sign the network traffic over the LiveView socket, we need to set a signing_salt for LiveView in the Endpoint’s config.

# in config/config.exs

config :fullstack, FullstackWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: "something-secret",
  render_errors: [view: FullstackWeb.ErrorView, accepts: ~w(html json)],
  pubsub: [name: Fullstack.PubSub, adapter: Phoenix.PubSub.PG2],
  live_view: [signing_salt: "mmmm-salty"]      # <-- Add this!

Rename the template

The template file that renders the table of companies is named index.html.eex. However, LiveView expects any templates that require live updating to have the .leex extension. All we need to do is rename lib/fullstack_web/templates/page/index.html.eex to lib/fullstack_web/templates/page/index.html.leex.

Update the router

Next, we need to update the router to route requests for the root of our application to the LiveView version of our site.

defmodule FullstackWeb.Router do
  use FullstackWeb, :router
  import Phoenix.LiveView.Router     # <-- add this

  # ... other setup stuff ...

  scope "/", FullstackWeb do
    pipe_through :browser

    live "/", PageLive               # <-- change this
  end
end

Define FullstackWeb.PageLive

We’ve routed all root requests to FullstackWeb.PageLive but we have yet to define that module. Here’s where the magic starts to happen and we start to get into the real implementation of LiveView.

# lib/fullstack_web/live/page_live.ex

defmodule FullstackWeb.PageLive do
  use Phoenix.LiveView
  alias Fullstack.FakeDB

  def mount(_session, socket) do
    {:ok,
     assign(socket,
       companies: FakeDB.get_companies()
     )}
  end

  def render(assigns) do
    FullstackWeb.PageView.render("index.html", assigns)
  end
end

Let’s break this down bit by bit.

use Phoenix.LiveView

This will bring in all the functions necessary to set up a LiveView. It expects us to define two functions: mount/2 and render/1.

mount/2

mount/2 is called when a new connection is established and is passed the session information and the socket. We can use this to set up the initial assigns that our template needs. This only runs once per connection. As a return value, it expects a tuple containing the :ok Atom and a socket.

Our implementation takes the provided socket and binds the list of companies in our fake database to companies in the socket’s assigns with the assign/2 function.

render/1

render/1 is tasked with pushing the result of its function to the client. It is passed socket.assigns as its one argument.

Our implementation simply passes the responsibility of rendering to FullstackWeb.PageView, making sure to pass on the assigns it receives.

Restart the server and refresh the browser

If you’ve done everything correctly, all we need to do now is restart the Phoenix server (since we’ve modified config/config.exs) and refresh the browser. If you see the same HTML rendered as before (with different prices), then our refactor was successful!

Enable Live Updating

We now have requests being routed to FullstackWeb.PageLive instead of FullstackWeb.PageController. Believe it or not, that was the hard part!

Register the connection to receive updates

I mentioned earlier in this post that there is a GenServer that updates the prices of the companies every two seconds. It has a register/1 function that we can use to register browser connection processes in order to receive an :update message whenever new data is available. We need to register every time a browser connects, but only one time. Where should we do this registration?

If you answered PageLive.mount/2, you get a gold star! Since mount/2 is called once per process on initial mount, we can use this function to register the current process with our fake database.

# lib/fullstack_web/live/page_live.ex

def mount(_session, socket) do
  if connected?(socket), do: FakeDB.register(self())  # <-- add this

  {:ok,
   assign(socket,
     companies: FakeDB.get_companies()
   )}
end

Handle the :update messages

Since we are now registered, PageLive will now receive an :update message every two seconds. PageLive is currently not set up to handle that message so we need to define a message handler function.

Whenever FakeDB tells us that there is an update available, we will simply pull in the new updated list of companies and assign that to the current socket. render/1 will then receive that information and push the changes to the browser. Finally, we will indicate that we will no send a reply to the message sender.

# lib/fullstack_web/live/page_live.ex

def handle_info(:update, socket) do
  {:noreply, assign(socket, companies: FakeDB.get_companies())}
end

Enjoy your live updates!

That’s it! Your browser’s connection to Phoenix is now a live one! Any time the fake database sends out an update, PageLive will push the new list of companies to the browser through the socket connection.

Coming In Part 2

In part 2, we’ll add a live chat and push the updating frequency waaay up.

If you have any questions or comments, hit me up on Twitter at @geolessel or in the Elixir Slack community at @geo.

Updated: