Declarative Logic with Knockout
Brandon Wallace

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.

Existing Scrollbar Rules

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:

self.showScrollbar = ko.computed(function () {    return !self.allDataVisible() &&        (self.mouseIsHovering() ||         self.isDragging() ||         self.isMomentumScrolling() ||         self.isOverflowScrolling());});

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.

Expanding the rule

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.

self.showScrollbar = ko.computed(function () {    return !self.allDataVisible() &&        (self.mouseIsHovering()         || self.isDragging()         || self.isMomentumScrolling()         || self.isOverflowScrolling()         || self.isFlashingScrollBar());});

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.

Defining isFlashingScrollBar property

Model it as an event or a value?

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.

Define the event

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.

self.onFlashScrollBar = new ko.subscribable();

Define the boolean property

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.

First imperative attempt

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.

// define the propertyself.isFlashingScrollBar = ko.observable(false); // All of our viewModels have a autoDispose CompositeDisposable that gets// disposed when the widget is destroyed.// So, just add our subscription to it so that it gets// unsubscribed when the widget is destroyed.self.autoDispose.add(self.onFlashScrollBar.subscribe(function () {    // set it to true.    self.isFlashingScrollBar(true);     // start a timer which sets it to false after 2s    setTimeout(function () { self.isFlashingScrollBar(false); }, 2000);}));

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.

Second imperative attempt

// define the propertyself.isFlashingScrollBar = ko.observable(false);self.autoDispose.add(self.onFlashScrollBar.subscribe(function () {    // set it to true.    self.isFlashingScrollBar(true);     // start a timer which sets it to false after 2s    self.autoDispose.add(Rx.Observable.timer(2000).subscribe(function () {        self.isFlashingScrollBar(false);    }));}));

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.

Third imperative attempt

// define the propertyself.isFlashingScrollBar = ko.observable(false);var timerSubscription = new Rx.SerialDisposable();self.autoDispose.add(timerSubscription); // ensure the final timer gets disposed when our widget gets disposedself.autoDispose.add(self.onFlashScrollBar.subscribe(function () {    // set it to true.    self.isFlashingScrollBar(true);     // start a timer which sets it to false after 2s    // store the subscription in timerSubscription, which will dispose of any earlier timer that is still running.    timerSubscription.setDisposable(Rx.Observable.timer(2000).subscribe(function () {        self.isFlashingScrollBar(false);    }));}));

This one does exactly what we want.

It still has 2 problems:

  1. It is pretty verbose. The timer management is obscuring the logic, making it harder for a developer to see the intent of the code. This is a common problem when you have “nested calls to subscribe”. We subscribe to onFlashScrollBarand in our callback, we subscribe to a timer. Rx has alot of operators that can be used to avoid this nested subscribe problem (because the nested subcribes and subscription management code is hidden inside the operators, freeing us from having to write it).
  2. Since isFlashingScrollBar is a simple observable property, anyone can just change it. What happens if some developer just decides to do scrolledView.isFlashingScrollBar(true);? It would totally break our logic.

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.

The Declarative attempt

In this version, we’ll actually make isFlashingScrollBar a computed property, using Rx to define a time-based computation:

self.isFlashingScrollBar = self.onFlashScrollBar.toRx()    .flatMapLatest(function () {        // Return a sequence that produces "true" immediately,        // then "false" after a 2 second delay        var falseAfterDelay = Rx.Observable.return(false).delay(2000);        return falseAfterDelay.startWith(true);    })    .toKO();

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.

Triggering the event when the widget is first created

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:

self.onFlashScrollBar.notifySubscribers();

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.

RECENT POSTS FROM
THIS AUTHOR