Search code examples
csstailwind-cssphoenix-frameworkalpine.jsphoenix-live-view

What is the correct way to implement Alpine.js and Phoenix 1.7 initialization with LiveView 0.18.3?


I'm having trouble getting Alpine and Phoenix LiveViews to work together.

With the code provided below, liveview phx-click events do not fire, but Alpine works. CSS animations specified by Tailwind do not activate as they should on page load, but other CSS properties specified by Tailwind do style elements correctly. There is a card on the page in question that is supposed to flip over on page load, but it just loads in and never flips.

If I remove Alpine.start(), alpine does not work correctly on page load, but does start working after I follow a liveview component link on this page that looks like this: <.link navigate={~p"/lessons"}>. All Tailwind css, including animations, works correctly. Notably, some of Alpine is indeed working on page load. The element on the page with x-show="open" is hidden because its parent has x-data="{ open: false }". The @click="open - !open" does in fact change the value of open. What's broken is the way that x-show does not alter the display: none property as the value of open changes.

If I remove liveSocket.connect(), alpine works on page load, but of course then liveviews don't work. The card never flips. It just loads in overturned. I don't understand why removing the socket connection disrupts the CSS.

If I move Alpine.start() to just after liveSocket.connect(). Alpine does not work on page load, but all Tailwind css and liveview functionality works correctly.

Inscrutably (to me), if I remove both Alpine.start() and liveSocket.connect(), Alpine works just fine on page load, but also here of course liveviews don't work.

I'm having a tough time searching for what Alpine.start() actually does, and worse, I'm struggling to understand its interactions with liveSocket.connect().


Relevant portion from app.js

import Alpine from '../vendor/alpine'
window.Alpine = Alpine
Alpine.start()

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
    params: { _csrf_token: csrfToken },
    hooks: Hooks,
    dom: {
        onBeforeElUpdated(from, to) {
            if (from._x_dataStack) {
                window.Alpine.clone(from, to)
            }
        }
    },
})

// connect if there are any LiveViews on the page
liveSocket.connect()

Relevant portion from nav.ex, a live componennt

<div class="relative ml-3" x-data="{ open: false }">
  <button
    @click="open = !open"
    @click.outside="open = false"
    type="button"
    class="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-green focus:ring-offset-2"
    id="user-menu-button"
    aria-expanded="false"
    aria-haspopup="true"
  >
    <span class="sr-only">Open user menu</span>
    <img
      class="h-8 w-8 rounded-full"
      src="https://i.kym-cdn.com/photos/images/facebook/001/431/201/40f.png"
      alt=""
    />
  </button>
  <!--
    Dropdown menu, show/hide based on menu state.
  -->
  <div
    class="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
    role="menu"
    aria-orientation="vertical"
    aria-labelledby="user-menu-button"
    tabindex="-1"
    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"
    x-cloak
  >...</div>       

Solution

  • This issue was due to the context in which the live component was rendered. The live component was being rendered outside of a live view, which is not allowed.

    I changed the live component in nav.ex to a regular Phoenix.Component. Then I removed the following lines from app.js:

    window.Alpine = Alpine
    Alpine.start()
    

    I kept the Alpine import line import Alpine from '../vendor/alpine'

    With those changes, I have working Alpine, liveview, and CSS on page load.