Home > Net >  How to keep the reference to a function without affecting the scope
How to keep the reference to a function without affecting the scope

Time:08-05

I want to keep the scope at a module level, while having a reference to a function, so that I can add it to an event listener (as a callback), and also remove it:

export default () => ({

init() {
    // Detect escape key press
    window.addEventListener('keyup', this.escapeKeyPressListener, true);
},

whatever() {
    // Do whatever
    ...

    // Remove event listener
    window.removeEventListener('keyup', this.escapeKeyPressListener);
},

escapeKeyPressListener: (event) => {
    console.log('added');
    if (event.key === 'Escape' || event.key === 'Esc') {
        event.preventDefault();
        this.whatever();
    }
}

});

Just as additional info, even if it shouldn't matter, the module is being used with Alpine, as follows:

import lightbox from "./alpine-modules/lightbox";
Alpine.data('lightbox', lightbox);

But this is not working, when this.whatever() is reached, the scope is incorrect.

How should I structure this so that I can add and remove the function to the listener and also keep the scope working as I want it to?

Did I try something or am I just guessing?

Well... after learning about arrow functions and reading this similar but not the same question, I tried using:

window.addEventListener('keyup', (event) => this.escapeKeyPressListener, true);

... and defining the function just as a normal one:

escapeKeyPressListener(event) {

It works as in... it triggers the listener and the scope is fine, but then, how do I remove the listener without having a reference to it?

Based on Quentin's feedback, I did it as follows (the method names are different because my previous example was a simplified/generalized version) with the inital aim of keeping this simple:

/**
 * @property isOpen - controls the display state of the lightbox
 */
let isOpen = false;

/**
 * Responsible for opening the lightbox
 */
const open = () => {
    isOpen = true;

    // Detect escape key press
    window.addEventListener('keyup', escapeKeyPressListener, true /* Capture it for any part in the DOM */);
};

/**
 * Responsible for closing the lightbox
 */
const close = () => {
    isOpen = false;

    // Stop detecting escape key press
    window.removeEventListener('keyup', escapeKeyPressListener);
};


/**
 * Responsible for adding listener function for escape keypress
 * @param {object} event - keyup event listener
 */
const escapeKeyPressListener = (event) => {
    console.log('added');
    if (event.key === 'Escape' || event.key === 'Esc') {
        event.preventDefault();
        close();
    }
};

const lightbox = () => ({
    isOpen,
    open,
    close,
    escapeKeyPressListener,
});

export default lightbox;

The module stopped working but shows no errors.

Interestingly, if I change the initial let isOpen = false; to let isOpen = open;, Alpine renders the component as opened, so... Alpine is actually getting and processing part of this.

CodePudding user response:

You're breaking the value of this in two different ways.


Arrow functions

escapeKeyPressListener is an arrow function, so the value of this inside it is lexical. It will be same as the this value of the module (undefined since modules run in strict mode) and not the object you are creating inside the module.

Passing functions

You are passing escapeKeyPressListener to addEventListener so if it wasn't an arrow function, this would be the object on which it is listening: window.

Neither of those will let you reference the object.


Use module scope

You said I want to keep the scope at a module level, but you haven't done that. This is easier if you do.

Everything has a name, and you don't need to use this at all.

const init = () => {
        // Detect escape key press
        window.addEventListener('keyup', escapeKeyPressListener, true);
};

const whatever = () => {
        // Do whatever
        ...

        // Remove event listener
        window.removeEventListener('keyup', escapeKeyPressListener);
    
};

const escapeKeyPressListener: (event) => {
    console.log('added');
    if (event.key === 'Escape' || event.key === 'Esc') {
        event.preventDefault();
        whatever();
    }
};


const defaultObject = {
    init,
    whatever,
    escapeKeyPressListener,
};

export default defaultObject;

This get more complex if you want to call init multiple times and have them deal with independent instances. At that point you should start looking into classes instead of plain objects.

CodePudding user response:

This is not a scope problem. This is a this problem.

The problem is that you are trying to be very terse (using an expression to immediately resolve to an anonymous object, which you are then immediately exporting), which denies you a handle to the this you want.

Quentin points you to the idiomatic solution, but what if you were wedded to keeping everything in a single expression?

I think something like the following might work. Don't do this, by the way.


export default () => ((function unboundEscapeKeyPressListener(event) => {
        console.log('added');
        if (event.key === 'Escape' || event.key === 'Esc') {
            event.preventDefault();
            this.whatever();
        }
    }), {

        init() {
            this.escapeKeyPressListener ??= unboundEscapeKeyPressListener.bind(this)
            // Detect escape key press
            window.addEventListener('keyup', this.escapeKeyPressListener, true);
         },

        whatever() {
            if(!this.escapeKeyPressListener) 
                throw 'Not initialized.'
            // Do whatever
            ...

            // Remove event listener
            window.removeEventListener('keyup', this.escapeKeyPressListener);
        }

    });

  • Related