Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS file always runs after the turbo:load event, causing x-data to throw a ReferenceError on first navigation #31

Open
timohermans opened this issue Mar 9, 2021 · 6 comments
Labels
bug Something isn't working enhancement New feature or request help wanted Extra attention is needed

Comments

@timohermans
Copy link

So when I'm trying to move from page A to page B with Turbo (7-beta4) drive, the alpine x-data function that is in a separate js file is always executed after the initCallback of this package, causing a ReferenceError. I'm not sure if it has anything with the following setup:

Both page A and B have a site.js (it's in the layout file) script that contains the following:

import * as Turbo from "@hotwired/turbo"
import 'alpine-turbo-drive-adapter'
import 'alpinejs';

Page A has some alpine functionality in the navbar.

Page B has an additional scripts which loads only the x-data functions. Example:

window.initStudentNameController = function() {
  [...]
}

I've tried several things that didn't work:

  • Not use webpack for the Page B js file and load it in separately with an old school script tag at the bottom of the body
  • Try to move around the Page A/B site.js script (not in head, not deferred, not type moduled, etc.)

The only thing that works (aside from reloading the page) is when I put the page B functions in a script tag in the HTML itself.

Does it have something to do with the fact that the site.js script doesn't get reloaded when moving from page A to B?

See gif below

maybe-bug

@SimoTod
Copy link
Owner

SimoTod commented Mar 9, 2021

Hi @timohermans
I would expect it to work. I'll try to replicate your setup and see if I can figure out what's going on.

Just to clarify, you have something like

site.js

import * as Turbo from "@hotwired/turbo"
import 'alpine-turbo-drive-adapter'
import 'alpinejs';
window.testComponent = function() {
  return {foo: 'bar'}
}

a.html

<html>
  <head>
    <script src="site.js">
  </head>
  <body>
    <div x-data x-text="'Alpine has loaded'"></a>
    <a href="b.html">click me</a>
  </body>
</a>

b.html

<html>
  <head>
    <script src="site.js">
  </head>
  <body>
    <div x-data="testComponent()" x-text="foo"></a>
  </body>
</a>

Is that correct?

@timohermans
Copy link
Author

Thanks for the quick reply! Not quite right. Let me see if I can edit your example:

site.js

import * as Turbo from "@hotwired/turbo"
import 'alpine-turbo-drive-adapter'
import 'alpinejs';
window.testComponentA = function() {
  return {foo: 'bar'}
}

a.html

<html>
  <head>
    <script src="site.js"></script>
  </head>
  <body>
    <nav>
      <div x-data="testComponentA()" x-text="'Alpine has loaded'"></a>
    </nav>
    <a href="b.html">click me</a>
  </body>
</html>

custom.js

window.testComponentB = function() {
  return {foo: 'bar'}
}

b.html

<html>
  <head>
    <script src="site.js"></script>
    <script src="custom.js"></script>
  </head>
  <body>
    <nav>
        <div x-data="testComponentA()" x-text="'Alpine has loaded'"></a>
    </nav>
    <div x-data="testComponentB()" x-text="foo"></div>
  </body>
</html>

@SimoTod
Copy link
Owner

SimoTod commented Mar 9, 2021

@timohermans I think it's a bug, or at least an unexpected behavior, in turbo, unless I misunderstood their events.
In turbolinks, the load event fires after the page content is swapped (and I believe after any additional scripts are loaded) so your custom script will be there but in turbo, the load event fires on the new HTML but before the dom is replaced in the browser so your custom script is not retrieved yet. I think that would cause issues with any script that would use the load event to initialize new libraries.

While I'm trying to understand it better, it would work if you use a single bundle with all your controllers in it.

@timohermans
Copy link
Author

it would work if you use a single bundle with all your controllers in it.

I think you are absolutely right. I still had the MPA mindset to separate all the JS per page. But with using turbo the border between MPA and SPA is fading and I guess it makes sense to put this all in one bundle, even though payload would become bigger and bigger per page load :(.

so possible solutions are:

  • Use inline javascript in a script tag in the HTML
  • Put all AlpineJS logic in one bundle

I think that would cause issues with any script that would use the load event to initialize new libraries.

Agree

I think it's a bug, or at least an unexpected behavior, in turbo, unless I misunderstood their events.
In turbolinks, the load event fires after the page content is swapped (and I believe after any additional scripts are loaded) so your custom script will be there but in turbo, the load event fires on the new HTML but before the dom is replaced in the browser so your custom script is not retrieved yet.

So then the question is: how can we tackle this issue 😊?

@SimoTod
Copy link
Owner

SimoTod commented Mar 9, 2021

I'm looking into it. I tried to delay the Alpine callback and I wrote a test to replicate the issue, it passes locally but it fails on github. I assume the turbo:load event fires independently from the page assets so there's no easy way to check when the new javascript has been downloaded, maybe I'll ask the turbo guys. I'll keep you posted.

@SimoTod
Copy link
Owner

SimoTod commented Mar 9, 2021

So, I ran more tests and it was actually the same with the old turbolinks. The main problem is that the turbo events (render, load, etc) fire when the new dom is ready but additional assets are loaded asynchronously and they can take an arbitrary time to load so, when turbo/turbolinks fire the load event, your custom.js may or may not be ready.

I can delay the execution a bit but people with a slow connection would still experience the issue so I'd rather not add an incomplete fix.

Unfortunately, the native readystate only works on a real page load and it cannot be used for those quick loadings so it seems it's impossible to know when your js libraries are ready. This becomes obvious with Alpine but you would have the same issue if you try to start a js datepicker on a secondary page if the script is loaded only on that page, for instance.

Blocking the page until the js is ready, if it's ever possible, would defeat the purpose of Turbo where the website should act as an SPA so for now the easy way would be one of the two you mentioned.

If you go for a single bundle the payload won't grow at each page load since it will only be loaded the first time (and possibly cached by the browser for future visits). Depending on your backend framework, it may be easy to include those scripts inline as well

@SimoTod SimoTod added enhancement New feature or request help wanted Extra attention is needed bug Something isn't working labels Mar 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants