This is a guide for converting an existing Phoenix view into a LiveView. From Chris McCord’s announcement of the library:
“Phoenix LiveView is an exciting new library which enables rich, real-time user experiences with server-rendered HTML. LiveView powered applications are stateful on the server with bidirectional communication via WebSockets, offering a vastly simplified programming model compared to JavaScript alternatives.”
I won’t be going into detail about how all the features work, but even if you’ve never used it before you should be able to follow along and get up and running easily. We will be building a dead-simple example in order to maximize the likelihood that you’ll be able to easily grok the steps and use them in your own projects. I’ll start by quickly bootstrapping a blog application with a single resource, posts.
Note: If you are using a Phoenix version earlier than v1.5, make sure you
install liveview: https://hexdocs.pm/phoenix_live_view/installation.html#content
If your app already exists, skip ahead to step 1.
Step 0.5: Initialize example app
From your preferred directory, run this command:
mix phx.new blog --live
When asked if you want to fetch and install dependencies, say yes (Y)
Enter the blog folder and set up the database:
cd blog
mix ecto.create
Use the handy generator tool to create the Post controller, context and views:
mix phx.gen.html Posts Post posts title:string body:text
As prompted, add the posts resource to your router in the browser scope:
resources "/posts", PostController
Now run migrations:
mix ecto.migrate
Now you can boot up your server with mix phx.server
and visit localhost:4000/posts
to see your (empty) list of posts. Follow the links to create a new post and save it to the database. Back on the posts index page you’ll see the default links for looking at, editing and deleting your new post. This is pretty much the simplest possible Phoenix app. Now here we go with the cool stuff: converting this default index page into a live view, so that when you create, update or delete posts any browser window that’s pointed at the page will update automatically, no refresh required.
1: Define live route
First we need to define a route to our live view. In your router.ex
file, after pipe_through: :browser
, add something like this:
live “/postslive”, PostsLive.Index
If you hit localhost4000/postslive in your browser right now you should get an error because the module BlogWeb.PostsLive
is not available. So let’s go create it.
2. Define module
Create a directory under lib/blog_web
called live
. This is where your live-view related code will live. Add another new directory called posts_live
. Within this directory add a file called index.ex
. Inside that file define the module BlogWeb.PostsLive.Index
like so:
defmodule BlogWeb.PostsLive.Index do
use BlogWeb, :live_view
end
Now our module is defined, but we get an error from Phoenix because render
is not implemented. We could add a render
function to our module, satisfying the error, but we’re going to take a different approach.
3. Create live template
Create a file in the blog_web/live/posts_live
folder called index.html.heex
. Now, go into the templates/post
folder (where the non-live views live) and copy the contents from index.html.heex
into lib/blog_web/live/posts_live/index.html.heex
. We have duplicated our old ‘dead’ view and must now plug in the wires to make the template work as a live view.
The first problem we must solve: since our view is no longer being rendered from the posts
controller, we don’t have access to our lists of posts, @posts
, like we used to. We need to get that data to the live view another way.
4. Assign resources to socket
Head back to the index.ex
module and define a mount
function. This function takes three arguments: params
, session
and socket
. We’re only going to use the socket
so you can prefix the others with an underscore to show that we don’t care about them. Manually assign our list of posts to the socket and then return the socket, like this:
defmodule BlogWeb.PostsLive.Index do
use BlogWeb, :live_view
alias Blog.Posts
def mount(_params, _session, socket) do
socket = assign(socket, posts: Posts.list_posts())
{:ok, socket}
end
end
Now we have a new error from Phoenix: key :conn not found
. This one is an easy fix.
5. Change mentions of @conn
to @socket
in live template
Go into the posts_live/index.html.heex
template and replace all instances of @conn
with @socket
. Boom! Your live template should load without error and look exactly like your original index page. There’s only one problem: it isn’t live. In order to fix that we need to leverage Phoenix PubSub.
6. Enable PubSub subscription
Head to lib/blog/posts.ex
and add this:
@topic inspect(__MODULE__)
def subscribe do
Phoenix.PubSub.subscribe(Blog.PubSub, @topic)
end
This will allow our live view and other modules to subscribe to events that we broadcast. Speaking of which, let’s set up the broadcast.
7. Broadcast changes
Still in lib/blog/posts.ex
, add a private function to broadcast changes:
defp broadcast_change({:ok, result}, event) do
Phoenix.PubSub.broadcast(Blog.PubSub, @topic, {__MODULE__, event, result})
{:ok, result}
end
Then go into your create_post
function and call broadcast_change
at the end, like this:
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
|> broadcast_change([:post, :created])
end
You can use this pattern wherever you want changes broadcasted. Create, update, delete functions are good candidates for broadcasting these messages, since other modules will likely want to respond to these changes..
8. Subscribe to broadcast
Now we can head back to posts_live/index.ex
and subscribe to the broadcast we just set up. Add this to mount
:
Posts.subscribe()
Our live view is now listening to events broadcasted from Posts, but we need to reflect those changes in the UI. One last step!
9. Handle Broadcast Messages
Add a new function to posts_live/index.ex
called handle_info
:
def handle_info({Posts, [:post | _action], _post}, socket) do
socket = assign(socket, posts: Posts.list_posts())
{:noreply, socket}
end
All we’re doing here is responding to an incoming broadcast from Posts by fetching the list of posts again and assigning it to the socket. While this is a basic example, you may add handle_info
callbacks for any subscribed PubSub topics which are relevant to the live view. Pattern matching allows you to respond to broadcasted messages in a granular way.
Now if you have two browser windows pointed at /liveposts
a post created in one window will automatically appear in the other. Pretty cool!
Conclusion
That’s it! Hopefully this simple example will provide a map for you to quickly implement live views in your pre-existing Phoenix projects.
Header Photo by Marek Piwnicki on Unsplash