Skip to content

Stimulus controller: event listened multiple times; how do I remove event listeners and retain the Context?

I’ve got the following controller on my HTML page:

...
<div data-controller="parent">
    <div data-target="parent.myDiv">
        <div data-controller="child">
            <span data-target="child.mySpan"></span>
        </div>
    </div>
</div>
...

This child controller is mapped to the following child_controller.js class:

export default class {
    static targets = ["mySpan"];

    connect() {
        document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
    }

    handleMyEvent(event) {
        console.log(event);
        this.mySpanTarget; // Manipulate the span. No problem.
    }
}

As you can see, there is an event listener on the connect() of the Stimulus controller, and when it detects that the event was fired up, it logs the event and manipulates the span target.

The problem arises when I replace the contents of the target myDiv from my parent_controller.js:

...
let childControllerHTML = "<div data-controller="child">...</div>"

myDivTarget.innerHTML= childControllerHTML;
...

Now that the myEvent gets fired up, the event listener picks it not once, but twice (because the same event got logged twice). With every subsequent replacement of the child HTML, the event gets logged one more time than it did before.

I know that one can make use of document.removeEventListener to prevent the old controller from still listening to the events:

export default class {
    static targets = ["mySpan"];

    connect() {
        this.myEventListener = document.addEventListener("myEvent", (event) => this.handleMyEvent(event));
    }

    disconnect() {
        document.removeEventListener("myEvent", this.myEventListener);
    }

    handleMyEvent(event) {
        console.log(event);
        this.mySpanTarget; // FAILS. Can't find span.
    }
}

But doing it like this makes the handleMyEvent method lose the context as it no longer finds the mySpanTarget under this.

How can I remove the listener from the child controller to which I already got no access as it is no longer in the DOM, while retaining the context?

Answer

I found the answer on StimulusJS’s Discourse page.

One has to make use of the bind method when initializing the controller:

export default class {
    static targets = ["mySpan"];

    initialize() {
        this.boundHandleMyEvent = this.handleMyEvent.bind(this);
    }

    connect() {
        document.addEventListener("myEvent", this.boundHandleMyEvent);
    }

    disconnect() {
        document.removeEventListener("myEvent", this.boundHandleMyEvent);
    }

    handleMyEvent(event) {
        console.log(event);
        this.mySpanTarget; // Manipulate the span. No problem.
    }
    ...
}

Now, the event is only listened once, and the context is not lost inside the handleMyEvent method.