Proper AlpineJS Integration with Liveview

Problem

When using more complex nested components, especially with live_components, AlpineJS can stop working after updates.

This issue is also documented here:

Source

All available documentation instructs you to integrate Alpinejs in the app.js within the onBeforeElUpdated hook like this.

// 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)
      }
    }
  }
})

This doesn’t factor in that LV updates can be nested html elements!

Solution

You need to clone the Alpine data not only on the root element but also in all child elements .

Here is our solution.

// assets/ts/liveview.ts
const cloneAlpineJSData: (from: HTMLElement, to: HTMLElement) => void = (from, to) => {
  if (!window.Alpine || !from || !to) return

  for (let index = 0; index < to.children.length; index++) {
    const from2 = from.children[index]
    const to2 = to.children[index]

    if (from2 instanceof HTMLElement && to2 instanceof HTMLElement) {
      cloneAlpineJSData(from2, to2)
    }
  }

  if (from._x_dataStack) window.Alpine.clone(from, to)

  // Set `x-persist="type,style.height,style.minHeight"` to prevent these attributes from being overrided by an LV update
  persistAttributes(from, to, from.getAttribute('x-persist'))
}

[...]

const liveSocket = new LiveSocket('/live', Socket, {
    hooks,
    params: { _csrf_token: csrfToken },
    dom: {
      onBeforeElUpdated(from, to) {
        cloneAlpineJSData(from, to)

        return true
      }
    }
  })

Note:
persistAttributes is an another helper which, persist certain attributes which are updated by Alpinejs. For example we have a <textarea> that changes style.height and style.minHeight depending on the entered data.

7 Likes

The solution can be optimized to stop searching, as soon as there was a node with alpinejs - and limit the depth.

const cloneAlpineJSData: (from: HTMLElement, to: HTMLElement, depth: number) => void = (from, to, depth) => {
  if (!window.Alpine || !from || !to || depth > 2) return

  if (from._x_dataStack) return window.Alpine.clone(from, to)

  for (let index = 0; index < to.children.length; index++) {
    const from2 = from.children[index]
    const to2 = to.children[index]

    if (from2 instanceof HTMLElement && to2 instanceof HTMLElement) {
      cloneAlpineJSData(from2, to2, depth++)
    }
  }
}