Phoenix LiveView JavaScript Hooks and Select2

Phoenix LiveView JS Hooks with Select2

We’ve already played with Select2 and LiveView, it was before the release of LiveView JS Hooks. To make select2 work with LiveView we had to listen to phx:update generic javascript events, re-initialize select2 and use a workaround to push events from the browser to LiveView.

With hooks now everything it’s much simpler and smooth. After we’ve added countries elixir dependency, jquery and select2 in assets/package.json and set up the latest LiveView release in our Phoenix project, we can write SelectLive module. In this view we are going to achieve the same results of the previous version, but with much less code on both elixir and javascript side.

defmodule DemoWeb.SelectLive do
  use Phoenix.LiveView
  # import Phoenix.HTML.Form
  @countries Countries.all() |> Enum.sort_by(&(&1.name))

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:countries, @countries)
      |> assign(:country, nil)
    {:ok, socket}
  end

  def render(assigns) do
    ~L"""
    <div class="liveview-select2"  
         phx-hook="SelectCountry" 
         phx-update="ignore"
    >
      <select name="country">
        <option value="">None</option>
        <%= for c <- @countries do %>
          <option value="<%= c.alpha2 %>">
            <%= c.name %>
          </option>
        <% end %>
      </select>
    </div>

    <%= if @country do %>
      <!-- Google Maps iframe -->
      <iframe src="http://maps.google.com/maps?q=<%= @country.name %>&output=embed"
        width="360" height="270" frameborder="0" style="border:0"></iframe>

    <% end %>
    """
  end

  ...
end

mount/3 callback is almost the same as before, we initialize the assigns with :countries (where we keep the countries list) and :country (the selected country).

The significant change is in the div tag that wraps the select element

<div ... phx-hook="SelectCountry" phx-update="ignore">
  <select>
  ...
  </select>
</div>

With phx-update="ignore" attribute, LiveView renders the select element avoiding to patch it during further content updates. In cases like this, where we have a select with a fixed set of options and a select2 JS library which adds its own elements in the DOM, the ignore option is pretty useful to avoid that LiveView re-renders the original select tag.

Patched DOM without phx-update="ignore"
Patched DOM without phx-update="ignore"

phx-hook="SelectCountry" attribute tells LiveView’s client side to use a SelectCountry hook object (that we see in a moment) to handle custom JavaScript.

In assets/js/app.js we initialize LiveView passing the hooks

// assets/js/app.js
import { Socket } from "phoenix"
import LiveSocket from "phoenix_live_view"

import jQuery from "jquery"
import select2 from "select2"
import "select2/dist/css/select2.css"

let Hooks = {}
Hooks.SelectCountry = {

  initSelect2() {
    let hook = this,
        $select = jQuery(hook.el).find("select");
    
    $select.select2()
    .on("select2:select", (e) => hook.selected(hook, e))
    
    return $select;
  },

  mounted() {
    this.initSelect2();
  },

  selected(hook, event) {
    let id = event.params.data.id;
    hook.pushEvent("country_selected", {country: id})
  }
}

let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks, ... })
liveSocket.connect()

When creating a new LiveSocket we pass the hooks with a SelectCountry object which implements the mounted callback.

mounted is called the first time our element (the <div ... phx-hook="SelectCountry" ...> tag which wraps select tag) is added on the page and the server LiveView has finished mounting. In the mounted callback the element is accessible with this.el – we use it to initialize select2.

Our select2 listens to "select2:select" events: when a user selects an option the SelectCountry.selected(hook, event) JavaScript function is called. It’s now available a JS pushEvent function to send events from the browser to the backend, so we get the value of the selected option and send a country_selected event to the LiveView process, with {country: id} payload.

defmodule DemoWeb.SelectLive do
  ...
  def handle_event("country_selected", %{"country" => code}, socket) do
    country = Countries.filter_by(:alpha2, code) |> List.first()
    {:noreply, assign(socket, :country, country)}
  end
end

On the backend side, in the SelectLive module we handle the event in the same way as we did previously, using country code to find and assign the selected :country. Then LiveView re-renders the view and patches the DOM, adding a Google Maps iframe that shows the country.

Working select2 with LiveView
Working select2 with LiveView

With JS hooks it’s now much simpler to do LiveView JS interoperability. For this simple example we could just use a Phoenix channel, but to me LiveView makes things even easier, since we don’t need to manually send the countries list to the browser, for example – in this way we can focus mostly on the backend side!