0

I have built the following loading spinner that counts a given number of seconds down and then executes some given callbacks. I did this for learning purposes and because I did not want to use another dependency.

When I click the circle, it should stop counting down. But the way it is implemented currently, it won't work. I if I add a break point to suspendCounter() I can see that we jump into it, and it does it thing but the next time the interval counts down the value is ignored.

I believe this is because my class-context gets "copied" when I execute setInterval() the first time, and thous any later changes are not passed, and it does not know about them.

But what can I do to make it work? I can set a new property to global and use that (which works well, btw) but this is surely frowned upon.

/**
 * class for creating a progress circle that is counting down
 * some callbacks can be passed to the constructor
 */
export class IntervalProgressCircle {
    #intervalID;

    /**
     * Creates the progress circle and mounts it to the parent element
     * @param {HTMLDivElement} parentEl - the parent element to mount the progress circle to
     * @param {number} seconds - the number of seconds the progress circle should take to complete
     * @param {function[]} callbacks - an array of functions to call when the progress circle completes
     */
    constructor( parentEl, seconds, ...callbacks ) {

        this.seconds = seconds;
        this.secondsElapsed = 0;
        this.callbacks = callbacks;
        this.pauseCounter = false;
        this.parentEl = parentEl;

        this.svgWrap = document.createElementNS(
            "http://www.w3.org/2000/svg", 'svg' );
        this.circle = document.createElementNS(
            "http://www.w3.org/2000/svg", 'circle' );

        this.#setStyle();

        this.radius = 32;
        this.circumference = this.radius * 2 * Math.PI;

        this.circle.style.strokeDasharray = `${ this.circumference } ${ this.circumference }`;
        this.circle.style.strokeDashoffset = `${ this.circumference }`;

        this.svgWrap.appendChild( this.circle );
        this.parentEl.appendChild( this.svgWrap );
    }

    setProgress( percent ) {
        this.circle.style.strokeDashoffset = ( this.circumference - percent / 100 * this.circumference ).toString();
    }

    suspendCounter() {
        this.pauseCounter = !this.pauseCounter;
    }

    startInterval() {
        /**
         * interval for reloading table data
         */
        if ( this.#intervalID ) {
            clearInterval( this.#intervalID );
        }
        this.#intervalID = setInterval( async () => {
            if ( this.pauseCounter === true ) return;
            if ( this.secondsElapsed > this.seconds ) {
                this.secondsElapsed = 1;
                await this.callbacks.forEach( cb => cb() );
                return;
            }
            this.setProgress( ( this.secondsElapsed / this.seconds ) * 100 );
            this.secondsElapsed += 1;
        }, 1000 );
    }

    #setStyle() {

        this.svgWrap.setAttributeNS( null, 'id', 'progress-ring' );
        this.svgWrap.setAttributeNS( null, 'class', 'progress-ring' );
        this.svgWrap.setAttributeNS( null, 'width', '5em' );
        this.svgWrap.setAttributeNS( null, 'height', '5em' );

        this.circle.setAttributeNS( null, 'stroke', 'white' );
        this.circle.setAttributeNS( null, "stroke-width", "0.4em" )
        this.circle.setAttributeNS( null, "fill", "transparent" )
        this.circle.setAttributeNS( null, "cx", "2.5em" )
        this.circle.setAttributeNS( null, "cy", "2.5em" )
        this.circle.setAttributeNS( null, "r", "2em" )
        this.circle.setAttributeNS(
            null, "style",
            "stroke-dasharray: 201.062px, " +
            "201.062px; stroke-dashoffset: 174.254px;" +
            "transition: 1.45s stroke-dashoffset;" +
            "transform: rotate(-90deg);" +
            "transform-origin: 50% 50%;"
        )
        this.circle.onclick = this.suspendCounter;
    }
}

SOLUTION

All that needed to change was:

this.circle.onclick = this.suspendCounter;

to

this.circle.onclick = () => this.suspendCounter();

see How to access the correct `this` inside a callback

Tim
  • 15
  • 9
  • Objects are never copied automatically in JavaScript. – Barmar Oct 05 '22 at 19:46
  • Please post a [mre]. I suspect the problem is that you're making multiple instances, and calling `suspendCounter()` on a different one than you started. – Barmar Oct 05 '22 at 19:49
  • 2
    `this.circle.onclick = this.suspendCounter` doesn't work, you'll need to bind the `suspendCounter` method to your instance. The `setInterval` is actually working fine, you've used an arrow function there (which is unnecessarily `async` - the `await` is pointless since `forEach` returns `undefined`, never a promise - but still works). – Bergi Oct 05 '22 at 20:06
  • @Barmar go ahead and paste it into you browser. This is my example. and its not even 100loc – Tim Oct 05 '22 at 20:30
  • @Bergi thanks! I am not sure if what you describe is what I found to be the solution but it definitely was that line which did not work! – Tim Oct 05 '22 at 20:31
  • 1
    @Tim Yes, that's what I meant – Bergi Oct 05 '22 at 20:47

0 Answers0