Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
Cyd

Adding locomotive scroll & scrollerProxy to Nuxt creates weird behavior when routing

Recommended Posts

It appears as though the scrollerProxy is initiated multiple times after routing which causes the matrix 3d animation to jump from -232213 to 0 and back.
I think this is because the nuxt instance is kept while routing between pages that also use the _.vue page template but I cant figure out how to force it to forget the past scrollerProxy ūüėĘ
Any advice as to how to initiate smoothscrolling in Nuxt?
 

// _.vue template
<template>
    <div>
    </div>
</template>

<script>
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
export default {
    mounted() {
        this.smooth()
        this.$forceUpdate()
    },
    destroyed() {
        // used to kill the ScrollTrigger instance for this component
        this.expand = false
    },
    methods: {
        smooth() {
            if (process.client) {
                window.scrollTo(0,0)
                const locoScroll = new this.LocomotiveScroll({
                    el: document.querySelector('.smooth-scroll'),
                    smooth: true
                })
                // each time Locomotive Scroll updates, tell ScrollTrigger to update too (sync positioning)
                locoScroll.on('scroll', ScrollTrigger.update)

                ScrollTrigger.scrollerProxy('.smooth-scroll', {
                    scrollTop(value) {
                        return arguments.length ? locoScroll.scrollTo(value, 0, 0) : locoScroll.scroll.instance.scroll.y
                    }, // we don't have to define a scrollLeft because we're only scrolling vertically.
                    getBoundingClientRect() {
                        return {top: 0, left: 0, width: window.innerWidth, height: window.innerHeight}
                    },
                    // LocomotiveScroll handles things completely differently on mobile devices - it doesn't even transform the container at all! So to get the correct behavior and avoid jitters, we should pin things with position: fixed on mobile. We sense it by checking to see if there's a transform applied to the container (the LocomotiveScroll-controlled element).
                    pinType: document.querySelector('.smooth-scroll').style.transform ? 'transform' : 'fixed'
                })
                // each time the window updates, we should refresh ScrollTrigger and then update LocomotiveScroll.
                ScrollTrigger.addEventListener('refresh', () => locoScroll.update())

                // after everything is set up, refresh() ScrollTrigger and update LocomotiveScroll because padding may have been added for pinning, etc.
                ScrollTrigger.refresh()
                const headerItems = ['.header__logo', '.nav', '.header__trigger']
                if (!this.isTouchDevice()) {
                    headerItems.forEach((item) => {
                        gsap.to(item, {
                            scrollTrigger: {
                                trigger: '.header',
                                scroller: '.smooth-scroll',
                                scrub: true,
                                pin: true,
                                start: 'top',
                                end: document.querySelector('body').offsetHeight,
                                pinSpacing: false,
                            },
                            y: document.querySelector('body').offsetHeight,
                            transformOrigin: 'center top',
                            ease: 'none'
                        })
                    })
                    const largeMedia = document.querySelectorAll('.large-media.no-controls')
                    if (largeMedia) {
                        largeMedia.forEach((media) => {
                            let mediaItem = media.querySelector('video')
                            if (media.querySelector('img')) {
                                mediaItem = media.querySelector('img')
                            }
                            gsap.to(mediaItem, {
                                scrollTrigger: {
                                    trigger: media,
                                    scroller: '.smooth-scroll',
                                    scrub: true,
                                    start: 'top',
                                    end: 'bottom',
                                },
                                y: '100%',
                                transformOrigin: 'center top',
                                ease: 'none'
                            })
                        })
                    }

                    const nextCase = document.querySelector('.next-case')
                    if (nextCase) {
                        gsap.to('.next-case .large-media', {
                            scale: 1,
                            opacity: 0,
                            scrollTrigger: {
                                trigger: nextCase,
                                scroller: '.smooth-scroll',
                                start: `top-=${window.innerHeight / 2}`,
                                end: `bottom-=${window.innerHeight}`,
                                scrub: 1,
                            }
                        })
                        gsap.to('.next-case__background', {
                            opacity: 1,
                            scrollTrigger: {
                                trigger: nextCase,
                                scroller: '.smooth-scroll',
                                start: `top-=${window.innerHeight / 2}`,
                                end: `bottom-=${window.innerHeight}`,
                                scrub: 1,
                            }
                        })
                        gsap.to('.large-text-header', {
                            opacity: 1,
                            scrollTrigger: {
                                trigger: nextCase,
                                scroller: '.smooth-scroll',
                                start: `top-=${window.innerHeight / 2}`,
                                end: `bottom-=${window.innerHeight}`,
                                scrub: 1,
                            }
                        })
                        const observerTrigger = document.querySelector('.next-case__observer-trigger')
                        const onIntersection = (entries) => {
                            for (const entry of entries) {
                                if (entry.isIntersecting) {
                                    this.loaded = entry.intersectionRatio
                                    if (entry.intersectionRatio > 0.95) {
                                        this.background = true
                                        if (!this.expand) {
                                            // window.location.href = nextCase.querySelector('a').getAttribute('href') // goor, doch effectief
                                            this.$router.push(nextCase.querySelector('a').getAttribute('href'))
                                            this.expand = true
                                        }
                                    }
                                }
                            }
                        }
                        let threshold = [] // create array with numbers between 0 and 1
                        for (var i = 0; i <= 100; i++) {
                            threshold.push(i / 100)
                        }
                        const observer = new IntersectionObserver(onIntersection, { threshold })
                        observer.observe(observerTrigger)
                    }
                }
            }
        },
        isTouchDevice() {
            try {
                document.createEvent('TouchEvent')
                return true
            } catch (e) {
                return false
            }
        }
    },
}
</script>


 

Link to post
Share on other sites

Hey @Cyd and welcome. Would you mind putting this into a live demo for us to edit?

 

Link to post
Share on other sites

Hi Zach, thanks, it took some time but I managed to recreate my issue in code sandbox https://codesandbox.io/s/rough-moon-171sw?file=/layouts/default.vue

The problem arises when you switch pages (route) with a nuxt link, it keeps it's initial data and I do not know how to remove all the scrolltrigger and locomotive scroll data beforeDestroy. 
I've tried killing all scrolltrigger instances in the Nuxt beforeDestroy method withScrollTrigger.getAll().forEach((trigger) => trigger.kill());

And stopping locoscroll with locoScroll.stop() but it's like it's all initiated multiple times or something.
 

Link to post
Share on other sites

It's mostly visible when you use the header navigation while on the bottom of the page 

Link to post
Share on other sites

Thanks for the demo. I had similar issues using just Locomotive Scroll when the scroll position was not 0 upon page load. Perhaps you could try setting the scroll position to 0 explicitly when the new page is loaded? In plain JS it'd be this:

document.documentElement.scrollTop = 0;

Alternatively you could try setting the scroll position with Locomotive Scroll or with ScrollTrigger. 

Link to post
Share on other sites

Hmm I tried both setting the scrollTop to 0 and using locomotive scroll to scroll to 0 and neither work unfortunately, can't figure out how to use the .scroll() function with a scrollerproxy though. 

Link to post
Share on other sites
  • 4 weeks later...

Hi everyone. I'm having some struggles with nuxt and ScrollTrigger.scrollerProxy also.

 

I'm using ASScroll (https://github.com/ashthornton/asscroll) as a mixin (also tried as a plugin, no luck), and I need to enable ASSCrolll on mount() and disable the same instance on beforeDestroy().

So, I've done two methods, one 'startSmoothScroll()' to start and another 'destroySmoothScroll()' that are called on the mount() and beforeDestroy() of the respective pages I need. 

However, I always get an error:

Cannot read property 'smoothScrollPos' of undefined
    at ScrollTrigger.scrollTop [as scroll] (smoothScrollGsapMixin.js?ee2c:59)
    at _updateAll (ScrollTrigger.js?1dac:448)

 

It seems that, inside ScrollTrigger.scrollerProxy() scrollTop(value) I cannot use this.asScrollInstance.smoothScrollPos, although is should be the same instance.

 

If I do something like const asscrollSmoothScrollPos = this.asScrollInstance.smoothScrollPos, and then replace it inside ScrollTrigger.scrollerProxy , the error is gone, but then ScrollTrigger stops working after the first page load.

 

If instead of this.asScrollInstance = new AsScroll(asscrollOptions) I use a direct variable like const asScrollInstance = new AsScroll(asscrollOptions) and replace the code with it, all works fine, but then I can't destroy the asScrollInstance that was created...

 

So I'm kind of stuck in a loop?

 

Here's the code I'm using. Any help would be appreciated, as I'm struggling with this for some days already.

 

methods: {
    startSmoothScroll() {
      // Register ScrollTrigger
      gsap.registerPlugin(ScrollTrigger)

      // Define options for AsScroll
      const asscrollOptions = {
        // disableRaf: true,
        // ease: 0.075 // default
        // customScrollbar: true
      }

      // Initialize AsScroll instance
      this.asScrollInstance = new AsScroll(asscrollOptions)


      // ScrollTrigger Defaults
      ScrollTrigger.defaults({
        scroller: '.innerscroller',
        markers: true
      })




      // each time asScroll updates, tell ScrollTrigger to update too (sync positioning)
      this.asScrollInstance.on('scroll', ScrollTrigger.update)

      // Define ScrollTrigger scrollerProxy, to delegate the position of scrolling to AsScroll

      ScrollTrigger.scrollerProxy(
        '.innerscroller', {
          id: 'SmoothScrollMixin',
          scrollTop(value) {
            return arguments.length ?
              this.asScrollInstance.scrollTo(value) :
              -(this.asScrollInstance.smoothScrollPos) // ¬ę¬ę¬ę THE ERROR HAPPENS HERE
          }, // we don't have to define a scrollLeft because we're only scrolling vertically.
          getBoundingClientRect() {
            return {
              top: 0,
              left: 0,
              width: window.innerWidth,
              height: window.innerHeight
            }
          }
        }
      )

      this.asScrollInstance.on('raf', ScrollTrigger.update)
      ScrollTrigger.addEventListener('refresh', () => {
        console.log('ScrollTrigger Refresh event. Starting asScroll.onResize');
        this.asScrollInstance.onResize()
      })

      console.log('ScrollTrigger.refresh() ');
      ScrollTrigger.refresh()

      this.asScrollInstance.enable(
        false,
        true,
        document.querySelector('.innerscroller')
      );
      console.log('asScrollinstance enabled');


      ScrollTrigger.refresh()

    },

    destroySmoothScroll() {
      if (this.asScrollInstance) {
        console.log('asScrollinstance destroyed');
        this.asScrollInstance.disable();
      }
    }
  }

 

Edited by BMateus
Highlighted the error position on the code
Link to post
Share on other sites

Hey BMateus. this is not what you think it is. Inside of the scrollTop function, this refers to the ScrollTrigger. You can console.log it to check what it is.

 

I think you should be able to use a variable to save what this is and refer to that instead inside your scrollTop function. 

  • Like 1
  • Thanks 1
Link to post
Share on other sites

Hi @ZachSaucier  and thanks for your help.

 

Duh! Of course, makes total sense, after your input! 

I've changed it to the code below.

 


      const asScrollInstance = this.asScrollInstance

      ScrollTrigger.scrollerProxy(
        '.innerscroller', {
          id: 'SmoothScrollMixin',
          scrollTop(value) {
            return arguments.length ?
              asScrollInstance.scrollTo(value) :
              -asScrollInstance.smoothScrollPos
          }, // we don't have to define a scrollLeft because we're only scrolling vertically.
          getBoundingClientRect() {
            return {
              top: 0,
              left: 0,
              width: window.innerWidth,
              height: window.innerHeight
            }
          }
        }
      )

 

Still having issues with it, though, as it seems the mounting and destruction are not really working properly.

As I navigate through pages, the performance is getting worse and worse, until AsScroll stops working completely.

But ScrollTrigger still works, at least, so its something surely unrelated to GSAP :)

 

Just for reference if someone reads this, the performance issue is also solved. I was creating new instances (new AsScroll(options)) in every call of the startSmoothScroll() method.

Placing it before, outside the method, is the correct thing to do, as you only need it once.  Then the startSmoothScroll() method just enables and disables it as needed.

 

Thank you again for your help, @ZachSaucier! Was really stuck until you input :)
GSAP rocks!

Edited by BMateus
Corrected the issue.
  • Like 1
Link to post
Share on other sites
  • 3 weeks later...

I have the same problem.
I'm using Nuxt, Smooth-scrollbar and gsap's scrollTrigger to create a product, but when I do a page transition with nuxt-link, the page doesn't work as I expected.

I think it's a problem with SSR and routing, so I can't make a demo either. Someone please help meūüė≠.

 

The code below is the configuration code for Smooth-scrollbar and ScrollTrigger.scrollerProxy, which are common to both the home page and the destination page.
 

 

<script>
import Scrollbar from 'smooth-scrollbar'
import gsap from "gsap"
import { ScrollTrigger } from "gsap/dist/ScrollTrigger.min.js"


export default {
  data() {
    return {
      bodyScrollBar: null,
    }
  },
  mounted() {
    gsap.registerPlugin(ScrollTrigger)
    this.scrollbarOnTrigger()
    this.animation()
  },

  beforeDestroy() {
    console.log('destroy')
    this.bodyScrollBar.destroy(document.body)
    this.bodyScrollBar = null
  },
  methods: {
    scrollbarOnTrigger() {
      this.bodyScrollBar = Scrollbar.init(document.body, {
        damping: 0.1,
        alwaysShowTracks: true,
        delegateTo: document,
      })
      const bodyScrollBar = this.bodyScrollBar

      bodyScrollBar.setPosition(0, 0)
      bodyScrollBar.track.xAxis.element.remove()

      ScrollTrigger.scrollerProxy('body', {
        scrollTop(value) {
          if (arguments.length) {
            bodyScrollBar.scrollTop = value
          }
          return bodyScrollBar.scrollTop
        },
        scrollLeft(value) {
          if (arguments.length) {
            bodyScrollBar.scrollLeft = value
          }
          return bodyScrollBar.scrollLeft
        },
      })
    },
  },
}
</script>

 

Link to post
Share on other sites
  • 3 months later...

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.

√ó