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 option
s 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.
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.
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!