RadDevon

How to Share a Variable Between Two Event Handlers in Javascript

Let’s take a look at a few different ways we can share data between event handlers. We’ll learn about scope along the way while building a (very contrived) example app.

A lot of basic Javascript concepts like scope can be tricky to keep in your head, so I created a reference with practical code examples of the fundamental parts of the language. Grab your copy so you’re not having to Google to refresh your memory.

The Secret Roller App

I’m tired of rolling dice for my bi-weekly D&D group, so I’m going to build a dice rolling app. For the most part, I don’t care what the value of the roll is. I want to put in a target roll and have the app tell me if I succeeded the check or failed. Sometimes, though, my players start getting suspicious of all my consecutive successes on difficult rolls and want to see what I actually rolled. I need a button that will show the actual roll so my players will put down their pitchforks when they suspect foul play.

Here’s a basic app I might write. First, a simple HTML form:

<form action=""> <label>Target: <input type="number" min="0" max="20" id="target"></label> <button type="button" id="roll">Roll</button> <button type="button" id="verify">Verify</button> </form> <span id="result"></span>

Then, a script to handle the rolling and displaying results:

const form = document.querySelector('form'); form.addEventListener('submit', (event) => { event.preventDefault(); }); const targetInput = document.querySelector('#target'); const rollButton = document.querySelector('#roll'); const resultElement = document.querySelector('#result'); rollButton.addEventListener('click', (event) => { let rollValue = Math.floor(Math.random() * 20); var targetValue = parseInt(targetInput.value); if (rollValue >= targetValue) { resultElement.textContent = 'Success!'; resultElement.style.color = 'green'; } else { resultElement.textContent = 'Failure!'; resultElement.style.color = 'red'; } }); const verifyButton = document.querySelector('#verify'); verifyButton.addEventListener('click', (event) => { alert(`Roll value was ${rollValue}`); });

This code all looks reasonable, but it doesn’t work. Why? Because of variable scope.

Understanding Scope

Everything in the app above works until we get to the Verify button. Once that’s clicked, we get an ugly exception in the Javascript console:

Uncaught ReferenceError: rollValue is not defined

This exception happens because of the scope of the rollValue variable. Javascript variables have one of two different kinds of scope based on how they are declared. Variables declared with var are function-scoped. This means they only exist inside the function which contains their declaration. Variables declared with let or const are block-scoped. These variables exist only inside the curly braces ({}) which contain their declaration. If function-scoped variables are declared outside a function or if block-scoped variables are declared outside a block, they have global scope meaning they are available anywhere in the script.

This explains why we got the exception above. Since the rollValue variable was declared in the rollButton’s click handler function, we’re not able to access the value inside the verifyButton’s click handler function. The variable is scoped to the first handler and doesn’t exist where we try to access it in the second. Let’s look at a few options for solving the problem.

Solution 1: Changing the Scope

The easiest way to fix this problem is the change the scope of the variable. You can do this by changing where the variable is declared. Here’s the current declaration:

rollButton.addEventListener('click', event => { let rollValue = Math.floor(Math.random() * 20); ... });

We can only read this variable inside the rollButton’s 'click' event handler function. If we move it outside the function, it will be globally scoped and can be read from anywhere in your application.

Note: This method will not work for variables defined with const since they have to be assigned a value when they are declared and the value cannot be changed. You can define your const-defined variable with let or var instead if you want to use this method.

You may wonder how we can do this since we need to set the value inside the event handler. Javascript will let us declare the variable without setting its value. Here’s what that looks like:

let rollValue; rollButton.addEventListener('click', event => { rollValue = Math.floor(Math.random() * 20); ...

Line 9 basically tells Javascript we have a variable called rollValue and sets its scope. In this case, the scope is global since it isn’t defined in a block. That means its value can be accessed anywhere in our code. At this point, the variable does not have a value.

Once we get inside the event handler down on line 11, we’ll have Javascript generate a new random number and assign it to the rollValue variable. Since this variable has already been defined, this line doesn’t need the let keyword. We’re just assigning a value to a variable that already exists.

Global variables are useful in cases like this, but they can be dangerous. If you have a lot of code sharing this same namespace, the rollValue variable is used up for the entire thing. If you accidentally start trying to use it for something else and then go back to it for this value, it could have been overwritten with a different value causing unpredictable results. We’re nowhere near that level of complexity or that amount of code, but your application might be.

Here’s a demo:

Solution 2: Using a Data Attribute

Data attributes were introduced in HTML5 as a convenient way to store data inside your HTML. It essentially allows you to create arbitrary attributes on an element.

Before data attributes, each HTML attribute had a specific function and expected a specific kind of value. img elements have a src attribute that defines the path to the image you want to display. input elements have a type attribute that tells the browser which form control to display (e.g. a plain text field, a password field, a button). You couldn’t just make up your own attributes and start sticking them anywhere.

That’s precisely what data attributes allow for. Preface your attribute with data- and stick whatever value you want in it. Here’s what a data attribute looks like:

<p data-id="2" data-name="Malcom" data-favorite-cat="Maru"></p>

The paragraph has an id data attribute with a value of "2", a name data attribute with a value of "Malcom", and, just to show you can really name these attributes whatever you want, a favorite-cat data attribute with a value of "Maru".

You can pull these out easily in your Javascript. We can also write a value to a data attribute with Javascript which is what we’ll use in this method. In our Roll button click handler, we’ll stuff the roll value into a data attribute on the Roll button.

rollButton.dataset.value = rollValue;

We’ll pull it back out in the Verify button click handler so we can show it to the user.

let rollValue = rollButton.dataset.value;

Here’s the full solution:

Solution 3: Putting the Data on the Page

The reason I don’t love the data attribute solution for this problem is that it makes the most sense to me when you need to get data from the back-end into the front-end. Your back-end code is probably generating your HTML, and, if you already know you’re going to need certain data that’s tied to DOM elements in your front-end Javascript, it makes a lot of sense to put it into data attributes instead of trying to make additional requests for the data after the page has already loaded… assuming it isn’t a ton of data.

That’s not exactly the case here. It’s also not the case that the data is intrinsically related to the DOM element or the object that element represents. (Although the button triggers a die roll, you wouldn’t say the button is a die roll or even represents a die roll.)

Putting the data on the page makes a little more sense here since we do ultimately want to display it to the user. It’s also a convenient workaround for needing to get data into two event handlers. We’ll put the roll value on the page as soon as the roll is made, but we’ll keep it hidden until the user clicks the Verify button. The Verify button won’t need the data since its only role is to reveal the data already on the page.

The first thing we’ll do is make an element we can place the roll value into. (We could add an element to the page later with Javascript, but we don’t really gain anything by doing that. We’ll just add the element in the HTML.)

<span id="rollValue" class="hidden"></span>

Note the hidden class we’ve applied. We’ll style this class so that the new element is not visible by default.

.hidden { display: none; }

We have a few things to do in the Javascript. First, we’ll select our new element in the global scope so we can manipulate it in any of the event handlers.

const rollValueElement = document.querySelector('#rollValue');

Each time we re-roll, we’re going to want to hide the roll result again since we want the Verify button to show the result only when clicked but not permanently for future rolls. (Look at line 11 in the code below.) We also need to put the roll result in the element. (Line 21.) Both of these happen in the Roll button click handler.

rollButton.addEventListener('click', (event) => { rollValueElement.classList.add('hidden'); let rollValue = Math.floor(Math.random() * 20 + 1); var targetValue = parseInt(targetInput.value); if (rollValue >= targetValue) { resultElement.textContent = 'Success!'; resultElement.style.color = 'green'; } else { resultElement.textContent = 'Failure!'; resultElement.style.color = 'red'; } rollValueElement.textContent = rollValue; });

This makes our Verify button click handler really simple. All it does is remove the hidden class from the result element.

verifyButton.addEventListener('click', (event) => { rollValueElement.classList.remove('hidden'); });

Here’s the full demo of solution 3:

Which Is Best?

In our particular situation, I like solution 3 best because it accomplishes what we want without having to pollute the global namespace. For applications where you really do need to assign a variable in one handler and use it in another, I like solution 1. It forces you to pay close attention to your variable names which can get more difficult as your program gets bigger, but it’s a simple solution that lets us accomplish exactly what we want.

Your best bet is to figure out if there’s a simpler way to do what you want without declaring a new global variable. Failing that, just use a global variable and be done with it.

It’s not always easy to remember the rules for scope in Javascript (especially when you’re new to the language, so grab a free reference before you go. Happy coding!