Let’s consider a simple but common situation. You have developed an e-commerce site; the user can fill their shopping cart, and in the end, they must click on a Bill me button so that their credit card will be charged. However, the user shouldn’t click twice (or more), or they will be billed several times.
The HTML part of your application might have something like this somewhere:
<button id="billButton"
onclick="billTheUser(some, sales, data)">Bill me
</button>
And, among the scripts, you’d have something similar to the following code:
function billTheUser(some, sales, data) {
window.alert("Billing the user...");
// actually bill the user
}
A bad example
Assigning the events handler directly in HTML, the way I did it, isn’t recommended. Instead, unobtrusively, you should set the handler through code. So, do as I say, not as I do!
This is a bare-bones explanation of the web page problem, but it’s enough for our purposes. Now, let’s get to thinking about ways of avoiding repeated clicks on that button. How can we manage to prevent the user from clicking more than once? That’s an interesting problem, with several possible solutions – let’s start by looking at bad ones!
How many ways can you think of to solve our problem? Let’s go over several solutions and analyze their quality.
Solution 1 – hoping for the best!
How can we solve the problem? The first solution may seem like a joke: do nothing, tell the user not to click twice, and hope for the best! Your page might look like Figure 2.1:
Figure 2.1 – An actual screenshot of a page, just warning you against clicking more than once
This is a way to weasel out of the problem; I’ve seen several websites that just warn the user about the risks of clicking more than once and do nothing to prevent the situation. So, the user got billed twice? We warned them... it’s their fault!
Your solution might simply look like the following code:
<button
id="billButton"
onclick="billTheUser(some, sales, data)">Bill me
</button>
<b>WARNING: PRESS ONLY ONCE, DO NOT PRESS AGAIN!!</b>
Okay, this isn’t an actual solution; let’s move on to more serious proposals.
Solution 2 – using a global flag
The solution most people would probably think of first is using some global variable to record whether the user has already clicked on the button. You define a flag named something like clicked
, initialized with false
. When the user clicks on the button, if clicked
is false
, you change it to true
and execute the function; otherwise, you do nothing at all. This can be seen in the following code:
let clicked = false;
.
.
.
function billTheUser(some, sales, data) {
if (!clicked) {
clicked = true;
window.alert("Billing the user...");
// actually bill the user
}
}
This works, but it has several problems that must be addressed:
- You are using a global variable, and you could change its value by accident. Global variables aren’t a good idea, in JavaScript or other languages. You must also remember to re-initialize it to
false
when the user starts buying again. If you don’t, the user won’t be able to make a second purchase because paying will become impossible.
- You will have difficulties testing this code because it depends on external things (that is, the clicked variable).
So, this isn’t a very good solution. Let’s keep thinking!
Solution 3 – removing the handler
We may go for a lateral kind of solution, and instead of having the function avoid repeated clicks, we might just remove the possibility of clicking altogether. The following code does just that; the first thing that billTheUser()
does is remove the onclick
handler from the button, so no further calls will be possible:
function billTheUser(some, sales, data) {
document
.getElementById("billButton")
.onclick = null;
window.alert("Billing the user...");
// actually bill the user
}
This solution also has some problems:
- The code is tightly coupled to the button, so you won’t be able to reuse it elsewhere
- You must remember to reset the handler; otherwise, the user won’t be able to make a second purchase
- Testing will also be more complex because you’ll have to provide some DOM elements
We can enhance this solution a bit and avoid coupling the function to the button by providing the latter’s ID as an extra argument in the call. (This idea can also be applied to some of the further solutions that we’ll see.) The HTML part would be as follows; note the extra argument to billTheUser()
:
<button
id="billButton"
onclick="billTheUser('billButton', some, sales, data)"
>Bill me
</button>
We also have to change the called function so that it will use the received buttonId
value to access the corresponding button:
function billTheUser(buttonId, some, sales, data) {
document.getElementById(buttonId).onclick = null;
window.alert("Billing the user...");
// actually bill the user
}
This solution is somewhat better. But, in essence, we are still using a global element – not a variable, but the onclick
value. So, despite the enhancement, this isn’t a very good solution either. Let’s move on.
Solution 4 – changing the handler
A variant to the previous solution would be not to remove the click function, but to assign a new one instead. We are using functions as first-class objects here when we assign the alreadyBilled()
function to the click event. The function warning the user that they have already clicked could look something like this:
function alreadyBilled() {
window.alert("Your billing process is running; don't
click, please.");
}
Our billTheUser()
function would then be like the following code – note how instead of assigning null
to the onclick
handler as in the previous section, now, the alreadyBilled()
function is assigned:
function billTheUser(some, sales, data) {
document
.getElementById("billButton")
.onclick = alreadyBilled;
window.alert("Billing the user...");
// actually bill the user
}
There’s a good point to this solution; if the user clicks a second time, they’ll get a warning not to do that, but they won’t be billed again. (From the point of view of user experience, it’s better.) However, this solution still has the very same objections as the previous one (code coupled to the button, needing to reset the handler, and harder testing), so we don’t consider it quite good anyway.
Solution 5 – disabling the button
A similar idea here is instead of removing the event handler, we can disable the button so the user won’t be able to click. You might have a function such as the one shown in the following code, which does exactly that by setting the disabled
attribute of the button:
function billTheUser(some, sales, data) {
document
.getElementById("billButton")
.setAttribute("disabled", "true");
window.alert("Billing the user...");
// actually bill the user
}
This also works, but we still have objections as with the previous solutions (coupling the code to the button, needing to re-enable the button, and harder testing), so we don’t like this solution either.
Solution 6 – redefining the handler
Another idea: instead of changing anything in the button, let’s have the event handler change itself. The trick is in the second line of the following code; by assigning a new value to the billTheUser
variable, we are dynamically changing what the function does! The first time you call the function, it will do its thing, but it will also change itself out of existence by giving its name to a new function:
function billTheUser(some, sales, data) {
billTheUser = function() {};
window.alert("Billing the user...");
// actually bill the user
}
There’s a special trick in the solution. Functions are global, so the billTheUser=...
line changes the function’s inner workings. From that point on, billTheUser
will be the new (null) function. This solution is still hard to test. Even worse, how would you restore the functionality of billTheUser
, setting it back to its original objective?
Solution 7 – using a local flag
We can go back to the idea of using a flag, but instead of making it global (which was our main objection to the second solution), we can use an Immediately Invoked Function Expression (IIFE), which we’ll see more about in Chapter 3, Starting Out with Functions, and Chapter 11, Implementing Design Patterns. With this, we can use a closure, so clicked
will be local to the function and not visible anywhere else:
var billTheUser = (clicked => {
return (some, sales, data) => {
if (!clicked) {
clicked = true;
window.alert("Billing the user...");
// actually bill the user
}
};
})(false);
This solution is along the lines of the global variable solution, but using a private, local variable is an enhancement. (Note how clicked
gets its initial value from the call at the end.) The only drawback we could find is that we'll have to rework every function that needs to be called only once to work in this fashion (and, as we’ll see in the following section, our FP solution is similar to it in some ways). Okay, it’s not too hard to do, but don’t forget the Don’t Repeat Yourself (DRY), usual advice!
We have now gone through multiple ways of solving our “do something only once” problem – but as we’ve seen, they were not very good! Let’s think about the problem functionally so that we get a more general solution.