Phoenix LiveView: writing dynamic frontend code on the backend

This is an intriguing Phoenix feature that I haven’t tried yet. We should build a better Pomodoro-style timer with it.

Phoenix LiveView for web developers who don’t know Elixir.

He has some interesting migration stories on his blog post, including a mention of Bleacher Report’s migration from Ruby on Rails to Elixir:

“On our monolith we needed roughly 150 servers to power the more intensive portions of BR. Following our move to Elixir we’re now able to power those same functions on five servers and we’re probably overprovisioned. We could probably get away with it on two,” Marks says.

This three-part tutorial on Phoenix LiveView is good.

The feature is really interesting, because it provides SPA-like functionality without needing to write any JavaScript, and the state is managed on the server, so you can save it and/or broadcast it to other users without much effort.

Here’s a basic description about how it works, based on the tutorial series above.

You tell the router what LiveView to load at a specific path:

scope "/", GalleryWeb do
  pipe_through :browser

  get "/", PageController, :index

  # This route loads the Gallery LiveView
  live "/gallery", GalleryLive
end

The LiveView module that handles it defines a mount and a render function.

The mount function does initial setup. In the tutorial’s example, it creates two key-value pairs for the state:

  1. key: current_id, value: the ID of the first image
  2. key: slideshow, value: stopped (the current state of the auto-advancing slideshow)
def mount(_session, socket) do
  # Adding key-value pairs to the websocket connection
  socket = socket
           |> assign(:current_id, Gallery.first_id())
           |> assign(:slideshow, :stopped)

  # returning a tuple with the message `:ok` and the new `socket`
  {:ok, socket}
end

The render function contains an initial template to send. The HTML gets rendered server-side (good for SEO), but then the JS establishes a websocket connection and further HTML changes are sent over the websocket.

def render(assigns) do
  # this returns the template
  ~L"""
  <center>
    <%= for id <- Gallery.image_ids() do %>
      <img src="<%= Gallery.thumb_url(id) %>"
      class="<%= thumb_css_class(id, @current_id) %>">
    <% end %>
  </center>
  <label>Image ID: <%= @current_id %></label>
  <center>
    <button phx-click="prev">Prev</button>
    <button phx-click="next">Next</button>

    <%= if @slideshow == :stopped do %>
      <button phx-click="play_slideshow">Play</button>
    <% else %>
      <button phx-click="stop_slideshow">Stop</button>
    <% end %>
  </center>
  <img src="<%= Gallery.large_url(@current_id) %>">
  """
end

This snippet loops over the image IDs:

<%= for id <- Gallery.image_ids() do %>

The templates indicate which actions should be run for events, in this case phx-click runs the "prev" (previous image) action:

<button phx-click="prev">Prev</button>

Then you can handle those events with handle_event and the name of the action. This example runs “prev” to tell the server that it should show the previous image:

def handle_event("prev", _event, socket) do
  # returns the new state in the socket
  {:noreply, assign_prev_id(socket)}
end

# Returns the socket with a new `current_id`
def assign_prev_id(socket) do
  assign(socket, :current_id, Gallery.prev_image_id(socket.assigns.current_id))
end

When events are fired, or the server updates, the changes are sent back and forth over the websocket with lightweight JSON messages (all managed by Phoenix) like this:

[
    "4",
    "7",
    "lv:phx-abcdef-",
    "event",
    {
        "type": "click",
        "event": "next",
        "value": {
            "altKey": false,
            "shiftKey": false,
            "ctrlKey": false,
            "metaKey": false,
            "x": 477,
            "y": 281,
            "pageX": 477,
            "pageY": 399,
            "screenX": 479,
            "screenY": 413,
            "value": ""
        }
    }
]

There’s a bit more to it, but that’s the basic idea.

I haven’t looked closely yet, but someone is building a Bulma-based component library:
http://surface-demo.msaraiva.io/getting_started