Programming: Closure Tutorial: Displaying Friendfeed Items

I recently looked at some simple JS code on my home page. The code JSON-ically fetches Friendfeed data, then displays that data as little bits of text and links.

A few days later, Google open-sourced Closure Tools, a set of tools, library code, and templating stuff for working with Javascript. So then I thought: how would I re-work my simple homepage code using Closure? I wouldn't really, of course. That code is pretty simple. Closure can keep a medium-sized project from growing into a monster, but my home page's JS doesn't really benefit much from Closure.

On the other hand, simple code makes for an easier-to-understand example, so then I went ahead and did it anyhow.

Development Environment

I downloaded the Closure Compiler, the optimizing JS compiler. I didn't need it as I developed the software. But without it, my code would have been a half-meg.

I got a read-only copy of the Closure Library, a big pile of platform code.

I set up the compiler in one directory, the library in another, and my stuff in yet another. Soon I had things set up so I could edit/compile/test.

My JS file was hello.js. For testing, I didn't do a full, optimized compile. I just made sure that I "linked" my code with the code it depended on. I used calcdeps.py from the Closure Library:

$ python ../closure-library-read-only/closure/bin/calcdeps.py \
  -i hello.js -p ../closure-library-read-only/ -o script > \
  hello-bundle.js

Running that produced a JS file, a "bundle" of my code along with the code it depended on. I created a HTML file to load this file:

<html>
  <head>
    <script src="hello-bundle.js"></script>
  </head>
  <body>
   <h1>Hello World</h1>
   <div id="putff"></div>

   <script>FFFetch();</script>
  </body>
</html>

The <div id="putff"></div> isn't a Closure thing. That's just a named div that my code uses to figure out where to display its text. The <script>FFFetch();</script> isn't a Closure thing either--except that it's a JS function, and I was going to use some Closure library code to implement that function.

I load this page up into Firefox, perform certain superstitious rituals over the Firebug icon, and I'm ready to dive in.

Includes, Includables, Imports, Exports, ...

goog.provide('lahosken.ffhello.send');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.net.Jsonp');

That first piece of code sure is cryptic. When I say goog.provide('lahosken.ffhello.send');, I say that my code "exports" something called lahosken.ffhello.send that other code can "import". If some other code wanted to import my code as a library, this would let them do it. (That's not likely--I didn't really set up this code with a nice library-ish API. So... let's say we did it for the sake of sample code.)

The next few lines, the goog.require are "imports". More precisely, they declare which other pieces of code my code depends on. I need several pieces of library code from the Closure Library: goog.array, goog.dom, goog.events, goog.events.EventType, goog.net.Jsonp.

They have "goog" at the start of their names because they're from Google. My code's name doesn't start with "goog" because then people might get it mixed up with Google's code. I used my name; if I was working for Yoyodyne, I might have used "yoyo" instead.

Weird Function Definition

The next piece of code is a strange way to define a function:

lahosken.ffhello.send = function() {
   ...
};

That goog.provide('lahosken.ffhello.send') I wrote earlier defined a lahosken.ffhello namespace. That is, it defined an object named lahosken which had a field lahosken.ffhello which could have other fields. We just set one of these to be a function.

This namespacing is handy if you're on a big project with a bunch of people, when you're worried that you're going to accidentally clobber each other's function names. For this project, it's not so useful. I did it anyhow, out of force of habi^W^W^W a wish to illustrate this technique which you will encounter when looking over the Closure Library code.

Making a JSON Request

lahosken.ffhello.send = function() {
  var jsonp = new goog.net.Jsonp(
    'http://friendfeed.com/api/feed/user/lahosken');
  var payload = { 'format': 'json' };
  jsonp.send(payload, 
             lahosken.ffhello.handleResponse_, 
             lahosken.ffhello.handleError_);
};

The Closure Library's goog.net.Jsonp API is a pretty handy way to send+handle a Json request. There's even some sample code in the source comments. Skim the source.

My code sends off a request and uses two callback functions: one to handle a successful response, and one to handle errors.

Creating DOM

The Closure Library has classes that let you create cool GUI-ish things like buttons and menus. It's a great reason to use the Closure Library. This code doesn't do that, since I was just being lazy and copying the simple functionality from my home page. My code just creates some simple dom: <p> elements and <a> elements and such.

So here's an example of using goog.dom.createDom and goog.dom.appendChild to slap together some DOM. Sorry about the if/else logic.

lahosken.ffhello.createDomlet_ = function(entry) {
  var p = goog.dom.createDom('p');
  if (entry.service.id == 'twitter') { 
    if (entry.title[0] == '@') { return null; }
    goog.dom.appendChild(p, goog.dom.createTextNode(entry.title + ' '));
    goog.dom.appendChild(p, 
    goog.dom.createDom('a', {'href': 'http://twitter.com/lahosken'}, '>'));
  } else if (entry.service.id == 'blog') {
    goog.dom.appendChild(p, goog.dom.createTextNode(entry.title + ' '));
    goog.dom.appendChild(p, 
    goog.dom.createDom('a', {'href': entry.link}, '>'));
  } else if (entry.service.id == 'googlereader') {
    goog.dom.appendChild(p, goog.dom.createDom('b', {}, 'Link: '));
    goog.dom.appendChild(p, goog.dom.createTextNode(entry.title + ' '));
    goog.array.forEach(entry.comments, function(comment) {
      if (comment.user && comment.user.nickname && comment.body &&
          comment.user.nickname == 'lahosken') {
        goog.dom.appendChild(p, goog.dom.createDom('b', {}, 
                                                   comment.body + ' '));
      }});
    goog.dom.appendChild(p, goog.dom.createDom('a', {'href': entry.link}, '>'));
  } else {
    goog.dom.appendChild(p, goog.dom.createTextNode(entry.title + ' ')); 
    goog.dom.appendChild(p, 
          goog.dom.createDom('a', {'href': entry.link}, '>'));
  }
  return p;
};

OK, that was kind of a swamp. You might have overlooked one cool thing:

    goog.array.forEach(entry.comments, function(comment) {
      if (comment.user && comment.user.nickname && comment.body &&
          comment.user.nickname == 'lahosken') {
        goog.dom.appendChild(p, goog.dom.createDom('b', {}, 
                                                   comment.body + ' '));
      }});

Thanks to browser incompatibilities, you might not be able to use "foreach" whenever you'd like. goog.array.forEach works though, and saves you the hassle of rolling your own for loop.

More Array Fun and Functions

You recall next function, lahosken.ffhello.handleResponse_ was one of the callback functions that we passed to jsonp.send. The Jsonp API calls our handleResponse function to handle a successful response from Friendfeed.

lahosken.ffhello.handleResponse_ = function(response) {
    if (!response || !response.entries || !response.entries.length) { return; }
    var put = goog.dom.getElement('putff'); // find place to insert DOM
    if (!put) { return; } // if not found, give up

    // for each entry, create a "domlet", a little bit of DOM, or null, 
    //                 if there was nothing useful.
    var domlets = goog.array.map(response.entries, 
				 lahosken.ffhello.createDomlet_);

    // add the domlets to the document:
    goog.array.reduce(domlets, function(parent, domlet) {
      if (domlet) { goog.dom.appendChild(parent, domlet); }
      return parent;
    }, put);
};

goog.dom.getElement('putff') finds the DOM element with that ID, which we set up in our HTML page.

Next we have another array-looping function:

    var domlets = goog.array.map(response.entries, 
				 lahosken.ffhello.createDomlet_);

goog.array.map is an array-looping function that takes an array parameter and a callback function. map calls the callback on each array item. The callback function should return something. Finally, map returns an array of the callback functions returned values. It's pretty handy. And each time you use it with a simple callback function, Hal Abelson cheers up a little. Unfortunately, here we're using it with createDomlet_, our overly-complicated function that creates some <p> DOM when passed in a Friendfeed item entry... but you can't have everything.

    goog.array.reduce(domlets, function(parent, domlet) {
      if (domlet) { goog.dom.appendChild(parent, domlet); }
      return parent;
    }, put);

goog.array.reduce is another array-looping function. It accumulates the array items into one big item using a callback function. Here, our callback function "accumulates" our <p> DOMs by inserting them into the document.

Yet Another Callback

It's so cool that the Jsonp API lets us specify a callback to handle errors that it seems a pity to waste that ability on something random, and yet here we are:

lahosken.ffhello.handleError_ = function(payload) {
    if (Math.random() > 0.5) {
        return;
    }
    lahosken.ffhello.send_();
};

Exporting to the Page

This line is mysterious (and not even useful unless we do an optimized compile on our code):

goog.exportSymbol('FFFetch', lahosken.ffhello.send);

When we fully compile our code, the compiler will shorten our long-winded-but-descriptive names like "lahosken.ffhello.send" into shorter but not-so-clear names like "g". And each time you compile, that "g" might turn out to be something else, like "h".

This goog.exportSymbol defines FFFetch to be another name for lahosken.ffhello.send. And even if the compiler mangles the name lahosken.ffhello.send to something shorter, FFFetch will still work.

Oh Hey I'm Out of Code

If you have questions/comments/criticism of this code, let me know. I'm sure it's not perfect; I knocked it off pretty casually. Badness lurks herein; example code should be exemplary.

If you have questions about Closure in general, the closure-library-discuss group contains many people who are much smarter than I am.

[^]

comment? | | home |