Javascript event listeners be careful where you bind this

Javascript event listeners be careful where you bind this

In Javascript User interface programming you often need to work with EventListeners. In modern Javascript you may also be doing this in a class, which is handy. One of the  things that sucks about Javascript event listeners is they eat memory and slow your app down, if you are not REALLY careful.

sweet meme
Slow down your app the easy way!

They do this because each time the action you use to add the event listener code fires, more and more code gets added to memory, more things need to be kept up with etc.

There are some best practices that help with this issue such as always remembering to remove the event listeners, never use anonymous functions, arrow functions etc. Here on MDN there is a lot of good information I won’t waste time repeating in this article.

What I want to cover in this article is properly binding this within a class when using Event Listeners so that the function signatures match and therefore the listeners can properly be removed. Don’t think reloading the page solves your problem either… it doesn’t. In fact it just makes the browser eat more and more memory.

The reason we need to bind this is the code in our Javascript classes that handle the events ( event handlers ) usually need to access other methods and properties within the class when called by the event listener code.

Magical Bugs

Worse than the memory issue is the magical, insanely, nearly impossible bugs that having multiple listeners calling the same function can create.

If for example the function changes a variable value so that some UI action changes etc. One example I just ran into was a method in a class that monitored a keypress then moved the cursor. This was for an auto completer with a drop list of suggestions. The idea was to have Javascript highlight the selected option and accept it when the user hits enter. Below is the code example.

keyPressHandler(event) {

        let target = event.target.id;
        this.keyPressed = event.keyCode;
        this.optionElementsList = document.getElementById(target + "-ez-hashtags-list");

        if (this.optionElementsList) {
            this.optionElementsList = this.optionElementsList.getElementsByTagName("div");
        }

        switch (event.keyCode) {
            /*If the arrow DOWN key is pressed,
           increase the currentChoice variable:*/
            case 40:
                console.log('inside keyPressHandler handling keydown event');
                this.currentChoice++;
                this.makeActive();
                break;
            /*If the arrow UP key is pressed,
       decrease the currentFocus variable:*/
            case 38 :
                console.log('inside keyPressHandler handling keydown event');
                this.currentChoice--;
                this.makeActive();
                break;
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
            case 13 :
                event.preventDefault();

                if (this.currentChoice > -1) {
                    /*and simulate a click on the "active" item:*/
                    if (this.optionElementsList) {
                        this.optionElementsList[this.currentChoice].click();
                    }
                    //empty the keysTyped variable
                    this.keysTyped = '';
                }
                break;
            case 8 :
            case 9 :
                //9 is tab, close any list if they hit tab
                //8 is backspace close the current list if they hit backspace
                this.closeOpenLists();
                break;
        }
    }

This is similar to what you need to do when creating a custom WYSIWYG html editor. You need to monitor every damn key pressed and perform actions based on that. The real fun begins when you have two listeners say one for keyup and one for keydown or just two of the same.

The code above if the user hits the up or down arrow decreases or increases the variable used to track the users expected action. It then calls another method to make the correct option highlighted for the user as selected. Below is the code.

makeActive() {

        if (!this.optionElementsList) {
            return false;
        }
        /*start by removing the "active" class on all items:*/
        this.makeInactive();

        if (this.currentChoice >= this.optionElementsList.length) {
            this.currentChoice = 0;
        }
        if (this.currentChoice < 0) {
            this.currentChoice = (this.optionElementsList.length - 1);
        }
        /*add class "autocomplete-active":*/
        this.optionElementsList[this.currentChoice].classList.add("ez-hashtags-active");
    }

This is where the currentChoice variable comes in play. The up and down arrows add the class which changes the background color to make it look highlighted. Then when the user hits enter, the selected value is entered into the input, but in this case a div with contenteditable=true This UI stuff is a real pain in the ass.

The BAD CODE

Now lets look at the wrong way to bind this, even though it looks like this should totally work properly.

 createChoiceList(elementId, inputValue) {

        if (Utils.isEmpty(inputValue)) {
            return false;
        }
        this.currentChoice = -1;

        if (!Utils.isEmpty(this.listOptions)) {

            if (Utils.notEmpty(this.optionContainer)) {
                this.closeOpenLists();
            }

            /*create a DIV element that will contain the items (values):*/
            this.optionContainer = document.createElement("DIV");
            this.optionContainer.setAttribute("id", elementId + "-ez-hashtags-list");
            this.optionContainer.setAttribute("class", "ez-hashtags-items");
            /*append the DIV element as a child of the hashtag div container:*/
            this.hashtagsDivElement.parentNode.appendChild(this.optionContainer);

            /*for each item in the array...*/
            for (let i = 0; i < this.listOptions.length; i++) {

                /*check if the item starts with the same letters as the text field value:*/
                let firstLetter = this.listOptions[i].substr(0, inputValue.length).toUpperCase();

                if (firstLetter === inputValue.toUpperCase()) {

                    /*create a DIV element for each matching element:*/
                    let b = document.createElement("DIV");
                    /*make the matching letters bold:*/
                    b.innerHTML = "#" + this.listOptions[i].substr(0, elementId.length) + "";
                    b.innerHTML += this.listOptions[i].substr(elementId.length);
                    /*insert an input field that will hold the current array item's value:*/
                    b.innerHTML += "";
                    /*execute a function when someone clicks on the item value (DIV element):*/
                    this.optionContainer.appendChild(b);
                }
            }
            this.hashtagsDivElement.addEventListener('keydown', this.keyPressHandler.bind(this));
            this.optionContainer.addEventListener('click', this.choiceSelectionHandler.bind(this));
        }
    }

The offending code is the last two lines that add the even listeners. The intention here was to make the signature match the remove event listener code. Like  this below.

removeKeypressHandlers() {
        try {

            if (this.hashtagsDivElement) {
                this.hashtagsDivElement.removeEventListener('keydown',this.keyPressHandler.bind(this));
            }

            if (this.optionContainer) {
                this.optionContainer.removeEventListener('click', this.choiceSelectionHandler.bind(this));
            }

        } catch (e) {
            console.log("OOPS we caught an error " + e);
        }
    }

It looks good, like they match like it should function, right?

makes sense to me meme
Looks good to me…

WRONG

Even though those functions look like they match, bind(this) creates a new function every time it is called.

scared chimp
Oh that can’t be good. That can’t be good at all.

So now we have LOTS OF EVENT LISTENERS. And that causes magical bugs like the video below.  The real good example is after the 30 second mark.

Notice how when I first type the selection works. Then when I backspace and start again it just jumps to the last one. Then I backspace and start again and it works. Then it doesn’t I was like WTFF I am losing my mind. Watch towards the end for the magic to start really good.

God forbid you ever meet an articulate ass.

articulate ass
Meet the articulate ass

The right way

So how should we defeat this Javascript demon and bind this properly for our event handlers??? Well the answer is quite simple actually. You create another new variable in the constructor and bind this to the handler there. This way bind creates only 1 function, 1 time and it can easily be removed. As a bonus if you add the function 100 times in 100 calls, the browser will only actually add it 1 time.

So lets see the code.

        constructor(inputElementId, endpointUrl, fetchOnce = true, keysThreshold = 2) {

        //set the routes for the Routing object
        Routing.setRoutingData(routes);
        this.inputElementId = inputElementId;
        this.currentChoice = -1;
        this.endpointUrl = endpointUrl;
        this.fetchOnce = fetchOnce;
        this.inputElement = document.getElementById(inputElementId);
        this.hashtagsDivElement = null;
        this.hashtagValues = '';
        this.inputValues = '';
        this.optionContainer = null;
        this.optionElementsList = null;
        this.listOptions = '';
        this.keyPressed = '';
        this.keysTyped = '';
        this.boundChoiceSelectionHandler = this.choiceSelectionHandler.bind(this);
        this.boundKeypressHandler = this.keyPressHandler.bind(this);
        this.boundTypingHandler = this.handleTyping.bind(this);
        //add the hashtags div and hide the actual input element
        this.hideInputElement();
        this.addHashtagsDiv();
    }

As you can see in the code above,this goes in the class constructor. Anywhere else and you could be creating the same listeners over and over.

Then to actually add the listeners when you want them to be added you do this little trick below.

this.inputElement.addEventListener('keydown', this.boundKeypressHandler);
            this.optionContainer.addEventListener('click', this.boundSelectionHandler);

Notice now the binding is done in the constructor and the variable that holds the bound function is added in place of the previous binding in the addEventListener() calls. This way these eventListeners are only ever created once, the other times are ignored. Javascript bind actually creates and returns a new function with the scope needed to use this keyword.

To remove the event listeners later to cleanup memory, you do this.

removeKeypressHandlers() {
        try {

            if (this.inputElement) {
                console.log('Removing the input element keydown handler');
                this.inputElement.removeEventListener('keydown', this.boundKeypressHandler);
            }

            if (this.optionContainer) {
                console.log('removing the option container click handler');
                this.optionContainer.removeEventListener('click', this.boundSelectionHandler);
            }

        } catch (e) {
            console.log("OOPS we caught an error " + e);
        }

    }

Notice now the signatures of the calls match exactly this.boundxxx

I had learned this information many years ago and forgot it until I had this hard to catch bug above. I then finally remembered I was was doing stupid things and how to do this properly.

These kind of things will make you hate Javascript UI programming or any DOM action programming. I hope I never forget this lesson again.


Need Help with your project, devops, debugging, wordpress issues? Contact Me! I’ll help you.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: