Let’sconsider 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 aBill me button so that their credit card will be charged. However, the user shouldn’t click twice (or more), or they will be billedseveral times.
The HTML part of your application might have something likethis somewhere:
<button onclick="billTheUser(some, sales, data)">Bill me </button>
And, among the scripts, you’d have something similar to thefollowing 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 asI 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 atbad ones!
How many ways can you think of to solve our problem? Let’s go over several solutions and analyzetheir quality.
Solution 1 – hoping for the best!
How can we solve theproblem? 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 likeFigure 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’stheir fault!
Your solution might simply look like thefollowing 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 anactual solution; let’s move on to moreserious proposals.
Solution 2 – using a global flag
The solutionmost 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 likeclicked
, initialized withfalse
. When the user clicks on the button, ifclicked
isfalse
, you change it totrue
and execute the function; otherwise, you do nothing at all. This can be seen in thefollowing 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 mustbe 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 willbecome impossible. - You will have difficulties testing this code because it depends on external things (that is, theclicked variable).
So, this isn’t a very good solution. Let’skeep thinking!
Solution 3 – removing the handler
We maygo 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 thatbillTheUser()
does is remove theonclick
handler from the button, so no further calls willbe possible:
function billTheUser(some, sales, data) { document .getElementById("billButton") .onclick = null; window.alert("Billing the user..."); // actually bill the user}
This solution also hassome problems:
- The code is tightly coupled to the button, so you won’t be able to reuseit elsewhere
- You must remember to reset the handler; otherwise, the user won’t be able to make asecond purchase
- Testing will also be more complex because you’ll have to provide someDOM 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 argumenttobillTheUser()
:
<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 receivedbuttonId
value to access thecorresponding 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 theonclick
value. So, despite the enhancement, this isn’t a very good solution either. Let’smove 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 thealreadyBilled()
function to the click event. The function warning the user that they have already clicked could look somethinglike this:
function alreadyBilled() { window.alert("Your billing process is running; don't click, please.");}
OurbillTheUser()
function would then be like the following code – note how instead of assigningnull
to theonclick
handler as in the previous section, now, thealreadyBilled()
functionis 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’tconsider it quitegood anyway.
Solution 5 – disabling the button
A similar idea hereis 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 thedisabled
attribute ofthe 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 thissolution either.
Solution 6 – redefining the handler
Another idea: instead ofchanging 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 thebillTheUser
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 anew function:
function billTheUser(some, sales, data) { billTheUser = function() {}; window.alert("Billing the user..."); // actually bill the user}
There’s aspecial trick in the solution. Functions are global, so thebillTheUser=...
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 ofbillTheUser
, setting it back to itsoriginal objective?
Solution 7 – using a local flag
We can goback to the idea of using a flag, but instead of making it global (which was our main objection to the second solution), we canuse anImmediately Invoked Function Expression (IIFE), which we’ll see more about inChapter 3,Starting Out with Functions, andChapter 11,Implementing Design Patterns. With this, we can use a closure, soclicked
will be local to the function and not visibleanywhere 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 howclicked
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 theDon’t Repeat Yourself (DRY),usualadvice!
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 moregeneral solution.