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

View Transitions Breaking AlpineJs alpine:init Event #11133

Closed
1 task done
ZachHandley opened this issue May 23, 2024 · 7 comments
Closed
1 task done

View Transitions Breaking AlpineJs alpine:init Event #11133

ZachHandley opened this issue May 23, 2024 · 7 comments
Labels
needs response Issue needs response from OP

Comments

@ZachHandley
Copy link

Astro Info

Astro                    v4.9.1
Node                     v22.2.0
System                   Linux (x64)
Package Manager          bun
Output                   static
Adapter                  none
Integrations             none

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

No response

Describe the Bug

Basically, I prefer to write my AlpineJs code inside script tags. It's cleaner, I benefit from Astro's TypeScript support, and I don't have to look at ugly JS in HTML quotes haha. SO anyways, AlpineJs has the event that fires, alpine:init. The issue I'm having is that, even if I inline the script, Astro's page-load event fires first. Which it should(? maybe) but I'm unable to get this to work. Take this example

<div x-data="someHomeDiv" x-ref="parentContainer">
  <img class="w-full h-full" x-ref="imgElement"/>
  <span class="text-lg font-blue-500" x-ref="textElement">Some Text to Animate</span>
</div>
<script>
const gsap = window.gsap
const ScrollTrigger = window.ScrollTrigger

document.addEventListener('alpine:init', () => {
  window.Alpine.data("someHomeDiv", () => {
    showing: false,
    init() {
      // This runs right away when the element is mounted, too soon for GSAP
      this.$nextTick(() => {
        // This runs after all content is loaded basically
        gsap.fromTo(this.$refs.textElement, {
          x: "-200vw",
        }, {
          x: "0",
          scrollTrigger: {
            trigger: this.$refs.parentElement,
          },
        });
    },
  });
});
</script>

This will error, because Alpine won't be defined yet

What's the expected result?

It would be awesome if astro:page-load waited until integrations were loaded, but I'm unsure of the best way to resolve it. I think either way, it should be fixed so one can use AlpineJs inside script tags. I could maybe make a function that waits for it to be initialized? I'm not sure yet

Link to Minimal Reproducible Example

https://stackblitz.com/edit/github-zdycxn?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 May 23, 2024
@martrapp
Copy link
Member

Hey @ZachHandley!
On first load, alpine:init fires before astro:page-load.
image

Your example does not use view transitions.
I added the Layout to the second page to enable them.
when I go to the second page and return, i see this:
image

The warning and the error in the first screenshot are due to astro:page-load firing too late for alpine initialization, not too early. Replace it with alpine:init as you wrote and the errors are gone.

What is your issue and how can i help you?

@martrapp martrapp added the needs response Issue needs response from OP label May 26, 2024
@ZachHandley
Copy link
Author

@martrapp Sorry, I suppose I should have advanced the example a bit to showcase what I mean.

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

The issue is, alpine:init doesn't run the same as page-load, at least not from my other projects, so if I use alpine:init, then I need to rerun the script with every load. Sure, you guys have a parameter for that, but it doesn't wait for alpine:init, so I ran in to this issue with gsap specifically, but.

https://stackblitz.com/edit/github-zdycxn?file=src%2Fpages%2Findex.astro maybe this will explain better?

@martrapp
Copy link
Member

Hi @ZachHandley thank you for updating the StackBlitz! I will take a look at right now.

@martrapp martrapp removed needs response Issue needs response from OP needs triage Issue needs to be triaged labels May 27, 2024
@martrapp
Copy link
Member

Hi @ZachHandley, could you please change the stackblitz a bit:

  • Please remove this.$refs.gradient.innerHTML = "ALPINEJS HECK YEAH"; from the second page as that element is not on this page.
  • Please define window.gsap somewhere, preferably in a script tag in your layout.

@martrapp martrapp added the needs response Issue needs response from OP label May 27, 2024
@martrapp
Copy link
Member

martrapp commented May 27, 2024

Hi @ZachHandley, if you want to use Astro's scripts (hoisted, bundled, module, typescript, ...) you must define the x-data entries before they are used in HTML elements. The simplest way to do this would be to move the scripts for all your pages into the Layout component and use the alpine:init event as currently in your index.astro.

That way, they all get executed on the initial page load (no matter if that is page one or two) and are available when you navigate to another page using view transitions.

If you dislike having all code in the layout, you could move it to separate .ts files as long as you import them all in a script tag in the layout.


Now that I assume that you still want to go the other route having per page scripts embedded in the Astro files as shown in your example, you can try to add this script to your Layout:

    <script>
      import Alpine from "alpinejs";
      document.addEventListener("astro:after-preparation", () => {
        Alpine.stopObservingMutations();
      });
      document.addEventListener("astro:page-load", () => {
        document.dispatchEvent(new Event("alpine:init"));
        Alpine.initTree(document.documentElement);
        Alpine.startObservingMutations();
      });
    </script>

Sadly, Alpine only offers a start method, but no stop or restart method. The code above is very close to that.

  • Before the view transition, stop Alpine's MutationObserver.
  • After swapping in the new DOM and after all scripts ran (i.e. added the eventListener for alpine:init), fire that event
  • let alpine do its thing on the whole DOM
  • reestablish the MutationObserver. (If you do not dynamically modify your HTML, you could omit this)

It is important to stop the MutationObserver, because it will react to DOM modifications rather directly. If it is not disabled, the x-data must be valid at after-swap which would not work using scripts on that page.

Let me know, which route you went and whether it worked 👍🏼 for you!

@ZachHandley
Copy link
Author

That works, it's not a perfect solution so I ended up going to Vue, but thank you regardless it's really cool that you can even do that haha

@jasonlav
Copy link
Contributor

jasonlav commented Sep 12, 2024

@martrapp Thank you for your contributions on this issue; they have been very helpful.

I was able to implement the "per page scripts embedded in the Astro files" approach in my application. However, I ran into a couple of issues.

  1. The init function on components was getting called twice on the first page load. I circumvented that issue fairly easily with a firstLoad variable.
let firstLoad = true;
document.addEventListener("astro:page-load", () => {
  if (firstLoad) {
    firstLoad = false;
  } else {
    document.dispatchEvent(new Event("alpine:init"));
    Alpine.initTree(document.documentElement);
    Alpine.startObservingMutations();
  }
});
  1. My application had a Header component that was persistent (transition:persist) across all pages. I was able to resolve the issue by adding x-ignore on Header component init. The component continued to function properly, but was never re-initiated. However, I was able to find a better solution when resolving issue the next issue below.

  2. Components in the existing page were never destroyed. The destroy Alpine function was never called and therefore I assume the code continued to run causing a potential memory leak (may be garbage collected when the HTML was removed, but unknown). To resolve this issue, I changed astro:before-preparation to call destroyTree(). However, this ended up destroying Alpine on all components, including the persistent Header. I wrapped the <slot /> tag in Layout.astro with <div id="site"><slot /></div> and then change destroyTree and initTree to target the #site div.

Therefore, on first page load, Alpine initiates on the entire page. However, when you switch pages, it only destroys and rebuilds the main slot in the layout. I have not noticed any issues with this approach, however, it could be problematic depending on the structure and necessary functionality of your application.

<html lang="en">
<head>
  <ViewTransitions />
</head>
<body>
  <Header /> <!-- persistent -->
  <div id="slot">
    <slot />
  </div>
</body>
</html>
let firstLoad = true;

document.addEventListener('astro:before-preparation', (event) => {
   Alpine.stopObservingMutations();
  Alpine.destroyTree(document.getElementById("slot"));
});

document.addEventListener("astro:page-load", () => {
  if (firstLoad) {
    firstLoad = false;
  } else {
    document.dispatchEvent(new Event("alpine:init"));
    Alpine.initTree(document.getElementById("slot"));
    Alpine.startObservingMutations();
  }
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs response Issue needs response from OP
Projects
None yet
Development

No branches or pull requests

3 participants