Skip to main content

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);
},

Comments

Popular posts from this blog

Mirth: recover space when mirthdb grows out of control

I was recently asked to recover a mirth instance whose embedded database had grown to fill all available space so this is just a note-to-self kind of post. Btw: the recovery, depending on db size and disk speed, is going to take long. The problem A 1.8 Mirth Connect instance was started, then forgotten (well neglected, actually). The user also forgot to setup pruning so the messages filled the embedded Derby database until it grew to fill all the available space on the disk. The SO is linux. The solution First of all: free some disk space so that the database can be started in embedded mode from the cli. You can also copy the whole mirth install to another server if you cannot free space. Depending on db size you will need a corresponding amount of space: in my case a 5GB db required around 2GB to start, process logs and then store the temp files during shrinking. Then open a shell as the user that mirth runs as (you're not running it as root, are you?) and cd in

From 0 to ZFS replication in 5m with syncoid

The ZFS filesystem has many features that once you try them you can never go back. One of the lesser known is probably the support for replicating a zfs filesystem by sending the changes over the network with zfs send/receive. Technically the filesystem changes don't even need to be sent over a network: you could as well dump them on a removable disk, then receive  from the same removable disk.

How to automatically import a ZFS pool built on top of iSCSI devices with systemd

When using ZFS on top of iSCSI devices one needs to deal with the fact that iSCSI devices usually appear late in the boot process. ZFS on the other hand is loaded early and the iSCSI devices are not present at the time ZFS scans available devices for pools to import. This means that not all ZFS pools might be imported after the system has completed boot, even if the underlying devices are present and functional. A quick and dirty solution would be to run  zpool import <poolname> after boot, either manually or from cron. A better, more elegant solution is instead to hook into systemd events and trigger zpool import as soon as the devices are created.