Converting A Traditional Controller Driven App To Phoenix LiveView - Part 1
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:
Have you played with @elixirphoenix #LiveView yet? It’s *INSANE*. This is a quick test app re-computing and re-rendering over 200 rows of data every 1/10 of a second. I wrote no JS code. Bravo @chris_mccord and team! #MyElixirStatus pic.twitter.com/E1oEkje8Ji
— Geoffrey Lessel (@geolessel) March 20, 2019
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.
Buy my book—Phoenix in Action
I've been working hard on the first book on the Phoenix framework from Manning Publications, Phoenix in Action. If you like what you've been reading and/or you have an interest in learning Phoenix, please purchase the book today! Want to know more, check out my blog post announcing the book or the one announcing its completion.