In the application I am working on, we use a custom ScrolledView widget to implement a custom scrolling UX that works well with both mouse and multitouch. One aspect of the UX is that the scrollbar is hidden when nothing is going on (similar to iOS scrollbars). A usability issue was reported that users sometimes did not know that more content was available if they scrolled. Our UX designer suggested that we change the UX so that when the content is first displayed, the scrollbar should be shown for a brief period of time to draw the user’s attention to the fact that scrolling was available. We also want to expose this functionality to the developer so they can cause the scrollbar flash whenever they want (perhaps when they add more content?)
Implementing this new logic “the declarative way” is an interesting and typical example of how we use Knockout and Rx to capture the logic and business rules in declarative statements that just work and avoid the complex logic you’d need to implement this functionality imperatively.
The existing rules for when we display the scrollbar are…well, I don’t actually remember them. Let’s take a look at the relevant line of code in the viewmodel:
The statement is fairly readable even for a non-programmer (as long as they know what && and || means). But let’s break it down:
We want to show the scrollbar when:
Pretty straight-forward and easy to understand. Each of those variables used in the expression may themselves represent complex logic and rules. But we do not need to worry about that. We just focus on the big picture. Computed observables allow us to take a big hard problem and turn it into a composition of a bunch of smaller and easier problems. I think this is a fundamental advantage we gain by using Knockout. We’ll explore it a bit while adding this new feature.
So, for this feature, we have an additional situation in which we want to show the scrollbar. Basically we want the scrollbar to be visible whenever we are flashing it. That just means we need to add another condition to the showScrollbar computed property.
We’ve added a new hypothetical observable property isFlashingScrollBar that will evaluate to true when we want to show the scrollbar for the purpose of drawing the user’s attention to it (e.g. flashing it). Implementing this new property will be an interesting exercise. But with a few keystrokes, we’ve isolated the flashing behavior from all other considerations. We don’t have to go modify the multi-touch event handlers or scrolling/bouncing animation routines to make sure they work correctly with our new feature. By using this computed, we let Knockout handle the state management for us.
The actual flashing behavior is this:
When some event occurs, display the scrollbar for some time, then hide the scrollbar. We know one case for some event is when the widget is first created. We know another case for some event is when a developer has logic which triggers the flash. It certainly sounds like we need to model this as an event which updates the boolean state of isFlashingScrollBar.
In our ScrolledView viewmodel constructor, we need to add the event. For this app, we use ko.subscribable to define events our viewmodels can trigger or emit.
Now, the next trick is to define the isFlashingScrollBar property so that it is truewhenever our event is triggered. Then, after some time passes, reset it to false.
So, for our first attempt, we’ll do it the ‘typical’ way. We’ll subscribe to our event, and whenever it triggers, set the property to true. Then start a timer to set it to false later. Let’s see what happens.
Works great, but what happens if the widget is disposed after that timer starts and before it completes? We want to cancel the timer as soon our widget is disposed. We could store the timerId and call clearTimeout in our widget’s dispose method. But that’s a bit of work. Our widget’s already provide an autoDispose property we can add disposables to for automatic destruction when our widget gets disposed. And Rx provides timers (timer and interval) which can be subscribed instead of setTimeout/clearTimeout. Rx has the added advantage of being testable, which means we can write a unit test which uses a mock clock so we can control the timer precisely from our unit test.
Now that we’ve solved that problem, lets solve the next. Specifically, what happens if the event fires and we show the scrollbar, then exactly 1.9s later the event fires again? We’d want the scrollbar to stay visible for another 2s. But instead it is going to disappear in 100ms when the first timer expires. So…we need to cancel the previous timer and start a new timer. Which means we want to dispose of the previous timer subscription when we start a new one. Rx provides the SerialDisposable, which allows you to keep assigning new disposables to it. As you do, it disposes the previous. This leads to our new version.
This one does exactly what we want.
It still has 2 problems:
We can solve both problems by leaning a bit more on Rx and moving away from imperative flow control and back to something more declaritive.
In this version, we’ll actually make isFlashingScrollBar a computed property, using Rx to define a time-based computation:
That’s all we need. We’ve gotten rid of all of the subscribe calls and management of those subscriptions. And our isFlashingScrollBar is now a read-only computed property so no one can break our logic by updating it directly.
How does it work?
Line 1, we use toRx to convert our ko subscribable into an Rx Observable so that we can use the Rx LINQ operators to define our “calculation”.
Line 2, we use flatMapLatest, which lets us return a true/false sequence in response to each event. Each time the event fires, our function will get called which will return the timed sequence which will produce our desired boolean values. Once our function returns, flatMapLatest subscribes to it and lets it run. If our event fires again, flatMapLatest unsubscribes from the previous sequence (and cancels its timers) then subscribes to our new sequence.
Line 4, this just says “delay for 2 seconds, then return a false value”
Line 5, this just says, start the previous sequence with an immediate true value. So now we have a sequence that will immediately produce a true, wait 2 seconds, then produce a false.
Line 7, creates a ko.computed which will hold the values the sequence produces.
The last thing we need to do is actually trigger the first event. Our widget’s have a ready method that gets called when the viewmodel has been bound to the DOM.
So, we just add this to our viewmodel’s ready method:
Since our view is presumably already honoring the showScrollbar property, we are essentially done implementing this feature. We didn’t even need to touch the HTML.