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

Combining types with additional features

Is it possible to configure the TypeScript compiler to generate an error when a function is called with an argument that can belong to both cases in a union type? For example: interface Name { name: string } interface Email { email: string } type ...

It is not necessary to specify a generic type on a Typescript class

When working with Typescript and default compiler options, there are strict rules in place regarding null values and uninitialized class attributes in constructors. However, with generics, it is possible to define a generic type for a class and create a ne ...

Enclose the type definition for a function from a third-party library

I prefer to utilize Typescript for ensuring immutability in my code. Unfortunately, many libraries do not type their exported function parameters as Readonly or DeepReadonly, even if they are not meant to be mutated. This commonly causes issues because a ...

Utilize the prototype feature from a versatile source

Can a class with a generic like class Foo<A> {} access A's prototype or use a typeguard on A, or perform any kind of logic based solely on A's type - without being given the class, interface, or instance to Foo's constructor (e.g. when ...

Having trouble with my React component timer not functioning properly

How can I utilize the Header Component as a Clock timer for my webpage to update every second? Despite searching on Google, I couldn't find examples that match my requirements. Why is the tick() function not functioning properly even though there are ...

The 'setState' property is not found on the 'Window' type

I am encountering an issue with the code snippet in my index.tsx file let state = {}; window.setState = (changes: any) => { state = Object.assign({}, state, changes); ReactDOM.render(<App {...state} />, document.getElementById("root")); ...

Finding the label that is linked to the current DIV or textarea by its "for" property

I am working on a project that involves two elements - a textarea and a div. Each element has a label attached to it using the "for" attribute in HTML. <textarea id="txta_1" class="txta" cols="40" rows="3" onkeyup ...

Functions designed to facilitate communication between systems

There is an interface that is heavily used in the project and there is some recurring logic within it. I feel like the current solution is not efficient and I am looking for a way to encapsulate this logic. Current code: interface Person { status: Sta ...

Manipulating a cloned object using jQuery

var clonedForm = $('form').clone(); //clonedForm.find('input[type="hidden"]').remove(); clonedForm.find('select').each(function(){ $($(this).val()).insertAfter($(this)); $(this).remove(); }); Atte ...

The type is lacking the property onAuxClickCapture and onAuxClick

When utilizing forwardRef from React, an unexpected type error occurs: The type '{ children: ReactNode; }' is lacking the properties specified in 'Pick<ILinkProps, "className" | "children" | "accept" | "acceptCharset" | "action" | ... 34 ...

struggling with configuring dependency injection in NestJS and TypeORM

Struggling with integrating nestjs and typeorm for a simple CRUD application, specifically facing issues with dependency injection. Attempting to modularize the database setup code and import it. Encountering this error message: [ExceptionHandler] Nest ...

Experimenting with async generator using Jest

It has become clear that I am struggling with the functionality of this code, especially when it comes to testing with Jest. Despite my efforts to use an await...of loop, I am not getting any output. The file path provided to the generator is correct and I ...

Tips for transferring the value of a text box between components bidirectionally in Angular 8

Let's create a scenario where we have two components: login and home. The goal is to capture the value entered in the text box of the login component and pass it to the text box in the home component when the "proceed" button in the login component is ...

The ngx-treeview is displaying an inaccurate tree structure. Can you pinpoint where the issue lies?

I have structured my JSON data following the format used in ngx-treeview. Here is the JSON file I am working with: [ { "internalDisabled": false, "internalChecked": false, "internalCollapsed": false, "text": "JOURNEY", "value": 1 } ...

Tips for directing your attention to an input field in Angular

I'm struggling to find a simple solution for setting focus programmatically in Angular. The closest answer I found on Stack Overflow is about dynamically created FormControl, but it seems more complex than what I need. My situation is straightforward ...

Display identical text using JavaScript filter

My search filter highlight is currently displaying [object Object] instead of <mark>match values</mark> when replacing the values. This is the code I am using: this.countries.response.filter((val) => { const position = val.value.toLowerCa ...

Convert TypeScript-specific statements into standard JavaScript code

For my nextjs frontend, I want to integrate authentication using a keycloak server. I came across this helpful example on how to implement it. The only issue is that the example is in typescript and I need to adapt it for my javascript application. Being u ...

Exploring Angular 4 with the power of Rangy modules

I've recently started working with Angular 4 and decided to create a basic app by forking the Angular quickstart. Now, I'm facing a challenge as I try to incorporate Rangy into my project. In my package.json, the dependencies are listed as follo ...

The 'connectedCallback' property is not found in the 'HTMLElement' type

After taking a break from my project for a year, I came back to find that certain code which used to work is now causing issues: interface HTMLElement { attributeChangedCallback(attributeName: string, oldValue: string, newValue: string): void; con ...

Obtaining data from a TypeScript decorator

export class UploadGreetingController { constructor( private greetingFacade: GreetingFacade, ) {} @UseInterceptors(FileInterceptor('file', { storage: diskStorage({ destination: (req: any, file, cb) => { if (process.env ...