Search code examples
javascriptpropertiesundefined

Converting plain script to js class to allow having multiple instances fails reading props (that exist)


Lets say we had this (working) script, but as it uses querySelector only works for the first instance:

if ( $( '.marquee' ).length ) {
    const marquee = document.querySelector( '.marquee__content' );
    let lerpScroll = 0;
    let lastScrollPos = 0;
    const maxSpeed = 20;

    function getScrollPercent() {
        let h = document.documentElement, 
            b = document.body,
            st = 'scrollTop',
            sh = 'scrollHeight';
        return (h[st]||b[st]) / ((h[sh]||b[sh]) - h.clientHeight) * 100;
    }

    function isInViewport() {
        const rect = marquee.getBoundingClientRect();
        const vOffset = marquee.offsetHeight;

        const percent = getScrollPercent() * -1;
        lerpScroll += (percent - lerpScroll) * 0.06;
        marquee.style.transform = `translateX(${lerpScroll * 4}%)`;

        // skew
        let scrollPos = window.scrollY;
        const container = marquee.querySelector( '.marquee__inner' );
        getDirection = scrollPos - lastScrollPos;
        lastScrollPos = scrollPos;

        if (getDirection > maxSpeed) {
            getDirection = maxSpeed;
        }
        if (getDirection < -maxSpeed) {
            getDirection = -maxSpeed;
        }

        container.style.transform = getDirection;

        window.requestAnimationFrame(isInViewport);
    }

    isInViewport();
}

So I tried converting it to a class (and dump the jquery code):

class Marquee {
    constructor(el) {
      this.marquee = el;
      this.maxSpeed = 20;
      this.lerpScroll = 0;
      this.lastScrollPos = 0;
    }

    getScrollPercent() {
        let h = document.documentElement, 
            b = document.body,
            st = 'scrollTop',
            sh = 'scrollHeight';
        return (h[st]||b[st]) / ((h[sh]||b[sh]) - h.clientHeight) * 100;
    }

    isInViewport() {
        if (this.marquee) { /* <----- this is line 24 */
            console.log(this.marquee.offsetHeight);
            const vOffset = this.marquee.offsetHeight;

            const percent = this.getScrollPercent() * -1;
            this.lerpScroll += (percent - this.lerpScroll) * 0.06;
            this.marquee.style.transform = `translateX(${this.lerpScroll * 4}%)`;

            // skew
            let scrollPos = window.scrollY;
            const container = this.marquee.querySelector( '.marquee__inner' );
            let getDirection = scrollPos - this.lastScrollPos;
            this.lastScrollPos = scrollPos;

            if (getDirection > this.maxSpeed) {
                getDirection = this.maxSpeed;
            }
            if (getDirection < -this.maxSpeed) {
                getDirection = -this.maxSpeed;
            }

            container.style.transform = getDirection;

            window.requestAnimationFrame(this.isInViewport);
        }
    }

    init () {
        this.isInViewport();
    }
};

And using it like this:

if ( document.querySelectorAll( '.marquee' ).length ) {
    document.querySelectorAll( '.marquee__content' ).forEach((element) => {
        const m = new Marquee(element);
        m.init();
    })  
}

But it fires this error: enter image description here

Even that it printed the value enter image description here

Any idea what i'm doing wrong? here's a fiddle https://jsfiddle.net/bkj3ry74/2/

So the problem is that it complains about it can't read a prop that i can see the console.log

scripts.js?ver=1.0.0:24 Uncaught TypeError: Cannot read properties of undefined (reading 'marquee')
    at isInViewport

Solution

  • on this line

    window.requestAnimationFrame(this.isInViewport);
    

    you are passing the function from the class as a callback function for .requestAnimationFrame which is fine and dandy, however because you are passing it as a function, in this context it is getting a new this inside the function. Maybe using an anonymous arrow function instead will keep the context of this i.e.

    window.requestAnimationFrame(() => this.isInViewport());