After another pat on the back from our manager, we get a notification that a new task has been assigned to us: “While most users find it useful, some have asked if they can disable the spinner. Add a feature that turns off the spinner functionality once a certain combination of keys has been pressed within a time period”. So in this lesson, we will look at building an observable factory, that can be initialized with a certain list of key codes, and will fire whenever that key combo gets pressed quickly, within a 5 second window. We will be introducing the fromEvent, concat and takeWhile operators.
Instructor: [00:00] Our virtual manager now tells us that our internal QA team needs to disable the spinner while testing certain features. The way we're going to do that is that whenever somebody quickly presses a secret key combination on the keyboard, the spinner will get disabled.
[00:16] Let's try and understand that requirement and see how we can break it down into simple English statements. Assume that we have a list of keys, A, S, D, N, F that the user has to type within a certain time limit. Our first thought is that we don't need to do anything until the combo has started.
[00:32] How do we know that a combo has started? Well, we wait for the user to press the A key and once they press that, then we are in combo mode. At this point, we need to listen really closely to see if they're correctly pressing the rest of the keys in the combo. Let's try that out.
[00:48] Whenever somebody starts a combo, keep taking or listening for the rest of the combo keys. How long do we want to listen for keys? What are going to be the conditions taking us out of the combo mode again?
[01:00] Well, time is the first one. If the user hasn't finished the combo within a certain time limit, the combo will fail. Let's write that out, "Until the timer has run out." Let's say that the user starts the combo again. They press the first letter in the combo, then they press the second, S, the third, D, but then on the fourth, they press W, which is the wrong key, so they miss out.
[01:24] That's our second condition, combo was not followed correctly. Write that out, "While the combo is being followed correctly." Now, the user presses A again to start the combo a third time. They then quickly press S, D, and F to complete the combo. Our third condition can be, "And until they've reached the end of the combo."
[01:47] We can be more specific than that. How do we know we got to the end of the combo? We know because we got back three letters while in combo mode. Why does getting three letters represent the completion of the combo?
[02:00] If our previous two conditions, the one that keeps track of time and the one that keeps track of whether the combo is being followed correctly, have let us get to all the three keys in our combo, and they have not cut us off, that means that the combo is successful.
[02:16] Three is actually the total length of the combo, which is four minus one, because we don't count the first letter. We can go back and revise our condition to, "And until we got combo length minus one, keys back."
[02:31] I'll now declare an observable called any key presses. I'll use the from event observable factory that allows us to pass a DOM element and the event we want to listen for. In our case, we want to listen for key presses on the whole document object. Because we get raw DOM events from this, we're going to extract the underlying key from each event.
[02:54] I'm also going to create an observable factory called key pressed, which is going to take in a key as the input and it's going to return an observable that emits anytime that specific key is pressed. Cool. Now that we have our building blocks, we can start assembling them to solve our problem.
[03:11] To make this truly useful and not lock it down to a single set of keys, we're going to have a function that can be invoked with an array of keys and will return an observable that will emit anytime a combo involving these four keys in order has happened. I'll define the function and move our requirements right above it so we can keep track of them.
[03:33] The combo initiator will be the first letter from our combo. Whenever the combo initiator is pressed, we want to switch map to another observable that only becomes alive when the combo has started.
[03:46] Once we're in combo mode, we want to continue listening for key presses and takeUntil our timer has expired. In this case, if the user hasn't completed the combo within three seconds, this inner observable will be disposed of and they're going to need to start again from scratch and keep taking while the combos being followed correctly.
[04:08] TakeWhile the key that was pressed matches the combo. takeWhile is an operator that keeps its source alive, as long as the function passed to it keeps returning true. It calls this function with each new element from the source, but it also passes in the index from the emission starting from zero.
[04:28] The reason we do index plus one in here is because the first element of our inner combo will be at index one. The first time this is called, index will be zero. User presses A, we start the inner combo. They then press S, this gets called with S and index zero. We check if S matches the letter that is at position one in our combo, which it is and so on for the rest of the keys.
[04:57] Finally, we want to takeUntil we get combo length minus one keys back. Take keyCombo.length minus one. Take keeps its source alive until it receives key combo length minus one emissions. After that, it just terminates the source.
[05:15] Let's try this out. I'll declare an interval, which will emit every second and I'll keep taking values from it until the combo is triggered. I'll subscribe to console log next notifications and I'll also log when it completes.
[05:30] Just so you can see what I'm pressing on the keyboard, I'll log out values anytime I press a key. If I bring in the app, you can see that we get values each second. I'll press A, S, D and F and we can see that the timer definitely stopped. We also see that it completed only after the second letter S and not when the full combo finished.
[05:53] What happened there is that the first key was pressed correctly, combo was initiated. Then, we press another correct key within our time limit so both of these let that value go through. Because we hadn't reached our limit for take, it went through this one as well.
[06:09] Our second letter was immediately emitted as a notification, which triggered the takeUntil. What we really want is instead of taking all these three values and letting all of them go through, we want to skip the first two and only take the last one if we get to it. If the combo is bigger, we want to skip the first four and only take the last one.
[06:33] Basically, we want to skip the first combo length minus two values inside the inner combo and only take the last one. In that case, I'll add the skip combo length minus two right before the take and then I'll just take the last one.
[06:51] If these two conditions don't terminate our combo prematurely, we're going to keep skipping elements right until we get to the last one. If that last one is correct, we're going to take it and emit it as a notification. We should now only be triggering the takeUntil if we successfully got to the end of the combo.
[07:10] Let's test that again. The timer starts, I'll start pressing keys and once we got to the last one, it completes. We can see that it actually completed once we press the combo keys in the correct order.
[07:24] I'll now try to refresh and I'll press the first letter correctly, but then press the wrong letter and then continue pressing the correct ones. Now you can see that the interval keeps going. What happened there is I press the first one, but then I broke the combo and then it didn't matter that I continue them press the letters correctly. The combo didn't complete.
[07:45] If I refresh again, I start the combo but then wait for three seconds and then press the rest of the combo, again, it's not going to complete. It's going to continue notifying.
[07:56] Again, even though we did press all of the keys in the combo, because there was such a huge gap between the first letter and the second letter, we didn't complete the combo correctly, so it works.
[08:08] To recap, we spent time to really understand our requirement and break it down into small problems that are defined as simple English sentences. We then separated our combo starting condition from the rest of the logic as that allows us to think in isolation and focus just on the question of what actually terminates the combo.
[08:30] We then learned about the various take operators and how they can be used to dispose of the source when different events happen.