I ran into an issue with Alpine and Phoenix's LiveView.
LiveView will dynamically render & replace html in a page. The issue I ran into is that a simple dropdown menu (from TailwindUI) would be in the 'closed' state, but the menu was still displaying. Clicking the menu toggle once would keep it open, and clicking it again would finally close it.
So alpine seemed to be initialized to the correct state, but x-show="open" did not have an effect initially.
My workaround was to manually set the dropdown to display: none; so that it would not initially be displayed.
Any ideas on a good way to handle this? Is there a way to tell alpine to 'initialize' a component again?
Ran into the same thing today. Quick hack was to initialize components after inserting.
Alpine.discoverUninitializedComponents(function(el){ Alpine.initializeComponent(el) })
Seems like the listenForNewUninitializedComponentsAtRunTime isn't picking them up correctly somewhere. I don't know enough about MutationObservers to say why.
Based on multiple occurrences, I'm going to say this is a bug and needs looking into. Not sure who's got time for this, @SimoTod @HugoDF @calebporzio .
I might get a chance to look into this, this week.
I'm seeing the same thing in a Phoenix Liveview. Tried to add the Alpine.discoverUninitializedComponents(function(el){ Alpine.initializeComponent(el) })
in a Liveview update hook but to no avail.
I can confirm that I am experiencing this issue as well (See #304 for a small gif). Where should I try to place the below code, to see if it fixes the problem?
Alpine.discoverUninitializedComponents(function(el){ Alpine.initializeComponent(el) })
@oliverbj run it after you insert.
I'm having the same issue, the only thing that somewhat fixes it is a manually set display: none;
@iwarshak @sweco-semtne One way to solve it in Phoenix LiveView is like: https://github.com/andreaseriksson/tutorials/commit/76321a27022fdfbec19704f28a17d12a25ec499a#diff-b60d18a72e4510b742ba1eb9bd28b3f9R2
This is an issue with the page being rendered twice. So either wait until socket is connected or add a new id to your alpine element after the socket is connected.
Also, just wrote something about it here:
https://fullstackphoenix.com/tutorials/combine-phoenix-liveview-with-alpine-js
@andreaseriksson Thanks for the writeup! I completely forget that the root of the issue was the double render.
I went with your last approach (changing the id when the LV socket is connected). In my case I was using a dropdown menu to display LV links, so I made my links like this, so that the menu would close when it was clicked
<%= live_patch "All Categories",to: Routes.dashboard_live_path(@socket, ListLive), "@click": "open = false" %>
Works great!
@andreaseriksson @iwarshak the above syntax <%= live_patch .... is that something specific for Phoenix Liveview?
Yes, it would create a link with markup that is handled with Phoenix LiveView.
I am not actually sure about the last part. "@click": "open = false".
That would just add that as url params but Im not sure alpine handles that.
@andreaseriksson Well it wasn't interpreted as URL params, but it did put the "@click": "open = false" attribute on the a tag - but unfortunately LiveView bombs out on this attribute
I’ve got another issue which I think might be related.
I load data in a list after user input and have alpine sprinkled in the list items to expand collapse additional details as well as controls for deleting subitems. (So the double render “should” not be a problem)
On delete of the sub item the whole list is rerendered (intentionally) however suddenly all items in the list have their subitems expanded.
I’ll investigate some more and report back
Ran into the same thing today. Quick hack was to initialize components after inserting.
Alpine.discoverUninitializedComponents(function(el){ Alpine.initializeComponent(el) })Seems like the
listenForNewUninitializedComponentsAtRunTimeisn't picking them up correctly somewhere. I don't know enough about MutationObservers to say why.
Great idea.
But in my case the component is already initialized, but I dynamically replace the html contents with new alpine directives.
Any way to refresh an existing alpine component?
Hey 👋
Sometime, the DOM inside a Alpine Component is rerendered by dynamic injected html without being detected by Alpine and cause Alpine to stop incorrectly or loose state like for x-show.
Tweaking with id="<%= if connected?(@socket), do: "connected-id", else: "not-connected-id" %>" doesn't always works or doesn't work at all for some cases.
Here is my solution for Phoenix Live View.
But it can be used with all dynamically injected template I guess (of course, it so shall be adapted).
First, I was trying to use phoenix live view hooks to tell Alpine to rerender the actual component. I had so:
<div phx-hook="AlpineComponentHook" x-data="{ tab: 'chat' }">
<button :class="{ 'active': tab === 'chat' }" @click="tab = 'chat'">Chat</button>
<button :class="{ 'active': tab === 'edit' }" @click="tab = 'edit'">Edit</button>
<div x-show="tab === 'chat'">
<%= live_component @socket, ChatComponent, id: :chat_component, room: @room %>
</div>
<div x-show="tab === 'edit'" style="display: none;">
<%= live_component @socket, EditComponent, id: :edit_component, room: @room %>
</div>
</div>
And the js file with the Phoenix hooks:
let hooks = {
AlpineComponentHook: {
actualizeAlpineComponent(e) {
Alpine.initializeComponent(e);
},
mounted() {
this.actualizeAlpineComponent(this.el);
},
updated() {
this.actualizeAlpineComponent(this.el);
},
}
};
// other code
let liveSocket = new LiveSocket("/live", Socket, {hooks: hooks});
I added the style="display: none;" attribute to the second tab because it wasn't correctly initialized / it blinks at page load.
But the Alpine.initializeComponent didn't work.
If the Alpine.initializeComponent is not working it's because the id is the same.
I achieve to make something pretty nice with generating random id and tell Alpine to initialize the "new" component.
function guidGenerator() {
var S4 = function() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
};
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
}
let hooks = {
AlpineComponentHook: {
actualizeAlpineComponent(e) {
e.id = guidGenerator();
Alpine.initializeComponent(e);
},
mounted() {
this.actualizeAlpineComponent(this.el);
},
updated() {
this.actualizeAlpineComponent(this.el);
},
}
};
__Note: Maybe the guidGenerator is too high-cpu consumption (you could find an alternative to generate random ids like the name + timestamp). Also, maybe Alpine keep track of old ids for old components, if Alpine didn't detect the removed component, there will be a high memory leak. Doing Alpine.start(); instead of Alpine.initializeComponent() may be a better approach ? To be confirmed__
But I'm still losing selected tab, at each page rerender I'm falling to the default 'chat' tab.
To cheat the Alpine system, I moved the x-data value in the js window object. And with x-init Alpine attribute, I watch for updates and copy it to the window x-data attribute.
<div phx-hook="AlpineComponentHook" x-init="$watch('tab', value => AlpineComponents.TabRoom.tab = value);">
...
</div>
window.AlpineComponents = {
TabRoom: {
tab: 'chat'
}
}
let hooks = {
AlpineComponentHook: {
actualizeAlpineComponent(e) {
e.id = guidGenerator();
e.setAttribute("x-data", JSON.stringify(AlpineComponents.TabRoom))
Alpine.initializeComponent(e);
},
mounted() {
this.actualizeAlpineComponent(this.el);
},
updated() {
this.actualizeAlpineComponent(this.el);
},
}
};
md5-8c5d6d4c74855176c04a8c5e77c83103
```js
e.setAttribute("x-data", JSON.stringify(AlpineComponents[e.getAttribute("x-name")]))
EDIT 4:
I'm still running into issues with x-ref inside re-rendered Alpine components... They'ren't detected, got: $refs.my_ref is undefined.
Looking at the source code help me found the best solution.
Forget everything about what I wrote before. Here the solution fixing all issues about x-ref/$refs, input loosing focus, ...
The HTML part, keep it simple and near of Alpine-original:
<div phx-hook="AlpineComponentsHook" x-data="{ tab: 'chat' }">
The JS part (without any lib):
let hooks = {
AlpineComponentsHook: {
actualizeAlpineComponent(el, force_render = false) {
if (el.__x === undefined) {
Alpine.initializeComponent(el)
} else {
const {$refs, $el, $nextTick, $watch, ...x_data} = el.__x.unobservedData;
if (force_render || x_data !== el.getAttribute("x-data")) {
el.__x = undefined;
el.setAttribute("x-data", JSON.stringify(x_data));
Alpine.initializeComponent(el);
}
}
},
mounted() {
this.actualizeAlpineComponent(this.el);
},
updated() {
this.actualizeAlpineComponent(this.el);
},
}
};
If you are experiencing issues, put force_render to true.
Note: I'm still playing with the xcloak attribute
Hope that helps ! :)
@scorsi, this looks very promising, will try it out. Thanks for all this work :)
Would love to make alpine.js my transitions helper when using LiveView, but wondering if there is just too much incompatibility between the two in general (beyond the fix above), as per this comment: https://github.com/phoenixframework/phoenix_live_view/issues/809#issuecomment-617195499
If we had a reliable/robust path forward, then alpine.js/liveview/tailwind would be an amazing stack...
I encountered an issue. Looking at https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-js-interop-and-client-controlled-dom helped me found the error.
Note: when using phx-hook, a unique DOM ID must always be set.
We have to add an unique ID to the AlpineComponent to make the hooks works great.
@scorsi: Thanks! Any chance you could share where exactly you added the unique ID?
@dbi1 The id takes place as follow:
<div id="some-unique-id-which-never-changes" phx-hook="AlpineComponentsHook" x-data="some data model">
Keep in mind that Phoenix LiveView + AlpineJS is a always day patch which can explode at any updates. I recommand using it only for small UI components like Dropdown, Tabulations, ... Making too advanced AlpineJS components sometimes dont work and needed to be done using LiveView.
I (partially) resolves an issue with x-transition using spruce. For exemple (cutted code):
<div id="chat_component" class="flex-1 flex flex-col" phx-hook="ChatMessagesHook" data-messages="<%= Jason.encode!(@messages) %>" x-data x-subscribe>
<div class="flex-1 flex overflow-y-auto mx-2" phx-update="ignore">
<div class="flex flex-col w-full min-h-content" x-ref="chat_messages">
<div class="mt-auto"></div>
<template x-for="message in $store.chat_messages.data" :key="message.id">
<div class="flex-initial my-2 first:mt-auto ml-auto"
x-transition:enter="transition ease-out duration-700"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:enter-start="opacity-0 transform scale-75"
x-transition:leave="transition ease-in duration-700"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-75">
<div class="p-1 bg-gray-100 rounded-full">
<div class="flex flex-row items-center content-center mr-2">
<img src="https://wolfy.fr/static/img/picture-2.jpg"
class="w-10 h-10 rounded-full"/>
<div class="ml-1 flex flex-col">
<div x-text="message.user" class="text-xs font-extrabold leading-tight"></div>
<div x-text="message.text" class="text-sm leading-tight"></div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
Spruce.store("chat_messages", {data: []});
const actualizeAlpineComponent = (el, force_rerender = false) => {...};
let hooks = {
AlpineComponentsHook: {
mounted() { ... },
updated() { ... },
},
ChatMessagesHook: {
mounted() {
hooks.AlpineComponentsHook.mounted.bind(this)();
Spruce.store("chat_messages").data = JSON.parse(this.el.dataset.messages);
},
updated() {
hooks.AlpineComponentsHook.updated.bind(this)();
Spruce.store("chat_messages").data = JSON.parse(this.el.dataset.messages);
},
}
};
Here Spruce is used like a glue between LiveView and AlpineJS.
But I'm still running an issue (https://github.com/alpinejs/alpine/issues/432) which can't be resolved easily with the given solution (using some "deleted" values). Needed to be done in the backend part.
Hey all you LiveViewers, I just posted a pretty big response on the subject of making LiveView integrate with Alpine here: https://github.com/phoenixframework/phoenix_live_view/issues/809#issuecomment-632366710
Take a look!
For anyone trying the suggested fix in https://github.com/phoenixframework/phoenix_live_view/issues/809#issuecomment-637671803
let liveSocket = new LiveSocket("/live", Socket, {
dom: {
onBeforeElUpdated(from, to){
if(from.__x){ window.Alpine.clone(from.__x, to) }
}
},
params: {_csrf_token: csrfToken}
})
and wondering why it isn't working, it's because dom option is added in v0.1.13 and it hasn't been released yet.
@scorsi @syfgkjasdkn @iwarshak are there still issues with Alpine-LiveView interop?
It looks like the fix has gone into Liveview, let me know so I can close this issue
@HugoDF I think we can comfortably close this issue now, there are no further reports and somebody on the other LiveView thread has confirmed that it is working.
Cool, closing then.
Feel free to open new issues if you're having problems with LiveView/Alpine interop
Most helpful comment
Hey all you LiveViewers, I just posted a pretty big response on the subject of making LiveView integrate with Alpine here: https://github.com/phoenixframework/phoenix_live_view/issues/809#issuecomment-632366710
Take a look!