Nasa
Nasa

Reputation: 419

CustomEvent listener callback is firing twice?

I've created a custom element:

const templ = document.createElement('template');
templ.innerHTML = `
<span><slot></slot></span>
`;
class SlideButton extends HTMLElement {

    constructor() {
        super();

        // Attach a shadow root to the element.
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.appendChild(tmpl.content.cloneNode(true));
        this.span = shadowRoot.querySelector('span');

        this.triggerEvent = new CustomEvent("trigger", {
            bubbles: false,
            cancelable: false,
        });

        this.initMouseEvents();
    }

    initMouseEvents() {
        this.span.addEventListener('mousedown', (e) => {

            //Watch and calculate slide amount..
            this.addEventListener('mousemove', this.slide, false);

        });

        //When button is released...
        document.addEventListener('mouseup', handleMouseUp = (e) => {

            this.removeEventListener('mousemove', this.slide, false);
            document.removeEventListener('mouseup', handleMouseUp);

            //If slided enough, dispatch event...
            if (Math.abs(this.slideAmount) > (this.maxSlide * 0.75)) {
                console.log('firing event');
                this.dispatchEvent(this.triggerEvent);
            }
            //Reset button to normal state...

        }, false);
    }
}

Somewhere else in my code..

class SpotLightModal {

    //Constructor..
    //code..
    //code..

    init() {
        this.actions.querySelector('slide-button[type="den"]').addEventListener('trigger', e => {
            console.log(e);
            console.log('den');
            //Do stuff..
        });
    }

    //code...
    //code...

}

Everything works as expected except that the callback in the event listener is runs twice and the output is:

firing event
CustomEvent {...}
den
CustomEvent {...}
den

Both e.stopPropagation() and e.preventDefault() have no effect and trying to use them did nothing..

I've edited to include this.span and also moved the "mouseup" event listener outside the "mousedown" event listener but that didn't work, infact when logging this, now, it gives another different element (of the same kind, <slide-button>, the first on the page), the "mouseover" listener doesn't get removed, and the event isn't fired.

Am I doing something wrong in here? or what am I missing exactly?

Thanks in advance..

Upvotes: 4

Views: 5638

Answers (4)

kyw
kyw

Reputation: 7555

In my case, I was using event bus to dispatch custom event which will trigger a callback function in which the double events happened.

Initially, the custom event listener was in the constructor()

constructor() {
    EventBus.addEventListener('someEvent', this.doSomething);
}

And I stopped seeing double events once I moved that line to connectedCallback():

connectedCallback() {
   EventBus.addEventListener('someEvent', this.doSomething);
}

Upvotes: 1

fallingpizza11
fallingpizza11

Reputation: 156

If anyone else is having a similar problem try using:

event.stopImmediatePropagation() into your callback function like so

window.addEventListener('message', (event) => {
  event.stopImmediatePropagation();
  console.log('detail:', event.detail);
})

In my case this seemed to do the trick, but it's definitely super hacky and would recommend trying to find out the root cause of your issue if it needs to be super reliable.

https://developer.mozilla.org/en-US/docs/Web/API/Event/stopImmediatePropagation

Upvotes: 14

Michael David Gieson
Michael David Gieson

Reputation: 11

There's a "bubbles" option that you can set to false, which should eliminate the mouse-down/up bubbling phases:

var evt = new CustomEvent(name, {
    detail : data,
    bubbles: true, // <-- try setting to false
    cancelable: true
});

document.dispatchEvent(evt);

Upvotes: 0

Emiel Zuurbier
Emiel Zuurbier

Reputation: 20954

The problem lies in the nesting of your code.

First you add the first mousedown event listener which will trigger on when the mouse is clicked and not released.

this.span.addEventListener('mousedown', (e) => { ...

Then inside of your mousedown event listener you listen for the mouseup event on the document.

document.addEventListener('mouseup', handleMouseUp = (e) => { ...

So now, whenever you click both the mousedown and the mouseup will be triggered. Even when you remove the event listener inside the mouseup event listener. The code inside there is already executing.
That is what causes your code to fire the CustomEvent twice.

Prevent the nesting of event listeners, except when one would rely on the other. Like in the mousedown event listener where you need to add the mousemove event listener. Now instead of nesting add another event listener that listens to mouseup adjecent to the mousedown event listener. Remove the mousemove event there. Example below:

class SlideButton extends HTMLElement {

    constructor() {
        super();

        // Attach a shadow root to the element.
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.appendChild(tmpl.content.cloneNode(true));
        this.span = shadowRoot.querySelector('span');

        this.triggerEvent = new CustomEvent("trigger", {
            bubbles: false,
            cancelable: false,
        });

    }

    connectedCallback() {

        // It's preferred to set the event listeners in the connectedCallback method.
        // This method is called whenever the current element is connected to the document.
        this.initMouseEvents();

    }

    initMouseEvents() {

        // Bind slider-button as this context to the slide method.
        // In case you use the this keyword inside of the slide method
        // you would need to do this to keep using the methods of this class.
        const slide = this.slide.bind(this);

        // Create a toggle to indicate that the slide-button has been clicked so that ONLY then the event listeners will be added.
        let targetIsClicked = false;

        // Mouse is clicked, mousemove added.
        document.addEventListener('mousedown', (e) => {

            // Check if this button has been clicked.
            const slideButton = e.target.closest('slide-button');
            if (slideButton === this) {

                // Toggle true and add mousemove event.
                targetIsClicked = true;
                document.addEventListener('mousemove', slide, false);
            }

        });

        // Mouse is released, remove mousemove and fire event.
        document.addEventListener('mouseup', (e) => {

            // Only do something if this button has been clicked in the mousedown event.
            if (targetIsClicked === true) {
                document.removeEventListener('mousemove', slide, false);

                // Reset toggle value.
                targetIsClicked = false;

                //If slided enough, dispatch event...
                if (Math.abs(this.slideAmount) > (this.maxSlide * 0.75)) {
                    console.log('firing event');
                    this.dispatchEvent(this.triggerEvent);
                }
                //Reset button to normal state...

            }

        });

    }
}

Edit

I've changed the event listener's target to document. You explained that you want the mousemove and mouseup events to work when triggering them outside the button.

In the mousedown I check if the current target that has been clicked is in fact this button. And if so the targetIsClicked value will toggled to indicate that the correct button has been clicked.

In the mouseup event first check if the targetIsClicked is true. Meaning you clicked this button and execute the rest of your code.

Although, if you'd have multiple <slide-button> elements there would be multiple mousedown, mousemove and mouseup listeners attached to the document, which could create some weird outcomes.

Note

I've moved the this.initMouseEvents() function call to the connectedCallback() method. It is good practice to do this because now you are interacting with the lifecycle of the custom element. Add the event listeners in connectedCallback() when the element is created and remove event listeners in disconnectedCallback() when the element is removed. The element itself triggers these methods, so you don't have to call them.

Upvotes: 0

Related Questions