Controller-style approach to LiveView resources

clock7 min read

In this guide I present how to quickly make an HTML resource come alive using abstraction offered by Phoenix.LiveController.

Recently I've finally managed to give a serious spin to the Phoenix.LiveView library. As a born pragmatist I fell in love with its concept, so I've been following its course all the way since the announcement, through the exciting Phoenix Frenzy contest and ending with recent changes that may indicate that the library starts approaching maturity. On the other side, the ecosystem around it is only starting to form with a lot of open questions concerning even the most basic & most obvious topics, e.g. authentication.

So I've started experimenting. And as back in the old days I was interested in building web apps that are fully reactive across client & server (such as those built with Meteor framework which didn't buy me after all), I took the all-or-nothing approach in which entire application, including navigation, is run on live views. Why? Well, for a couple of reasons:

  1. First & foremost because LiveView allows that - it provides a complete solution for router-aware live navigation, including the concept of live actions in routing and the socket struct. 
  2. Also, navigating without HTTP requests has all the benefits for which libs like Turbolinks were previously born, i.e. speed and additional client-side possibilities. 
  3. Even if some live views only mount & render once without extra magic, they become a part of the request-free navigation and extending them with extra magic becomes very cheap.
  4. Finally, because the library seems to add very little coding overhead over traditional HTTP implementation so it shouldn't be costly to go with all-in solution.

Of course, that last concern - the overhead - highly depends on maturity of the library as well as on maturity of the entire ecosystem - including libraries, resolved issues and learning materials. And it's this concern that I'm trying to support with this article. 

Introducing Phoenix.LiveController

What I've discovered is that this kind of all-in approach, although already well supported by the library itself, is barely present in the ecosystem. I was looking for a clear path for converting my HTTP controllers, such as those coming from mix phx.gen.html, into live views, but most of the available guides focused on more specific cases for live views. Still, along with well-written LiveView docs, they were enough to teach me how to get there myself.

Then, looking at my code, I've noticed a lot of redundancy and unnecessary scattering of my live view equivalents of good ol' controllers. I felt like I'm lacking an abstraction for this kind of application, so I've created Phoenix.LiveController. Please take a look at its docs for a thorough explanation and examples before carrying on in order to grasp the idea behind steps below.

Using LiveController allowed me to make my live views more compact, easier to approach and just faster to code. Perhaps this abstraction will fill the gap for organizing the multi-action live view code, bringing more developers onboard this exciting ship.

So, without further delays, let's prepare the app and give LiveController a spin.

Preparing the Phoenix app

If you're starting from scratch or haven't configured Phoenix.LiveView in your project yet, then this section will get you started. But rather than repeating official guides, I'll point you at appropriate places in docs and briefly list the required steps.

Update: With a recent release of Phoenix 1.5.0-rc.0, you can now setup a new Phoenix project including LiveView and live layout by passing the --live option to mix phx.new, making this procedure unnecessary for a fresh project. It's still viable for existing 1.4 or 1.5 projects. 

All steps are backed by commits & diffs from the example project based on following libs:

  • phoenix v1.4.16
  • phoenix_live_view v0.11.1
  • phoenix_live_controller v0.1.0

Setting up a new Phoenix project

We'll create an app using mix phx.new task (commit):

  1. Create the app:
    mix phx.new my_app
    cd my_app
  2. Create the database:
    mix ecto.create
  3. Start the server:
    mix phx.server

Generating HTML resource

We'll generate an HTML resource using mix phx.gen.html task (commit).

  1. Invoke the generator:
    mix phx.gen.html Blog Article articles title
  2. Migrate the database:
    mix ecto.migrate
  3. Add the resource to MyAppWeb.Router (diff).

Configuring LiveView

We'll configure Phoenix.LiveView as described in Phoenix.LiveView installation guide (commit).

  1. Add phoenix_live_view Elixir package in mix.exs (diff) and install it:
    mix deps.get
  2. Add phoenix_live_view JS package in assets/package.json (diff) and install it:
    npm i --prefix assets
  3. Import live helpers in MyAppWeb (diff).
  4. Add live socket in MyAppWeb.Endpoint (diff).
  5. Replace :fetch_flash with :fetch_live_flash in MyAppWeb.Router (diff).
  6. Initialize LiveView JS in assets/js/app.js (diff).

Setting up layout

We'll set up layout to support both live views and regular HTML controllers as described in Layouts section of Phoenix.LiveView installation guide (commit).

  1. Add :live macro in MyAppWeb (diff).
  2. Set root layout in MyAppWeb.Router (diff).
  3. Keep only flash + inner render in layout/app.html.eex template (diff).
  4. Save live flash + inner render as layout/live.html.leex template (diff).
  5. Save the rest of original app layout as layout/root.html.eex template (diff).

Migrating to live controllers

Phew, now that we're done with preparations we have following prerequisites in place:

  • Phoenix application
  • Phoenix.LiveView configured with HTTP & live layouts
  • Main web module with reusable :live macro
  • HTML resource backed by non-live Phoenix controller

Let's see what it takes to convert that HTML resource to multi-action live view (commit).

Configuring LiveController

  1. Add phoenix_live_controller package to mix.exs (diff) and install it:
     mix deps.get
  2. Modify the :live macro to use LiveController instead of LiveView (diff).

Converting routes

Remove old resources call and add live equivalents of previous GET routes (diff).

Converting controller

Update: In phoenix_live_controller v0.3.0, @action_mount annotation is renamed to @action_handler. It still behaves the same for the exercise below.

Live controller code will be very similar to HTTP controller, so it makes sense to start by recycling it. Following Linux/Mac shell commands will put the file in a right place:

mkdir lib/my_app_web/live
mv lib/my_app_web/controllers/article_controller.ex \
   lib/my_app_web/live/article_live.ex

Then, make following changes in the live controller code:

  1. Rename the module with Live suffix (old/new). 
  2. Use :live macro instead of :controller macro (old/new).
  3. Replace occurrences of conn with socket (old/new).
  4. Annotate index, new, show and edit functions with @action_mount true (new).
  5. Annotate create, update and delete functions with @event_handler true (new).
  6. Call assign instead of render with assigns (old/new).
  7. Replace redirect calls pointing at live actions with push_redirect (old/new).
  8. Modify update function to rely on edit assign instead of ID passed in URL (old/new).

Converting templates

Start off by changing extensions of all templates in lib/my_app_web/templates/article from .eex to .leex. Following Linux/Mac shell commands will do the trick:

cd lib/my_app_web/templates/article
for f in *.eex; do mv -- "$f" "${f%.eex}.leex"; done

Then, make following changes in all templates:

  1. Replace occurrences of @conn with @socket (old/new).
  2. Replace link calls pointing at GET actions (now live actions) with live_redirect (old/new).
  3. Update link calls pointing at non-GET actions (now live events) to emit click events (old/new).
  4. Update form_for calls not to wrap the form in a function that LiveView couldn't update (diff).
  5. Update form_for calls to emit submit events (diff).
  6. Update render calls of "form.html" to assign event name instead of URL (old/new).

Fixing tests

I haven't covered that part yet. At the time of writing this article, phoenix_live_view v0.12 is still under development with planned overhaul of the testing module, so you're best off referencing current docs for Phoenix.LiveViewTest for up-to-date testing instructions.

Summary

Now that you're done, the entire resource will run navigation without any requests while still being crawlable and ready for any JS-free live additions. If you carry on, you may end up converting all routes into live views, making the result complete.

It may still seem like a lot of work, but I hope that this step-by-step diff-backed approach towards implementing live controllers for resources will be just as useful to you as it was for me. That it'll paint a clear logical picture of similarities and differences between the old & the new. And that once you dare to clear that path, you'll efficiently carry on with more and more of live view code, contributing into making the LiveView ecosystem rich & packed.

P.S. I'm currently in the process of adopting the new authentication solution by José Valim for this approach with a great progress so far, so stay tuned for the results.