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

Astro v5: AlpineJS alpine:init event not working #12710

Open
1 task
xandermann opened this issue Dec 10, 2024 · 18 comments
Open
1 task

Astro v5: AlpineJS alpine:init event not working #12710

xandermann opened this issue Dec 10, 2024 · 18 comments
Labels
- P2: has workaround Bug, but has workaround (priority) needs discussion Issue needs to be discussed needs triage Issue needs to be triaged

Comments

@xandermann
Copy link

xandermann commented Dec 10, 2024

Astro Info

Astro                    v5.0.4
Node                     v23.4.0
System                   Linux (x64)
Package Manager          npm
Output                   static
Adapter                  none
Integrations             @astrojs/alpinejs

If this issue only occurs in one browser, which browser is a problem?

No response

Describe the Bug

The event of AlpineJS alpine:init is not working anymore in Astro 5.

document.addEventListener('alpine:init', () => {
    console.log("test") // never called in astro 5
});

It works in astro 4.16.17, but not in astro 5.0.4

What's the expected result?

document.addEventListener('alpine:init', () => {
// this event should be triggered
});

The AlpineJS documentation shows that it should work : https://alpinejs.dev/globals/alpine-data

Link to Minimal Reproducible Example

https://stackblitz.com/edit/github-sscqvr22?file=src%2Fpages%2Findex.astro

Participation

  • I am willing to submit a pull request for this issue.
@github-actions github-actions bot added the needs triage Issue needs to be triaged label Dec 10, 2024
@xandermann xandermann changed the title Astro v5 - AlpineJS not working Astro v5: AlpineJS alpine:init event not working Dec 10, 2024
@florian-lefebvre
Copy link
Member

I'm surprised this even worked in v4, since the recommend way for Astro is to use the entrypoint option, see updated reproduction.

I wonder is this is caused by this breaking change https://docs.astro.build/en/guides/upgrade-to/v5/#script-tags-are-rendered-directly-as-declared

@florian-lefebvre florian-lefebvre added - P2: has workaround Bug, but has workaround (priority) and removed needs triage Issue needs to be triaged labels Dec 13, 2024
@emmanuelsw
Copy link

Has anyone found a possible solution?
It's the only thing stopping us from upgrading to v5.

@thiagokisaki
Copy link

thiagokisaki commented Jan 15, 2025

Moving <script> to <head> seems to work for me.

<!doctype html>
<html lang="en-US">
  <head>
    ...
    <script>
      document.addEventListener('alpine:init', () => {
        console.log('test');
      });
    </script>
  </head>
  <body>
    ...
  </body>
</html>

If you want to add a <script> to <head> inside a page that uses layouts, you can use slots.

layouts/Layout.astro:

---
---

<!doctype html>
<html lang="en-US">
  <head>
    ...
    <slot name="in-head"></slot>
  </head>
  <body>
    <slot></slot>
  </body>
</html>

pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="Home">
  <main>
    <h1>Home</h1>
    <div x-data="counter">
      <span x-text="value"></span>
      <button @click="increment">Increment</button>
    </div>
  </main>
  <Fragment slot="in-head">
    <script>
      import Alpine from 'alpinejs';

      Alpine.data('counter', () => ({
        value: 0,
        increment() {
          this.value++;
        }
      }));
    </script>
  </Fragment>
</Layout>

However, this workaround doesn't work for scripts in components. I think it would be great if <script> supported a directive to allow moving it to <head>.

@florian-lefebvre
Copy link
Member

By the way, does it work if you add the is:inline directive to the concerned script? I think that could work in most cases, assuming you don't need to use import a npm dependency

@thiagokisaki
Copy link

Yes, using is:inline also works.

---
import Layout from '@/layouts/Layout.astro';
---

<Layout title="Home">
  <main>
    <h1>Home</h1>
    <div x-data="counter">
      <span x-text="value"></span>
      <button @click="increment">Increment</button>
    </div>
  </main>
</Layout>

<script is:inline>
  document.addEventListener('alpine:init', () => {
    Alpine.data('counter', () => ({
      value: 0,
      increment() {
        this.value++;
      }
    }));
  });
</script>

@florian-lefebvre
Copy link
Member

I discussed it with other core maintainers and we think you were depending on implicit, unintentional behavior, and directRenderScript broke that. You'll have to adjust to the new behavior but there's nothing we can fix here, because it's not a bug. The main takeways are:

  • Use is:inline scripts
  • If you reference Alpine, make sure to use the window global and not an import to the alpine npm package
  • if you need something global, use the app entrypoint

Hope that helps

@florian-lefebvre florian-lefebvre closed this as not planned Won't fix, can't repro, duplicate, stale Jan 15, 2025
@n-gt
Copy link

n-gt commented Jan 15, 2025

better than nothing!
time to is:inline that $$$$ and write some typeless Alpine JS with 0 auto completion like it's 2025.

StandingOvationGIF

@jasonlav
Copy link

jasonlav commented Jan 15, 2025

When using AlpineJS with an entry point (as recommended) the issue exists in v5 and works in v4. As far as I can tell, the real issue is that alpine:init event handler isn't being called unless a script tag is is:inline.

The beauty of AstroJS + AlpineJS is that Alpine components that may be used on multiple times within a page or on different pages are bundled into a shared Javascript file. If you have an Alpine component (e.g. an article card) that is render 20 times on a page, the is:inline script tag would be rendered 20 times! When I use AstroJS + AlpineJS, I specifically create components to not use is:inline to avoid this issue.

If all scripts must be inline, existing AstroJS v4 sites really cannot be upgraded to v5 without significant issues, unless they were built sub-optimally (e.g. all scripts where is:inline). To be direct, that sounds like a bug not previously leveraging "unintentional behavior."

Here is another example illustrating the issue. Note that this example works in v4, but does not work in v5. https://stackblitz.com/edit/withastro-astro-xaotss6x?file=src%2Fpages%2Findex.astro&title=Astro%20Starter%20Kit:%20Minimal

@kslstn
Copy link

kslstn commented Jan 16, 2025

I think I found a workaround! Me just being a tinkerer and the 'depending on implicit, unintentional behavior' comment makes me wonder how solid it is. If someone could explain why this is a bad idea, I'd be happy to hear it.

Edit: I do not recommend this! It only works when a single Alpine component is on the page, as otherwise Alpine.start() is called multiple times.

I've removed alpinejs() from the integrations in defineConfig(). Then in my .astro file's script tag, I import Alpine and only start it after defining my Alpine component:

<!-- Do not copy this code before reading my edit above! -->
<script>
    import Alpine from "alpinejs";
    import myUtility from "@src/utilities/myUtility";
    import type { Locale } from "@i18n/i18n";

    type Props = {
        value: string | number;
        locale: Locale;
    };
    document.addEventListener("alpine:init", () => {
        Alpine.data("myComponent", (props: Props) => ({
          // Add component logic here ...
        }));
    });

    window.Alpine = Alpine;
    Alpine.start();
</script>

@florian-lefebvre
Copy link
Member

The beauty of AstroJS + AlpineJS is that Alpine components that may be used on multiple times within a page or on different pages are bundled into a shared Javascript file. If you have an Alpine component (e.g. an article card) that is render 20 times on a page, the is:inline script tag would be rendered 20 times! When I use AstroJS + AlpineJS, I specifically create components to not use is:inline to avoid this issue.

That sounds fair, I'll reopen and bring it up again

better than nothing! time to is:inline that $$$$ and write some typeless Alpine JS with 0 auto completion like it's 2025.

Fwiw you can have type checked inline scripts by using // @ts-check and jsdoc annotations IIRC

@florian-lefebvre florian-lefebvre added the needs discussion Issue needs to be discussed label Jan 16, 2025
@github-actions github-actions bot added the needs triage Issue needs to be triaged label Jan 16, 2025
@kslstn
Copy link

kslstn commented Jan 16, 2025

Turns out, my silly workaround above isn't very useful if you have more than one Alpine component on a page.

I've fallen back to my Astro <4 workaround:

  1. Remove alpinejs() from the integrations in defineConfig()
  2. Add this to all layouts using Alpine (I do this via my <Html> Astro component that I use in all layouts):
<script>
  import Alpine from "alpinejs";
  window.setTimeout(() => {
    window.Alpine = Alpine;
    Alpine.start();
  }, 1);
</script>

@jasonlav
Copy link

Thank you for reopening the ticket.

I'm not highly familiar with the inner workings of Astro under the hood, but I did some investigation.

It appears that the alpine:init event is being triggered prior to script tags that are not is:inline. In addition, script tags that are not is:inline do not appear to be bundled into compiled Javascript. Instead they are being appended to the page.

For example the following component (Welcome.astro) script:

<script>
	document.addEventListener('alpine:init', () => {
		Alpine.data('Welcome', () => ({
			welcome: "Hello, world!",
		}));
	});
</script>

On build, this script is compiled into the HTML as a module:

<script type="module">document.addEventListener("alpine:init",()=>{Alpine.data("Welcome",()=>({welcome:"Hello, world!"}))});</script>

This may be considered intentional behavior, however, it is not how Astro V4 handled scripts tags in components from my experience.

@florian-lefebvre
Copy link
Member

I think injecting the script at the end of the page could help, as it would run after all other scripts are initialized (I may be wrong). @bluwy do we have a way to do that? Right now the integration uses injectScript('page', '...') but I suspect it adds the script to the head

@jasonlav
Copy link

jasonlav commented Jan 16, 2025

According to the Astro documentation (https://docs.astro.build/en/reference/directives-reference/#isinline):

By default, Astro will process, optimize, and bundle any <script> and <style> tags that it sees on the page. You can opt-out of this behavior with the is:inline directive.

This statement does not appear to be true in Astro V5 regardless of using the AlpineJS integration.

<script>
	console.log("hello, world!");
</script>

The above script will be rendered as an inline script module:

<script type="module">console.log("hello, world!");</script>

With AlpineJS integrated, if I manually remove type="module" from script tag in a build, Alpine appears to function properly. According to MDN (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#applying_the_module_to_your_html) type=module scripts are automatically deferred. Alpine documentation (https://alpinejs.dev/essentials/installation#from-a-script-tag) recommends including the script deferred. My assumption is that this enables Alpine to automatically run after non-deferred scripts. If there are other deferred scripts depending on Alpine, Alpine would need to be run last.

// Works
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<script>
  document.addEventListener('alpine:init', () => {
    alert("alpine:init");
  });
</script>
// Works
<script type="module">
  document.addEventListener('alpine:init', () => {
    alert("alpine:init");
  });
</script>
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
// Does not work
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<script type="module">
  document.addEventListener('alpine:init', () => {
    alert("alpine:init");
  });
</script>

In AstroV4 + AlpineJS, it appears there are two compiled javascript files for each page: hoisted.{hash}.js and page.{hash}.js. Both are included as modules; hoisted.{hash}.js runs before page.{hash}.js. The very last line of page.js is starting Alpine JS.

In AstroV5 + AlpineJS it appears that the hoisted.{hash}.js file is no longer used in favor of just page.{hash}.js. The last line of page.{hash}.js is starting AlpineJS.

Optimally, AlpineJS integration should work for scripts that are bundled or inline. For that to be the case, it seems that Alpine.start() needs to be called at the end of the page as you appear to be suggesting.

@florian-lefebvre
Copy link
Member

@jasonlav
Copy link

jasonlav commented Jan 17, 2025

@florian-lefebvre That is correct. However, from what I can see in the Git history, that integration has not changed from Astro 4 to Astro 5.

I appreciate the fact that I am one developer using Astro with AlpineJS and Astro is used by many developers with other integrations. However, from my perspective, the issue is that Astro is not properly bundling scripts as the documentation states (https://docs.astro.build/en/reference/directives-reference/#isinline).

By default, Astro will process, optimize, and bundle any <script> and <style> tags that it sees on the page. You can opt-out of this behavior with the is:inline directive.

Since the compiled script with AlpineJS is included as a type=module in the head and the component script is included as type=module in the body, the component script therefore fires after Alpine.start(). The proper solution, as I see it, is to restore Astro 4 functionality where component scripts are bundled into the page javascript file. This will not only resolve the Astro JS issue, but it will also properly optimize Javascript for user delivery.

@florian-lefebvre
Copy link
Member

florian-lefebvre commented Jan 17, 2025

@jasonlav this is not as easy, we've removed the old behavior for good reasons (I can't explain because I was not involved in this process). Anyways, thanks for all the details. At this point, I feel like we have all the information we need to think about it, we just need to take the time. Do not expect a quick fix because it doesn't sound trivial

@jasonlav
Copy link

jasonlav commented Jan 17, 2025

I understand.

Correction: I previously said if a page had multiple instances of the same component and that component had an is:inline script, it will print the same script tag multiple times. That is incorrect; it will only print it once.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
- P2: has workaround Bug, but has workaround (priority) needs discussion Issue needs to be discussed needs triage Issue needs to be triaged
Projects
None yet
Development

No branches or pull requests

7 participants