Tutorial
How to combine Phoenix LiveView with Alpine.js
This post was updated 05 May - 2022
Combine Phoenix LiveView with Alpine.js
No matter how great Phoenix LiveView is, there is still some use case for sprinkling some JS in your application to improve user experience. For example, tabs, dropdowns, popovers and modals.
There is no need to keep the state of a dropdown on the server so you might just as well do it on the client.
However, there is still no fun to start writing javascript files but there is a library that have recently gained a lot of traction. Its called Alpine.js. Just like Tailwind it just relies in an enriched DOM.
An easy example for tabs would look like:
<div x-data="{ tab: 'foo' }">
<button :class="{ 'active': tab === 'foo' }" @click="tab = 'foo'">Foo</button>
<button :class="{ 'active': tab === 'bar' }" @click="tab = 'bar'">Bar</button> <div x-show="tab === 'foo'">Tab Foo</div>
<div x-show="tab === 'bar'">Tab Bar</div>
</div>
However, to use Alpine with Phoenix LiveView, you might run into some issues. In this tutorial, I will try to show you how to overcome those issues.
STEP 1 - Install Alpine
First step is to install Alpine js.
cd assets && yarn add alpinejs
And the only change required in app.js
is to simply require it:
// assets/js/app.js
import 'alpinejs'window.Alpine = Alpine
Alpine.start()
Out of the box, this solution would not work since every time LiveView updates the DOM, Alpine will lose track on the elements its attached to. So, there is a way to handle that built into LiveView. Further down when in the app.js file, where LiveSocket is initialized, I can add some code to keep track on the elements Alpine is attached to.
// assets/js/app.js
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
dom: {
onBeforeElUpdated(from, to) {
if (from._x_dataStack) {
window.Alpine.clone(from, to)
}
}
}
})
STEP 2 - Add the dropdown component
The first example is a common dropdown. These exists in basically every web application and is quite simple to setup with Alpinejs.
Initially I want the dropdown to have two states. Open true / false. That can be achieved with a x-data directive that basically takes a javascript object.
<div x-data="{ open: false }" class="relative text-left">
Next is to set the button itself. The dropdown should be toggeable, meaning it should be possible to both open and close it. Also, it should be closable by clicking on some other element or hitting the ESC key.
<button
@click="open = !open"
@keydown.escape.window="open = false"
@click.away="open = false"
class="flex items-center h-8 pl-3 pr-2 border border-black focus:outline-none">
So, The entire code are:
<div x-data="{ open: false }" class="relative text-left">
<button
@click="open = !open"
@keydown.escape.window="open = false"
@click.away="open = false"
class="flex items-center h-8 pl-3 pr-2 border border-black focus:outline-none">
<span class="text-sm leading-none">
Options
</span>
<svg class="w-4 h-4 mt-px ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div
x-cloak
x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute flex flex-col w-40 mt-1 border border-black shadow-xs">
<a class="flex items-center h-8 px-3 text-sm hover:bg-gray-200" href="#">Settings</a>
<a class="flex items-center h-8 px-3 text-sm hover:bg-gray-200" href="#">Support</a>
<a class="flex items-center h-8 px-3 text-sm hover:bg-gray-200" href="#">Sign Out</a>
</div>
</div>
Note above that I also use the transition directives that alpine provides to get a smooth animation effect when the dropdown open and closes.
STEP 3 - Add an animated toggle and interact with LiveView
Lets say you have an animated toggle and want it to connect to LiveView. Meaning, when clicking on the toggle, and it animates from on, to off, there should also be an event sent to the LiveView so some backend code can be triggered.
<div
class="flex items-center justify-start"
x-data="{ toggle: '0' }">
<div
class="relative w-12 h-6 rounded-full transition duration-200 ease-linear"
:class="[toggle === '1' ? 'bg-green-400' : 'bg-gray-400']"> <label
for="toggle"
class="absolute left-0 w-6 h-6 mb-2 bg-white border-2 rounded-full cursor-pointer transition transform duration-100 ease-linear"
:class="[toggle === '1' ? 'translate-x-full border-green-400' : 'translate-x-0 border-gray-400']"></label>
<input type="hidden" name="toggle" value="off" />
<input type="checkbox" id="toggle" name="toggle" class="hidden" @click="toggle === '0' ? toggle = '1' : toggle = '0'">
</div>
</div>
When this component starts, its starts with the toggle set to off. And when its clicked, it animates to on.
Since this is a checkbox under the hood I can use LiveView to track the changes. For example, I could do that with a form. However, in this case, I want to hook into the javascript event that happens when a change is triggered. I will do that by setting up a LiveView hook and listen for toggle events and then push that to the LiveView.
In the toggle component, I can add Alpines $watch
to keep track in of the open-state changes. And if it does, I can use $dispatch
to emit a javascript event.
<div
class="flex items-center justify-start"
x-data="{ toggle: '0' }"
x-init="() => { $watch('toggle', active => $dispatch('toggle-change', { toggle: active })) }"
id="toggle-example"
phx-hook="Toggle">
<!-- REST OF THE TOGGLE CODE -->
</div>
Note that I also add an id-attribute and a LiveView hook with phx-hook="Toggle"
.
In the JS file, I will need to add the hook like:
let Hooks = {}
Hooks.Toggle = {
mounted() {
this.el.addEventListener("toggle-change", event => {
this.pushEvent('toggle-change', event.detail)
})
}
}
This means, when the Apline dispatches the toggle-change
-event, the event-listener in the hook will intercept that and use LiveViews pushEvent
to communicate with the LiveView backend.
In the LiveView, I need to add a handle_event
to receive that. Also, do display that the backend actually changes state, Im adding a toggle
-variable that the LiveView can update.
defmodule TutorialWeb.PageLive do
use TutorialWeb, :live_view @impl true
def mount(<i>params, </i>session, socket) do
assigns = [
toggle: "off"
]
{:ok, assign(socket, assigns)}
end @impl true
def handle_event("toggle-change", %{"toggle" => toggle}, socket) do
toggle = if toggle == "1", do: "on", else: "off" {:noreply, assign(socket, :toggle, toggle)}
end
end
All I need to do is to output this in the template by adding this next to the toggle button
<span class="inline-block px-4">
<%= @toggle %>
</span>
And toggling should now indicate that the text changes:
As I mentioned above, there are several ways to achieve the same result. I could have tracked this with a form or add the phx-click
to track the changes. What works for you depends on the situation.