Phoenix Liveview and Web Components

We’ve been using Phoenix LiveView on several projects now and it’s safe to say we are pretty sold on its benefits. It lets us use Elixir to write our front end, but still get the high-performance interactivity we need. On most LiveView applications, there are typically cases where we want to bring in a javascript library that solves a common problem well. While hooks are one way to interact with javascript in a LiveView app, I’ve found using web components to often be a cleaner solution.

LiveView at its core is about updating the DOM without a page reload. As such, it’s perfectly suited to managing DOM elements. With a web component, we get to choose the perfect abstraction for interacting with it: a Custom Element. Because Custom Elements are real DOM elements, they can be created from LiveView just like any other.

Attributes in, events out

I’ve become a huge fan of a simple approach to interacting with Custom Elements: pass data to your element via attributes (or properties), and allow them to interact with the rest of your application via dispatching events. LiveView has support for many built-in events out of the box. In the case of Custom Elements, it often makes sense to dispatch a Custom Event. The CustomEvent API is not usually lumped in with the rest of the Web Component APIs, but is still a great tool for building Web Components. In a nutshell, a Custom Event is DOM event you define with a payload of your choosing. It’s a great way for Custom Elements to communicate with their surroundings. LiveView needs a little help to handle Custom Events, but we’ve got you covered.

An example

All this is easier to see by the example, so let’s look at a very common scenario that necessitates adding a javascript library to a LiveView app: adding a map. For this example, I am implementing a google map that renders general aviation airports within a given area. In order to do this, I’m using a web component that will give us custom elements to add a google map and markers to our page called lit-google-map. The lit refers to LitElement, a really nifty library to make creating Custom Elements easier.

Using the lit-google-map element and its companion element lit-google-map-marker lets us add a google map with markers for each airport to our LiveView template:

  <lit-google-map api-key="">
    <%= for %{geo_location: %Geo.Point{coordinates: {lng, lat}}, name: name, ident: ident} <- @airports do %>
      <lit-google-map-marker slot="markers" latitude="<%= lat %>" longitude="<%= lng %>">
        <p><%= name %> (<%= ident %>)</p>
      </lit-google-map-marker>
    <% end %>
  </lit-google-map>

This works great to render the map with airports, but we’ve got a problem. Whenever the user zooms or pans, we need to fetch a different set of airports. Of course, in order to be able to do that, we need the map to let us know the boundaries of the currently displayed map. This is where Custom Events come to our rescue: the lit-google-map element emits a bounds_changed Custom Event with a payload containing the latitudes and longitudes of the four corners of the map. We can use this information to do a new search for airports that are within these bounds.

First though, we need to get the event sent to LiveView. I mentioned earlier that LiveView isn’t able to handle Custom Events out of the box just yet, so we created a phoenix-custom-event-hook package to do just that. Adding it to your project requires you to do just a little setup when you create your LiveSocket:

import PhoenixCustomEvent from 'phoenix-custom-event-hook';
 
let liveSocket = new LiveSocket("/live", Socket, { hooks: { PhoenixCustomEvent } });

This LiveView hook will let us send Custom Events to LiveView. To use it, we need to add a couple more attributes to our lit-google-map element. First, we need to add the phx-hook attribute to our element to enable the hook. Next, we add attributes to map our Custom Events to LiveView events. We do this by adding an attribute for each event, similar to other LiveView event bindings. In our case, we want to have our LiveView handle the bounds_changed Custom Event so we need to add an attribute with the prefix phx-custom-event- concatenated with the name of the Custom Event emitted. The value of the attribute is the event name we will use in the LiveView. Here’s what it looks like in action:

<lit-google-map api-key="" phx-hook="PhoenixCustomEvent" phx-custom-event-bounds_changed="bounds_changed">

Finally, we are able to handle the bounds_changed event in our LiveView and search for airports within the bounds of our map:

  def handle_event(
        "bounds_changed",
        %{"north" => north, "east" => east, "west" => west, "south" => south},
        socket
      ) do
    airports =
      Airports.list_airports_in_bounds(%{north: north, east: east, west: west, south: south})
    {:noreply, socket |> assign(airports: airports)}
  end

Here, we delegate to our context function search for airports and assign them to our socket which triggers our LiveView to re-render with the new map makers.

Hope you enjoyed reading. It’s worth pointing out that if LiveView adds support for Custom Events, the hook and the library which contains it will no longer be needed. Lastly, the code for this example project is available in this here repo.

Related Posts

Want to learn about the types of products we build?

Check out our projects

Like what you're seeing? Let's keep in touch.

Subscribe to Our Newsletter