Stratified
JavaScript (SJS) might still be work in progress, but that doesn't mean
it can't be used for building some (semi-)useful things.
Consider e.g.
the 'FeedChumby' RSS feed reader which automatically cycles through the posts
of some popular RSS feeds, 'slide-show' style - similar to the way in which a Chumby cycles through its widget
stream.
In this blog post I will dissect the code for this webapp,
tutorial-style.
Accessing feeds
At its core, FeedChumby uses the Google AJAX Feeds API
which allows us to retrieve RSS feeds without having to worry about
any crossdomain issues and without requiring any server support from
our side.
SJS gives us access to this API through the unassuming function
Ajax.Google.Feeds.loadFeed(url);
which returns a JSON results object as described in the Google documentation.
Because we're using SJS, we can call this function synchronously - no
callback or other asynchronous construct needed. All the asynchronous
action is automatically choreographed behind the scenes: loading the
Google API if it hasn't been loaded yet, making sure the 'feeds'
module is loaded, and finally loading the actual feed itself. While
all of this is going on, the browser stays perfectly responsive.
Coupled with SJS's 'Ajax.DOM.waitForEvent' library function (which -
as one would expect from the name - waits for a particular DOM event),
loading and displaying a feed in response to a button click is then as
easy as this:
Ajax.DOM.waitForEvent("click", button);
var result = Ajax.Google.Feeds.loadFeed(feed_url);
renderResult(result);
Here is a complete example: View
Source | Run
Basic program structure
By sticking 'loadFeed' into a loop, iterating over a few different
feeds and over individual feed posts, and pausing for a few seconds
after each post is displayed (using SJS's "hold()" primitive), we've
got the basic structure for our Chumby-style feed reader - again no
asynchronous constructs (like e.g. setTimeout) needed:
var feeds = [ "...slashdot...", "...digg...", "...elreg...", ... ];
while (true) {
for (var i=0; i<feeds.length; ++i) {
var result = Ajax.Google.Feeds.loadFeed(feed[i]);
for (var j=0; j<result.feed.entries.length; ++j) {
renderFeedEntry(result.feed.entries[j]);
hold(10000); // <-- wait 10s before continuing with next post
}
}
}
Let's also add a button that allows the user to shortcut through the
10s waiting time. The easiest way to provide this sort of
functionality is by using SJS's '@' combinator (as described in an earlier blog post, "A @ B" executes both A
and B in parallel, and returns as soon as either of them have finished
evaluating; at the same time cancelling the still pending expression):
for (...) {
...
hold(10000) @ Ajax.DOM.waitForEvent("click", nextButton);
}
As a little refinement, we can replace the 'hold(1000)' by code that
will provide the user with some UI feedback during the waiting period:
function countdown() {
for (var i=0; i>0; --i) {
counter.innerHTML = i;
hold(1000);
}
}
...
for (...) {
...
countdown() @ Ajax.DOM.waitForEvent("click", nextButton);
}
Here is a complete example with the functionality so far: View Source | Run
Adding 'pause' functionality
Again using '@' and 'Ajax.DOM.waitForEvent()', we can modify our
countdown() function to stop counting down while in a paused state:
var paused = false;
function countdown() {
var i=10;
while (i>0) {
counter.innerHTML = i;
if (paused) {
pauseButton.innerHTML = "Play";
Ajax.DOM.waitForEvent("click", pauseButton);
paused = false;
pauseButton.innerHTML = "Pause";
}
(hold(1000), --i) @
(Ajax.DOM.waitForEvent("click", pauseButton), paused=true);
}
}
Note the '@' expression here. It combines the two simpler expressions
"(hold(1000), --i)", which waits for 1s and then decrements 'i', and
"(Ajax.DOM.waitForEvent("click", pauseButton), paused=true)", which
waits for a click on 'pauseButton' and then sets the variable 'paused'
to true. (In both 'normal' JavaScript and also SJS, for two expressions
A and B, "A,B" is an expression that means "execute first A and then
B".)
Here is the example with the pause functionality rolled in: View Source | Run
Adding birectional navigation
To add functionality that allows the user to navigate back to the
previous post, we need to change our loop logic a little bit to
account for the situtation where we're moving past the beginning of a
feed to the previous feed. The gory details are here: View Source | Run.
Note the '@' expression in the main loop:
(countdown(), ++j) @
(Ajax.DOM.waitForEvent("click", "next"), ++j) @
(Ajax.DOM.waitForEvent("click", "previous"), --j);
Previously we only waited for the countdown to finish or for the user
to click on the 'next' button. Now we're also waiting for clicks on
the 'previous' button, and we're incrementing/decrementing variable
'j' to communicate the direction of navigation.
More embellishments
The absence of any asynchronous constructs (callbacks, setTimeouts,
...) makes the code quite modular. It's easy to add little features
here and there without having to take a "global view" of the
application, and the '@' combinator relieves us from a great deal of
manual bookkeeping.
The final version of the FeedChumby adds a couple more small touches,
like e.g. an animated pause button, and it contains some CSS to make
the application more presentable:
View Source | Run
I think FeedChumby is a pretty good example of the power of SJS for writing
Ajax-style applications. The real win, however, will only show in big
'mashup' style applications that access several different APIs
concurrently. Expect to see more demos coming along with APIs such as
the Google Translation API, Google Contacts, Facebook Connect, and others.