Phoenix LiveView helps devs to avoid writing lots of JS code. But there are still some times when we need to execute custom javascript on events initiated by a live view.

Let's say you're making a clone of GitHubGist using LiveView. You want to render a gist inside a live view, and you are going to use some external JS library to highlight a code written in the gist.

How to call the library once the gist is rendered on the page?
Client hooks are coming to the rescue.

Overview

So we need to do something on the client side once a live view mounts an element. How do we do this?

We’ll need two things:

  1. An element controlled by a live view. (I'll call it an observable element)
  2. A hook object: a plain old JS object that stores callbacks executed once the live view changes the observable. (I'll refer to it as an observer hook object)

Observable element

For the observable element I've built a tiny functional component:

gist_component.ex
defmodule GistCloneWeb.GistLive.GistComponent do use Phoenix.Component def show_gist(assigns) do ~H""" <pre><code><%= @gist.code %></code></pre> """ end end

Once this component is mounted or updated I want an external library to be called to highlight the contents of the <code> element

Observer hook object

We need to create a JS object and implement several methods used as callbacks.

In general, there are a few callbacks that we can implement to react to changes in the observable element:

  • mounted - will be called once the observable element is rendered on a page and the parent live view has finished mounting
  • beforeUpdate - will be called before the element is going to be updated on a page
  • updated - will be called after the element is updated on a page
  • destroyed - will be called after the element is removed from a page

There are also two additional callbacks related to the element's parent live view:

  • disconnected - will be called when the parent live view is disconnected from the server
  • reconnected - will be called when the parent live view is reconnected to the server

Each callback is optional, so we can define only those we need.


In our case, mounted and updated is enough:

gist_hook.js
export const GistHook = { highlightElement() { // Call external library to highlight // the observable element hljs.highlightElement(this.el); }, mounted() { this.highlightElement(); }, updated() { this.highlightElement(); } }

highlightElement is my helper function that calls external lib to highlight the observable element.

this.el is a pointer to the observable element. It's injected by the LiveView js library when a view initializes the client hook.

Glue together

To finish the setup, we need to register our client hook object via LiveSocket's hooks option:

app.js
import {GistHook} from "./gist_hook" ... let liveSocket = new LiveSocket("/live", Socket, {hooks: {GistHook}, params: {_csrf_token: csrfToken}})

And annotate the observable element with a phx-hook attribute:

gist_component.ex
def show_gist(assigns) do ~H""" <pre><code id={@gist.id} phx-hook="GistHook"><%= @gist.code %></code></pre> """ end

Note that it's also necessary to pass the id attribute. Otherwise, our LiveView component won't compile with the following error:

attribute "phx-hook" requires the "id" attribute to be set

Conclusion

Here's a quick recap of how to set up a client hook:

  1. Add a JS object. Implement the necessary callbacks.
  2. Pass this object to the LiveSocket constructor
  3. Set phx-hook attribute for the elements that should trigger the callbacks.

There are some other useful things that client hook objects can do, like pushing events to the LiveView server, handling events coming from the LiveView server, or uploading files.
I'll explore these possibilities in later posts.