Unable to delete event listeners from the browser's Document Object Model

Issue at hand involves two methods; one for initializing event listeners and the other for deleting them. Upon deletion, successful messages in the console confirm removal from the component's listener array. However, post-deletion, interactions with the UI reveal that the listeners were not completely deleted. Curiously, DevTools' "Event Listeners" tab also displays their presence. Manually deleting via DevTools rectifies this issue. Suspecting a context loss in the removeDOMListeners method despite the cleared array. Seeking advice on this matter.

Output following removal of event listeners:

Listeners before removal: 1 Listeners after removal: 0

DOMListener.ts:

export abstract class DOMListener<T extends DOMElement> {
    protected readonly $root: T;
    protected readonly listeners: string[];

    protected constructor($root: T, listeners: string[] = []) {
        if (!$root) {
            throw new Error(`No $root provided for DomListener!`);
        }
        this.$root = $root;
        this.listeners = listeners;
    }

    [key: string]: any;
    public initDOMListener(): void {
        this.listeners.forEach((listener) => {
            const method = prefixSetter('on', listener);
            if (!this[method]) {
                const name = this.name || '';
                throw new Error(`Method ${method} is not implemented in ${name} Component`);
            }
            this[method] = this[method].bind(this);

            if (this.$root && this.$root.on) {
                this.$root.on(listener, this[method]);
                console.log(listener)
            }
        });
    }


    public removeDOMListener(): void {
        console.log('Listeners before removal:', this.listeners.length);
        this.listeners.forEach((listener) => {
            const method = prefixSetter('on', listener);
            const listenerFunction = this[method].bind(this);
            if (this.$root && this.$root.off) {
                this.$root.off(listener, listenerFunction);
            }
        });
        this.listeners.length = 0;
        console.log('Listeners after removal:', this.listeners.length);
    }
}

DOM.ts:

export class DOM {
    public $el: HTMLElement;

    constructor(selector: string | HTMLElement) {
        this.$el = typeof selector === 'string'
            ? document.querySelector(selector)!
            : selector;

    }

    public on(eventType: string, callback: EventListenerOrEventListenerObject) {
        this.$el.addEventListener(eventType, callback);
    }

    public off(eventType: string, callback: EventListenerOrEventListenerObject) {
        this.$el.removeEventListener(eventType, callback);
    }

    public append(node: DOM | HTMLElement): DOM {
        if (node instanceof DOM) {
            node = node.$el
        }

        this.$el.append(node)

        return this
    }
}

export function $(selector: string | HTMLElement): DOM {
    const el = typeof selector === 'string'
        ? document.querySelector(selector)
        : selector

    if (!el) {
        throw new Error(`Element not found: ${selector}`)
    }

    return new DOM(el as HTMLElement)
}



$.create = (tagName: string, classes = ''): DOM => {
    const el = document.createElement(tagName)
    if (classes) {
        el.classList.add(classes)
    }
    return $(el)
};

Component.ts:

export class Header extends BaseComponent {
    static className = 'header'

    constructor($root: HTMLElement) {
        super($root, {
            name: 'Header',
            listeners: ['input']
        });
        console.log($root)
    }

    toHTML(): string {
        return `
            <input type="text" class="header-input" value="Sheet name" />
            <div>
                <div class="header-button">
                    <i class="material-icons">delete</i>
                </div>
                <div class="header-button">
                    <i class="material-icons">exit_to_app</i>
                </div>
            </div>
        `;
    }

    public onInput(e: Event): void {
        const $target = e.target as HTMLInputElement;
        if ($target.closest('.header-input')) {
            console.log('Header input val:' + $target.value);
        }
    }
}

BaseComponent.ts:

export class BaseComponent extends DOMListener<any> {
    name: string;

    constructor($root: string | HTMLElement, options: {name?: string; listeners?: string[]} = {}) {
        if (!$root) {
            throw new Error('No $root provided for BaseComponent');
        }
        super($root, options.listeners);
        this.name = options.name || '';
    }

    toHTML(): string {
        return '';
    }

    init(): void {
        this.initDOMListener()

    }

    destroy(): void {
        this.removeDOMListener()
    }
}

Answer №1

In order to remove an event listener, it is crucial to pass the exact same function that was originally bound to it.

When you use the .bind method multiple times, it creates different functions each time.

Therefore, it is important to store references to your bound functions somewhere accessible.

class X {
    constructor() {
        this.method2 = this.method2.bind(this)
    }
    method1(e: Event) { }
    method2(e: Event) { }
    method3(e: Event) { }
    test() {
        // not keeping a reference:
        let listener1a = this.method1.bind(this)
        let listener1b = this.method1.bind(this)
        console.log(`binds are ${listener1a === listener1b ? 'equal' : 'not equal'}`)
        
        // making references on instantiation:
        let listener2a = this.method2
        let listener2b = this.method2
        console.log(`constructor-binds are ${listener2a === listener2b ? 'equal' : 'not equal'}`)

        // making references dynamically: 
        let listener3a = this.bind('method3')
        let listener3b = this.bind('method3')
        console.log(`override-binds are ${listener3a === listener3b ? 'equal' : 'not equal'}`)
    }

    bind(key: keyof this) {
        if (this.constructor.prototype[key] !== this[key]) {
            return this[key]
        } else {
            return this[key] = this[key].bind(this)
        }
    }
}

new X().test()

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I am encountering a problem with my Material UI react-swipeable-views while using TypeScript

It seems that there is a mismatch in the version of some components. import * as React from "react"; import { useTheme } from "@mui/material/styles"; import Box from "@mui/material/Box"; import MobileStepper from "@mui/ma ...

Issue with TypeScript not detecting exported Firebase Cloud Functions

Dealing With Firebase Cloud Functions Organization I am managing a large number of Firebase Cloud Functions, and in order to keep the code well-structured, I have divided them into separate files for each function category (such as userFunctions, adminFun ...

Encountering "No overload matches this call" error in Typescript while working with fetched data and material-ui

Before attempting to create a dropdown menu with an array retrieved using useSWR, I first practiced creating one with a hardcoded array. I used this for the initial practice: https://codesandbox.io/s/76k0ft?file=/demo.tsx:1819-2045 Instead of using a hard ...

Navigating the terrain of multiple checkboxes in React and gathering all the checked boxes

I am currently developing a filter component that allows for the selection of multiple checkboxes. My goal is to toggle the state of the checkboxes, store the checked ones in an array, and remove any unchecked checkboxes from the array. Despite my attemp ...

"Encountering a module not found issue while trying to

Attempting to test out 3 node modules locally by updating their source locations in the package.json files. The modules in question are sdk, ng-widget-lib, and frontend. ng-widget-lib relies on sdk, while frontend depends on ng-widget-lib. To locally build ...

Strategies for extracting the type argument from a nested property and transforming it into a different value

I’m struggling to find the right way to frame my question, so I’ll provide an example of what I need help with. Let's assume I have the following object: const obj = { one: 'some string', two: new Set<string>(), }; Now, I wan ...

Retrieve a list of class names associated with a Playwright element

Can anyone suggest the best method to retrieve an array of all class names for an element in Playwright using TypeScript? I've searched for an API but couldn't find one, so I ended up creating the following solution: export const getClassNames = ...

Dealing with server-side errors while utilizing react-query and formik

This login page utilizes formik and I am encountering some issues: const handleLogin = () => { const login = useLoginMutation(); return ( <div> <Formik initialValues={{ email: "", password: "" }} ...

Exploring the potential of AssemblyScript in creating immersive WebXR

I have been exploring three.js and webXR for some time now, and I wanted to incorporate it into assembly script. While I know how to make webXR work in TypeScript, I encounter an error when trying to use it in assembly script with the import statement. Her ...

The swipe motion will be executed two times

By pressing the Circle button, the Box will shift to the right and vanish from view. Further details (FW/tool version, etc.) react scss Typescript framer-motion import "./style.scss"; import React, { FunctionComponent, useState } from &q ...

Angular 8: Issue with PatchValue in Conjunction with ChangeDetector and UpdateValue

I am puzzled by the fact that PatchValue does not seem to work properly with FormBuilder. While it shows data when retrieving the value, it fails to set it in the FormBuilder. Does anyone have an idea why this might be happening? I am utilizing UpdateValue ...

Having trouble resolving 'primeng/components/utils/ObjectUtils'?

I recently upgraded my project from Angular 4 to Angular 6 and everything was running smoothly on localhost. However, during the AOT-build process, I encountered the following error: ERROR in ./aot/app/home/accountant/customercost-form.component.ngfactory. ...

Setting up Mailgun with TypeScript on Firebase Cloud Functions

Currently, I am working on a Cloud Function within Firebase to integrate with Mailgun for sending emails, following the guidelines provided in the Mailgun documentation. My challenge lies in implementing this functionality using TypeScript, as I have been ...

Why is it that TypeScript struggles to maintain accurate type information within array functions such as map or find?

Within the if block in this scenario, the p property obtains the type of an object. However, inside the arrow function, it can be either an object or undefined. const o: { p?: { sp?: string } } = { p: {} } if (o.p) { const b = ['a'].map(x => ...

When running `aws-cdk yarn synth -o /tmp/artifacts`, an error is thrown stating "ENOENT: no such file or directory, open '/tmp/artifacts/manifest.json'"

Starting a new aws-cdk project with the structure outlined below src └── cdk ├── config ├── index.ts ├── pipeline.ts └── stacks node_modules cdk.json package.json The package.json file looks like this: " ...

Differentiating between mouseenter and tap events: What's the key?

When a mouseenter event is present, touch-enabled devices will activate this event when the user taps on the element. Is there a way to differentiate between an actual physical mouse entering and a simulated tap (which resembles a mouse enter)? ...

Creating animations for freshly added content on the DOM

In the development of a web application, I am focusing on the front end aspect. My approach involves creating content based on JSON objects received from the back end. This content is then encapsulated in HTML and added to the DOM using jQuery's appen ...

Merging multiple observables with RxJs forkJoin

UPDATE : I'm currently facing a challenging issue that I can't seem to resolve. Within my code, there is a list of objects where I need to execute 3 requests sequentially for each object, but these requests can run in parallel for different obje ...

typescript: declaring types in a separate JavaScript file

Imagine you have a JavaScript library that exports some types for use (let's call it js1.js). You also have some TypeScript code sitting in a <script type="module"> tag that you want to use these types with (let's say ts1.ts). To make this ...

How to properly display an Angular Template expression in an Angular HTML Component Template without any issues?

When writing documentation within an Angular App, is there a way to prevent code from executing and instead display it as regular text? {{ date | date :'short'}} Many sources suggest using a span element to achieve this: <span class="pun"&g ...