Walk-Through of Phoenix LiveView

19 Mar 2019 · by Sophie DeBenedetto in Today I Learned

It’s here! Phoenix LiveView leverages server-rendered HTML and Phoenix’s native WebSocket tooling so you can build fancy real-time features without all that complicated JavaScript. If you’re sick to death of writing JS (I had a bad day with Redux, don’t ask), then this is the library for you!

Phoenix LiveView is brand brand new so I thought I’d provide a short write-up of a super simple demo I built for anyone looking to get up and running. Keep in mind that the library is still a release candidate and as such, is subject to change.

What is LiveView?

Chris McCord said it best in his announcement back in December:

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.

Kill Your JavaScript

If you’ve waded through an overly complex SPA that Reduxes all the things (for example), you’ve felt the maintenance and iteration costs that often accompany all that fancy JavaScript.

Phoenix LiveView feels like a perfect fit for the 90% of the time that you do want some live updates but don’t actually need the wrecking ball of many modern JS frameworks.

Let’s get LiveView up and running to support a feature that pushes out live updates as our server enacts a step-by-step process of creating a GitHub repo.

Here’s the functionality we’re building:


Getting Started

The following steps are detailed in Phoenix LiveView Readme.

  • Install the dependency in your mix.exs file:
def deps do
  [
    {:phoenix_live_view, "~> 0.2.0"}
  ]
end
  • Update your app’s endpoint configuration with a signing salt for your live view connection to use:
# Configures the endpoint
config :my_app, MyApp.Endpoint,
  ...
  live_view: [
    signing_salt: "YOUR_SECRET"
  ]

Note: You can generate a secret by running mix phx.gen.secret from the command line.

  • Update your configuration to enable writing LiveView templates with the .leex extension.
config :phoenix,
  template_engines: [leex: Phoenix.LiveView.Engine]
  • Add the live view flash plug to your browser pipeline, after :fetch_flash
pipeline :browser do
  ...
  plug :fetch_flash
  plug Phoenix.LiveView.Flash
end
  • Import the following in your lib/app_web.ex file:
def view do
  quote do
    ...
    import Phoenix.LiveView, only: [live_render: 2, live_render: 3]
  end
end

def router do
  quote do
    ...
    import Phoenix.LiveView.Router
  end
end
  • Expose a socket for LiveView to use in your endpoint module:

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint

  socket "/live", Phoenix.LiveView.Socket

  # ...
end
  • Add LiveView to your NPM dependencies:
# assets/package.json
{
  "dependencies": {
    ...
    "phoenix_live_view": "file:../deps/phoenix_live_view"
  }
}

You’ll need to run npm install after this step.

  • Use the LiveView JavaScript library to connect to the LiveView socket in app.js
import LiveSocket from "phoenix_live_view"

let liveSocket = new LiveSocket("/live")
liveSocket.connect()
  • Your live views should be saved in the lib/my_app_web/live/ directory. For live page reload support, add the following pattern to your config/dev.exs:
config :demo, MyApp.Endpoint,
  live_reload: [
    patterns: [
      ...,
      ~r{lib/my_app_web/live/.*(ex)$}
    ]
  ]

Now we’re ready to build and render a live view!

Rendering a Live View from the Controller

You can serve live views directly from your router. However, in this example we’ll teach our controller to render a live view. Let’s take a look at our controller:

defmodule MyApp.PageController do
  use MyApp, :controller
  alias Phoenix.LiveView

  def index(conn, _) do
    LiveView.Controller.live_render(conn, MyAppWeb.GithubDeployView, session: %{})
  end
end

We’re calling on the live_render/3 function which takes in an argument of the conn, the live view we want to render, and any session info we want to send down into the live view.

Now we’re ready to define our very own live view.

Defining the Live View

Our first live view will live in my_app_web/live/github_deploy_view.ex. This view is responsible for handling an interaction whereby a user “deploys” some content to GitHub. This process involves creating a GitHub organization, creating the repo and pushing up some contents to that repo. We won’t care about the implementation details of this process for the purpose of this example.

Our live view will use Phoenix.LiveView and must implement two functions: render/1 and mount/2.

defmodule MyAppWeb.GithubDeployView do
  use Phoenix.LiveView

  def render(assigns) do
    ~L"""
    <div class="">
      <div>
        <%= @deploy_step %>
      </div>
    </div>
    """
  end

  def mount(_session, socket) do
    {:ok, assign(socket, deploy_step: "Ready!")}
  end
end

Now that we have the basic pieces in place, let’s break down the live view process.

How It Works

The live view connection process looks something like this:

When our app receives an HTTP request for the index route, it will respond by rendering the static HTML defined in our live view’s render/1 function. It will do so by first invoking our view’s mount/2 function, only then rendering the HTML populated with whatever default values mount/2 assigned to the socket.

The rendered HTML will include the signed session info. The session is signed using the signing salt we provided to our live view configuration in config.exs. That signed session will be sent back to the server when the client opens the live view socket connection. If you inspect the page rendering your live view in the browser, you’ll see that signed session:

<div
  id="phx-20gvOvqvFMA="
  data-phx-view="MyApp.GithubDeployView"
  data-phx-session="SFMyNTY.g3QAAAACZAAEZGF0YW0AAACQZzNRQUFBQUVaQUFDYVdS">
  ...
</div>

Once that static HTML is rendered, the client will send the live socket connect request thanks to this snippet:

import LiveSocket from "phoenix_live_view"

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

This initiates a stateful connection that will cause the view to be re-rendered anytime the socket updates. Since the page first renders as static HTML, we can rest assured that our page will always render for our user, even if JavaScript is disabled in the browser.

Now that we understand how the live view is first rendered and how the live view socket connection is established, let’s render some live updates.

Rendering Live Updates

LiveView is listening to updates to our socket and will re-render only the portions of the page that need updating. Taking a closer look at our render/1 function, we see that it renders the values of the keys assigned to our socket.

Where mount/2 assigned the values :deploy_step, our render/1 function renders them like this:

def render(assigns) do
  ~L"""
  <div class="">
    <div>
      <%= @deploy_step %>
    </div>
  </div>
  """
end

Note: The ~L sigil represents Live EEx. This is the built-in LiveView template. Unlike .eex templates, LEEx templates are capable of tracking and rendering only necessary changes. So, if the value of @deploy_step changes, our template will re-render only that portion of the page.

Let’s give our user a way to kick off the “deploy to GitHub” process and see the page update as each deploy step is enacted.

LiveView supports DOM element bindings to give us the ability to respond to client-side events. We’ll create a “deploy to GitHub” button and we’ll listen for the click of that button with the phx-click event binding.

def render(assigns) do
  ~L"""
  <div class="">
    <div>
      <div>
        <button phx-click="github_deploy">Deploy to GitHub</button>
      </div>
      Status: <%= @deploy_step %>
    </div>
  </div>
  """
end

The phx-click binding will send our click event to the server to be handled by GithubDeployView. Events are handled in our live views by the handle_event/3 function. This function will take in an argument of the event name, a value and the socket.

There are a number of ways to populate the value argument, but we won’t use that data point in this example.

Let’s built out our handle_event/3 function for the "github_deploy" event:

def handle_event("github_deploy", _value, socket) do
  # do the deploy process
  {:noreply, assign(socket, deploy_step: "Starting deploy...")}
end

Our function is responsible for two things. First, it will kick off the deploy process (coming soon). Then, it will update the value of the :deploy_step key in our socket. This will cause our template to re-render the portion of the page with <%= @deploy_step %>, so the user will see Status: Ready! change to Status: Starting deploy....

Next up, we need the “deploying to GitHub” process to be capable of updating the socket’s :deploy_step at each turn. We’ll have our view’s handle_event/3 function send a message to itself to enact each successive step in the process.

def handle_event("github_deploy", _value, socket) do
  :ok = MyApp.start_deploy()
  send(self(), :create_org)
  {:noreply, assign(socket, deploy_step: "Starting deploy...")}
end

def handle_info(:create_org, socket) do
  {:ok, org} = MyApp.create_org()
  send(self(), {:create_repo, org})
  {:noreply, assign(socket, deploy_step: "Creating GitHub org...")}
end

def handle_info({:create_repo, org}, socket) do
  {:ok, repo} = MyApp.create_repo(org)
  send(self(), {:push_contents, repo})
  {:noreply, assign(socket, deploy_step: "Creating GitHub repo...")}
end

def handle_info({:push_contents, repo}, socket) do
  :ok = MyApp.push_contents(repo)
  send(self(), :done)
  {:noreply, assign(socket, deploy_step: "Pushing to repo...")}
end

def handle_info(:done, socket) do
  {:noreply, assign(socket, deploy_step: "Done!")}
end

This code is dummied-up––we’re not worried about the implementation details of deploying our GitHub repo, but we can imagine how we might add error handling and other responsibilities into this code flow.

Our handle_event/3 function kicks off the deploy process by sending the :create_org message to the view itself. Our view responds to this message by calling on code that enacts that step and by updating the socket. This will cause our template to re-render once again, so the user will see Status: Starting deploy... change to Status: Creating GitHub org.... In this way, the view enacts each step in the GitHub deploy process, updating the socket and causing the template to re-render each time.

Now that we have our live updates working, let’s refactor the HTML code out of our render/1 function and into its own template file.

Rendering a Template File

We’ll define our template in lib/my_app_web/templates/page/github_deploy.html.leex:

<div>
  <div class>
    <button phx-click="github_deploy">Deploy to GitHub</button>
    <div class="github-deploy">
      Status: <%= @deploy_step %>
    </div>
  </div>
</div>

Next, we’ll have our live view’s render/1 function simply tell our PageView to render this template:

defmodule MyApp.GithubDeployView do
  use Phoenix.LiveView

  def render(assigns) do
    MyApp.PageView.render("github_deploy.html", assigns)
  end
  ...
end

Now our code is a bit more organized.

Conclusion

From even this limited example, we can see what a powerful offering this is. We were able to implement this real-time feature with only server-side code, and not that many lines of server-side code at that! I really enjoyed playing around with Phoenix LiveView and I’m excited to see what other devs build with it. Happy coding!

Article tags

phoenix

Sophie DeBenedetto

Sophie is an engineer and teacher at The Flatiron School. She loves teaching and learning and finding the Elixir School community felt like the perfect fit!