SproutCore 1.0: SC.Enumerable and SC.Array

One of the trickier aspects of making SproutCore DOM-library independent is dealing with enhancements these libraries make to built-in JavaScript objects.

For example, Prototype adds some very useful iterator methods - such as uniq() and compact() - to Array.prototype that we use frequently.  These helpers reduce our code size and make it easier to read.  On the other hand, depending on them limits your ability to use other libraries.  What if you want to load a library that defines these methods in a conflicting way?   For example, the way each() (or forEach() in the JavaScript standard) is implemented by Prototype, jQuery, and built-in by Firefox each pass different parameters to your callback.  Not pretty.

Some libraries, like jQuery, avoid modifying built-in objects altogether to work around this problem.  I didn’t think we needed to be that dogmatic about it, because some of these are so useful.  So I decided on a few simple rules:

  1. Never, ever, modify Object.prototype.  This breaks too many things.
  2. Many of the more useful enhancements that libraries add, such as forEach(), map(), and reduce() on Arrays, are actually part of the new JavaScript standard but just unevenly implemented.  SproutCore will provide implementations of these on all browsers that do not have them consistent with the standard.  You can basically use any of the iterators described in JavaScript 1.8 on any browser with SproutCore.
  3. A few of the more useful helpers defined by Prototype, such as compact() and uniq(), which have common, unambiguous functions, are provided by SproutCore as well.  They are implemented in the same way as Prototype so that if you do not want to.
  4. We will also enhance Array and String as needed to support property observing and localization.  For these methods, we try to name them in a way that is unlikely to conflict with other libraries.

While I was at it, I decided to make these enhancements available as mixins as well.  The SC.Enumerable and SC.Array mixins in SproutCore provide our uniform enumeration API.  They are applied automatically to Array, but you can apply them to any other object as well to make it “array-like”.   This opens up lots of possibilities for creating sets, sparse arrays, and other complex collection-oriented data structures that you can essentially pass in anywhere you might normally expect an Array.

Overall, I’m pretty happy with the results.  SC.Enumerable implements common iterators such as forEach(), map(), filter(), every(), some() and reduce().  These methods are all implemented natively in Firefox and some are in WebKit.  For all other browsers, SproutCore provides a generic implementation for you.  In addition, we’ve come up with some really useful extra features, such as:

SC.Enumerator

Sometimes you don’t want to just iterate through an array.  Instead, it is easier to pass around an “iterator” that can simply return the next value until you are done.  For example, let’s say you are going to process a bunch of data to produce some graph results.  This would potentially lock up the browser if you have a large data set.  It would be better to schedule a timer and then process them one or two at a time.  Here is how you could do this:

function processData(array) {
  var enumerator = array.enumerator(); // get an SC.Enumerator instance
  var process = function() {
    var next = enumerator.nextObject() ;
    if (next) {
      processItem(next);
      process.invokeLater(); // schedule to fire again
    } else process = enumerator = null; // cleanup closure
  };
  process(); // start processing
} ;

processData(aBigArrayOfData);

The nice thing about this is the enumerator will work even if you edit the array of data, whereas keeping a simple index would be more difficult to keep in line.  Note how you can call nextObject() on the enumerator until it reaches the end of the array, afterwhich it will return undefined.

Content Observing

One other benefit SC.Enumerable brings is it makes your array observable.  All arrays have a ‘[]‘ property which you can observe to be notified whenever the membership of the array changes.  For this to work, you must use the SproutCore-specific API for manipulating the array instead of the native methods, but this is cheap and yields huge wins.  Here is a banking example:

MyApp.account = SC.Object.create({
  ledger: [],

  ledgerDidChange: function() {
    console.log('account ledger changed!');
  }.observes('.ledger.[]')
});

With this controller, you could now insert or remove items like so:

MyApp.account.ledger.pushObject(newDeposit);
> "account ledger changed!"

MyApp.account.ledger.popObject();  // this is probably stealing. :-)
> "account ledger changed!"

MyApp.controller.records.insertAt(3, newWithdrawl);
> "account ledger changed!"

This makes it incredibly easy to observe changes on an array when you care about the member of the array itself.

Reduced Properties

Of course, we didn’t stop there.  Observing when an array value has changed is nice, but often times you don’t care about the array itself, only a summary of its contents.  For example, consider the ledger above.  Maybe what you really care about are not the specific transactions, but the sum total of the account.    You can get this with something called a “reduced property”.  A reduced property is simply a value that is computed dynamically by evaluating all of the items in an array and combining their results.  You can write your own reduced properties, but SproutCore comes with a couple of useful ones built in as well.

To get the value of a reduced property, you just use get() on the array with the property name.  All reduced properties begin with an @ symbol and may contain a parameter identifying a specific property on the array contents you are interested in.  For example, assuming all of the objects in the ledger above have an “amount” property that tells you their value, you could get a sum of the transactions with:

MyApp.account.ledger.get('@sum(amount)');
> (returns total amount)

Best of all, reduced properties are observable as well.  So you can bind to them.  If you wanted to show this account balance in a label view, for example, you could just bind the “value” property to "MyApp.account.ledger.@sum(amount)".

And so much more…

SC.Enumerable opens up a wide array (no pun intended) of possibilities for working with collections of objects in SproutCore.  Best of all, these features are available TODAY in the SproutCore version you probably have installed on your machine.  Checkout the SC.Enumerable and SC.Array mixins today.  They both get a minor facelift for 1.0, but for the most part they work now.  Get to know them.

Discussion Area - Leave a Comment