Tutorial

How to combine Phoenix LiveView with Alpine.js

This post was updated 05 May - 2022

phoenix liveview alpinejs tailwind

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.

Related Tutorials

Published 11 Jul - 2020
Updated 22 May - 2021

Create a reusable modal with LiveView Component

To reduce duplicity and complexity in your apps, Phoenix LiveView comes with the possibility to use reusable components. Each component can have its..

Published 13 Feb - 2020
Updated 01 May - 2020

Tagging interface with Phoenix LiveView and Tailwind - Tagging part 2

In the previous tutorial, I set up the the backend for being able to add tags to products. I have also written a tutorial about adding a LiveView an..