A case for manipulating the DOM outside the Sproutcore RunLoop

KVO is one of Sproutcore coolest features. At the core of it there is the RunLoop in which events and notifications are efficiently processed, dispatched to their destination(s) and views modify the DOM to reflect the new application state.
Sproutcore developers are consequently told not to manipulate directly the DOM. When out-of-band events, like an ajax call returning from a third-party library or a WebSocket receiving a new message happen it is possible to trigger the RunLoop manually by calling SC.RunLoop.begin() ... SC.RunLoop.end().

Sometimes though it is not only necessary, but recommended, to bypass the RunLoop and manipulate directly the DOM to either provide UI refreshes as fast as the browser allows or avoid the expensive computations implicated in the RunLoop. These concepts were incidentally discussed on IRC just when I needed to implement a progress bar to provide feedback on the loading state of a particularly slow datasource and I am writing them down here so that others might benefit from them.

WFM disclaimer : now I don't know if the implementation I am going to document in this post is completely sound so take it with a grain of salt and/or discuss it on IRC before adopting it.

Run Loop links:
http://guides.sproutcore.com/core_concepts.html#the-run-loop (a bit short, but gives the idea)
http://frozencanuck.wordpress.com/2010/12/21/why-does-sproutcore-have-a-run-loop-and-when-does-it-execute/ (a must read, the post and the whole blog)

The problem
The application loads a GetCapabilities XML document from a WMS server. The document describes, among other things, the projection used by each layer. To correctly display a layer the projection must be loaded in the web gis and this operation might require a remote call to a projection registry.
Until this second remote call has completed the datastore cannot continue loading the layer list from the GetCapabilities document.

This process is, in most cases, immediate because the projections used are so common that they ship with the web gis and therefore do not require the remote call. But for thise cases when the remote call is needed, it is importanto to let the user know about what is going on and how long it is going to take.

The first approach
My first approach was to implement a SC.ProgressView in a modal pane. Unfortunately this does not work because datastore operations are already executed in a RunLoop and all notifications are therefore delayed until the loop end.
The result is that the SC.ProgressView is not updated until the loading process has completed and just jumps from 0 to 100% without any intermediate step. The user is not getting better feedback than if the application did without the progress bar altogether.

The solution
To provide better and faster (in fact as fast as the browser allows) feedback to the user the application needs to be able to modify the DOM directly, bypassing the facilities provided by Sproutcore.
To do it we need the following:
  1. a view representing a progress bar of some sort which can be directly updated, bypassing kvo
  2. a counter tracking progess
  3. a way to update the view with the current progress status
For demonstration purposes we will create an anonymous view embedded in a modal pane like the following:

App.progressPane = SC.PanelPane.create({
    layout:{ width:400, height:60, centerX:0, centerY:0 },
    contentView:SC.View.extend({
        childViews:"labl bar".w(),
        labl:SC.LabelView.design({
            layout:{top:10, centerX:0, width: 100, height:30},
            value:"_loading".loc()
        }),
        bar:SC.View.design({
            layout:{top:30, centerX:0, width:350, height:20},
            render:function (ctx, firstTime) {
                if (firstTime) {
                    ctx.push("<progress style="width: 100%\";"></progress>");
                }
                return ctx;
            },
            updateProgress:function (progress) {
                var bar = this.$("progress")[0];
                if(bar) {
                    bar.max=100;
                    bar.value = progress;
                }
            }
        })
    })
});

Note that to keep things simple I went with a progress HTML5 element. In browsers that do not support it (notably Safari) the users sees nothing but the loading labl. Implementation of a fallback strategy is left as an exercise to the reader ;-).
I'd also like you to note the updateProgress function which by use of a jquery selector grabs the progress element and updates its value. This function is not part of any SC specification and expressly violates the principle of not manipulating the DOM directly.

The counter is very much implementation dependent: one quick and dirty solution could be to to hook it up to SC.Request.manager and count inflight+pending down to 0, but it might not work because it also depends on the RunLoop which, remember, we're trying to do without. In the specific case that sparked this post requests were fired from a third party library and could be counted down by using a store-local variable decremented by a callback.

Whenever the counter is increased/decreased (depends if it's counting down or up) the callback must also update the view. Again we cannot rely on KVO and must explicitly invoke the updateProgress function which we added to our custom view just for this purpose.
The code at controller, or statechart level, could look something like this:

updateProgress: function(progress) {
   App.progressPane.contentView.bar.updateProgress(progress);
},

Final touch
In the last snippet you might have noticed the ugly hardcoded path coded into the controller: smells like bad code.
Looks like a perfect case for using SC.outlet. The PanelPane gets a new property:

App.progressPane = SC.PanelPane.create({
    progressbar: SC.outlet("contentView.bar"),
    layout:{ width:400, height:60, centerX:0, centerY:0 },
    contentView:SC.View.extend({

And the function then becomes:

updateProgress: function(progress) {
   App.progressPane.get("progressbar").updateProgress(progress);
},