I’m experiencing the same issues as well.
After lots of trial and error, the problem seems to be that, when using lazy-loaded frames, Alpine doesn’t initialize the newly rendered components.
General problem
The reason is that Alpine needs to run Alpine.initializeComponent(componentDOMElement) in order to activate it. (if you have properly installed it) Alpine already does this to all components that are initially in the document, upon first load of the page.
The problem seems to be that: upon first page load with lazy-loaded turbo-frames, all Alpine sees is:
...
<turbo-frame src="..." id="...">
</turbo-frame>
...
The frame hasn’t (yet) loaded the html where your Alpine components are, because they just aren’t present upon first page load.
Your (@BryTai ) project-specific problem
When a user goes to the admin dashboard (admin.html.erb), after the HTML is downloaded and Alpine tries to mount the components, the only thing it sees is something like:
<h1>Admin</h1>
<turbo-frame id="tab_content" src=" the tab_1_content_path ">
</turbo-frame>
(You can verfy this by checking the response to the initial page request)
Alpine can’t mount any component: because there are none in the HTML (yet!).
But because the turbo-frame has a src
: it inmediately downloads the contents of tab_1_content.html.erb
. Loading its HTML and showing the broken dropdown and modal.
This translates to “When navigating to the admin page, the dropdown and modal are shown when they are suppose to be hidden and do not function correctly until I refresh the page”.
Now, in contrast, in the user page you render the components upon first page load. So the html received by the browser (and Alpine) upon page load is something like:
<h1>User</h1>
<div class="dropdown">
Alpine controlled drop down
</div>
<div class="modal">
Alpine controlled modal
</div>
Alpine is then able to mount the components because they have been rendered.
Hotfix solution (?)
(At least this works for me)
The basic solution that I have found does the following:
Upon first page load: set a MutationObserver that will listen to changes inside of all <turbo-frames>
present in the first page load. When the interior of those turbo-frames (aka: its child-nodes) change: look for all uninitialized Alpine components, and initialize them.
In case it can help you out: my (working) code is the following. This script (that first goes through webpack) is loaded only once in the <head>
. The thing I like the least is the setTimeout
, but I’ve checked and without it the code doesn’t work (but might be something specific to my project tho, I suggest you to try it without it).
import 'alpine-turbo-drive-adapter'
import Alpine from 'alpinejs'
let turboFramesObserver
document.addEventListener('turbo:load', () => {
if (turboFramesObserver && turboFramesObserver.disconnect)
turboFramesObserver.disconnect()
// The setTimout gives a little bit of time to Vue or other agents to render properly the newly injected HTML
setTimeout(() => {
// This has been greatly inspired by https://www.smashingmagazine.com/2019/04/mutationobserver-api-guide/
turboFramesObserver = new MutationObserver(mutations => {
for (let mutation of mutations) {
if (mutation.type === 'childList') {
Alpine.discoverUninitializedComponents(function(el) {
Alpine.initializeComponent(el)
})
}
}
})
// Gets all <turbo-frames> present in the page. Before they lazy-load their content.
let turboFrames = document.querySelectorAll('turbo-frame')
for (let turboFrame of turboFrames) {
// Observes each frame, firing the MutationObserver callback upon any "childList" mutation
turboFramesObserver.observe(turboFrame, {
attributes: false,
childList: true,
})
}
}, 500)
})
Ideal solution
In order to avoid the MutationObserver
object/API, the best (in my humbe opinion) would be if Turbo fires an event called something like turbo:frame-loaded
every time a frame finishes its rendering after receiving a new request from the server.
I’ve seen this talked in the Github discussions and in a couple of pull requests, so I (really) hope it will be implemented soon.