Jump to content
Search Community

Problem with accesing some properties of an element when using Nuxt 3

stefanobartoletti test
Moderator Tag

Recommended Posts

Hi, I'm trying to solve a problem that I'm having when trying to integrate Gsap with Nuxt 3.

 

I started from the official example here https://stackblitz.com/edit/nuxt-starter-khrgsj

 

My component is this one

 

<template>
  <section class="overflow-hidden py-24">
    <div ref="main" class="flex">
      <span
        v-for="i in 3"
        :key="i"
        class="block whitespace-pre font-headings text-7xl font-light leading-tight text-base-300 md:text-9xl lg:text-[15rem]"
        gsap="scrolling-text"
      >{{ text + ' - ' }}</span>
    </div>
  </section>
</template>

<script setup>
import gsap from 'gsap'

const text = 'Lorem ipsum dolor'

const main = ref()
const tl = ref()
const ctx = ref()

onMounted(() => {
  ctx.value = gsap.context((self) => {
    const scrollingText = self.selector('[gsap="scrolling-text"]')

    const textWidth = scrollingText[0].offsetWidth
    const textHTML = scrollingText[0].innerHTML
    const windowWidth = window.innerWidth

    console.log(scrollingText[0])
    console.log(textWidth, textHTML)

    tl.value = gsap
      .timeline({ repeat: -1 })
      .to(scrollingText, {
        x: -textWidth,
        duration: 20 * (textWidth / windowWidth),
        ease: 'none',
      })
  }, main.value) // <- Scope!
})

onUnmounted(() => {
  ctx.value.revert() // <- Easy Cleanup!
})
</script>


The gsap animation is meant to animate a given element based on its dimensions amongst other things.

At first page load everything works well, the element is correctly animated, and the console.log give this output:

2131 Lorem ipsum dolor -

but when navigating to other pages/routes, the animation is not working, and console.log gives this:

0 Lorem ipsum dolor -

Please note that the first value is offsetWidth and the second innerHTML

I don't really have many clues about what's going on there, except that maybe these values are get in a wrong moment of the lifecycle when the elements are present (so the correct access of innerHTML) but not yet properly rendered/displayed (so, no correct value of offsetWidth)

Or it can be some issues with the unmount phase, since on the first page load everything works fine.

Can someone try to help me debug this and maybe propose a solution?

Link to comment
Share on other sites

It's pretty tough to troubleshoot without a minimal demo - the issue could be caused by CSS, markup, a third party library, your browser, an external script that's totally unrelated to GSAP, etc. Would you please provide a very simple CodePen or CodeSandbox that demonstrates the issue? 

 

Please don't include your whole project. Just some colored <div> elements and the GSAP code is best (avoid frameworks if possible). See if you can recreate the issue with as few dependancies as possible. If not, incrementally add code bit by bit until it breaks. Usually people solve their own issues during this process! If not, then at least we have a reduced test case which greatly increases your chances of getting a relevant answer.

 

Here's a starter CodePen that loads all the plugins. Just click "fork" at the bottom right and make your minimal demo

See the Pen aYYOdN by GreenSock (@GreenSock) on CodePen

 

If you're using something like React/Next/Vue/Nuxt or some other framework, you may find StackBlitz easier to use. We have a series of collections with different templates for you to get started on these different frameworks: React/Next/Vue/Nuxt.

 

Once we see an isolated demo, we'll do our best to jump in and help with your GSAP-specific questions. 

Link to comment
Share on other sites

This is a basic fork of StackBlitz, to show what I'm trying to achieve.

https://stackblitz.com/edit/nuxt-starter-hxrstv?file=pages%2Findex.vue

It seems that on stackblitz is is working normally even on route changes, but in my actual setup I have dynamic routes accessed from pages/[...slug].vue instead of static routes.

 

I still don't exactly know how to diagnose further this.

Thanks for your help.

Link to comment
Share on other sites

Also, I've tried to use a timeout, something like this

 

onMounted(() => {
  ctx.value = gsap.context((self) => {
    const scrollingText = self.selector('[gsap="scrolling-text"]')

    setTimeout(() => {
    	const textWidth = scrollingText[0].offsetWidth
    	const textHTML = scrollingText[0].innerHTML
    	const windowWidth = window.innerWidth

    	console.log(scrollingText[0])
        console.log(textWidth, textHTML)

        tl.value = gsap
          .timeline({ repeat: -1 })
          .to(scrollingText, {
            x: -textWidth,
            duration: 20 * (textWidth / windowWidth),
            ease: 'none',
          })
    }, '1000')
  }, main.value) // <- Scope!
})

onUnmounted(() => {
  ctx.value.revert() // <- Easy Cleanup!
})

 

And now it seems to work, but of course it is not a proper solution, the animation should not be delayed that much

Link to comment
Share on other sites

Hi,

 

I just tried the link you provided and it seems to work as I navigate to other pages and then back to the home route.

 

What you could try is Vue's nextTick method:

https://vuejs.org/api/general.html#nexttick

 

Something like this:

<script setup>
import { nextTick } from 'vue';
import gsap from 'gsap';

const text = 'Lorem ipsum dolor';

const main = ref();
const tl = ref();
const ctx = ref();

onMounted(async () => {
  await nextTick();
  ctx.value = gsap.context((self) => {
    const scrollingText = self.selector('[gsap="scrolling-text"]');

    const textWidth = scrollingText[0].offsetWidth;
    const textHTML = scrollingText[0].innerHTML;
    const windowWidth = window.innerWidth;

    console.log(scrollingText[0]);
    console.log(textWidth, textHTML);

    tl.value = gsap.timeline({ repeat: -1 }).to(scrollingText, {
      x: -textWidth,
      duration: 20 * (textWidth / windowWidth),
      ease: 'none',
    });
  }, main.value); // <- Scope!
});

onUnmounted(() => {
  ctx.value.revert(); // <- Easy Cleanup!
});
</script>

That also seems to work as expected.

 

Hopefully this helps. Let us know if you have more questions.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Thanks for your answer, it is really appreciated :)

Unfortunately nextTick isn't working either 😕

 

I don't exactly know what is causing the difference between my real project and the demo example on stackblitz. In my real project I have dynamic routes and data received from a CMS API, but this seems to be unrelated.
Even by putting in fake static data (i.e. like I did on the example), the problem is still there, so I think that is is not caused by a micro delay in waiting the API (it wuld break the animation even on the first load I think).

The only difference is the dynamic route, but even here, it does not really seem so related.

 

At the moment, my only clue is that with the timeout set at a high value (1000ms) it works, if I lower it it stops working. So there should be some kind of issue o the lifecycle of the component.

 

Do you think that in my code, the context is correctly destroyed at onUnmounted? could the problem be related to a previous instance of the animation not completely unloaded and causing problems with the new one?

Link to comment
Share on other sites

1 hour ago, StefanoB said:

Do you think that in my code, the context is correctly destroyed at onUnmounted? could the problem be related to a previous instance of the animation not completely unloaded and causing problems with the new one?

This is definitely problematic: 

onMounted(() => {
  ctx.value = gsap.context((self) => {
    const scrollingText = self.selector('[gsap="scrolling-text"]')

    setTimeout(() => {
    	const textWidth = scrollingText[0].offsetWidth
    	const textHTML = scrollingText[0].innerHTML
    	const windowWidth = window.innerWidth

    	console.log(scrollingText[0])
        console.log(textWidth, textHTML)

        tl.value = gsap
          .timeline({ repeat: -1 })
          .to(scrollingText, {
            x: -textWidth,
            duration: 20 * (textWidth / windowWidth),
            ease: 'none',
          })
    }, '1000')
  }, main.value) // <- Scope!
})

onUnmounted(() => {
  ctx.value.revert() // <- Easy Cleanup!
})

Because the gsap.context() will only record all the GSAP-related stuff that is created WHILE that function is executed. But you're using a setTimeout(), so you're no longer creating your GSAP-related stuff during that function execution, thus it doesn't get recorded in the context and your ctx.value.revert() in your onUnmounted() isn't actually cleaning things up. It was very curious to me that you used a string ('1000') for your setTimeout() instead of a number. I'm pretty sure that's supposed to be a number. Are you confident that it's waiting the amount of time you intended? 

 

You can still use a setTimeout() if you really want to, you just need to put your gsap.context() inside that (instead of the other way around): 

setTimeout(() => {
  ctx.value = gsap.context(() => {
    // your code here
  }, main.value);
}, 1000);

 

I would recommend putting a console.log() in your onMounted() and onUnmounted() just to make sure things are getting called in the order you think they are. For example, I wonder if your onUnmounted() is getting called (and reverting stuff) when you didn't think it was. I'm totally guessing here since we don't have a minimal demo that shows the issue occurring. 

  • Like 1
Link to comment
Share on other sites

Thanks for the insights.

I do not really need or want to use a timeout, it is not a clean solution, I just tried to use it to better understand what was going on.


Anyway, I tried putting console.logs along all the lifecycle of the component, and all of them fire in the expected order.

I also tried assigning some extra properties (color and background color) from gsap, something like this:

 

    tl.value = gsap
      .timeline({ repeat: -1 })
      .to(scrollingText, {
        x: -textWidth,
        duration: 20 * (textWidth / windowWidth),
        ease: 'none',
        color: '#976239',
        backgroundColor: '#456234',
      })


And they are applied also in changing routes, so I can also exclude  gsap not correctly targeting the given element.

The real problem lies in reading some of the targeted element properties, specifically the dimensions, since apparently some others are read without any issue (like innerHTML)

I'd like to provide another minimal demo, but it seems that there is something going on related to the full configuration of my project (the same specific gsap instance is applied to the stackblitz that I provided and it works there).

Are there some other information that I can gather to tr to get a better picture if this issue?

Link to comment
Share on other sites

Hi,

 

I downloaded the project and ran it on my local machine and I can't replicate the issue 🤷‍♂️

 

The one thing I can get from the context of your app is that you're trying to create an endless marquee, right? If that is the case you should take a look at the endless horizontal loop helper function:

https://greensock.com/docs/v3/HelperFunctions#loop

 

That should remove the need for, well, pretty much all of your setup and perhaps solve the issues you're having now.

 

Hopefully this helps.

Happy Tweening!

Link to comment
Share on other sites

Thanks for the suggestion, I managed to get an inspiration by your solution by using the xPercent property instead of trying to access the element width directly.

 

Now this code works:

 

onMounted(() => {
  ctx.value = gsap.context((self) => {
    const scrollingText = self.selector('[gsap="scrolling-text"]')

    tl.value = gsap
      .timeline({ repeat: -1 })
      .to(scrollingText, {
        xPercent: -100,
        duration: 20 * ((1 / 1920) * window.innerWidth), // Need to better define duration/speed
        ease: 'none',
      })
  }, main.value) // <- Scope!
})

 

I still need to tweak the duration to provide a consistent experience between various screensizes, but this is another matter.

 

And of course I have not solved the main problem itself, directly accessing needed properties, but at least I have fixed this animation.

 

Thanks for your help.

  • Like 1
Link to comment
Share on other sites

This really sounds like you're creating your animation(s) BEFORE images load or something, so perhaps the width is 0. For example, an <img> element that you set no width/height on in your CSS will have to wait until the image loads to determine its width/height and then reflow everything (that's generally a bad idea, as reflow is expensive). Did you set a width/height on your elements in CSS? If not, maybe you just need to add a "load" event listener and THEN create your animations. But using xPercent/yPercent is a good idea since that's based on the size of the element anyway. 

Link to comment
Share on other sites

2 hours ago, StefanoB said:

Does it matter for Gsap where my ref="main" is located on the template?

It matters regarding the scope GSAP Context will have for the selectors you use inside that particular context instance, nothing more.

 

Once again, without a minimal demo is hard for us to know exactly what the issue could be.

 

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...