Refactoring and Making Promises

After spending the last year writing Ember.js code I've realized one really big thing - Ember is about promises. I recently had a chance to refactor some code with a number of asynchrounous calls. The original logic used timers and polling to monitor the state of a bunch of recursive calls to the Google API to retrieve a complete folder structure. Timers get the job done, but promises make it elegant.

Original Code

Requirement: wait until the entire tree is loaded before transition.


App.GoogleFile = Ember.Object.extend({
});

App.ApplicationRoute = Ember.Route.extend({

  model: function(){
    var model = App.GoogleFile.create({
      id: $('meta[name="google-folder-id"]').attr('content');
      children: Ember.ArrayProxy.create({content: []})
    });
    this.loadChildren(model.get('children'));
    return model;
  },

  afterModel: function(transition){
    if(transition.targetName == "application.index"){
      this.wait(model.get('children'), function(){
        this.transitionTo('anotherPlace');
      });
    }
  },

  // Waiting for content to load using a timer.
  wait: function(children, callback){
    if(Ember.isEmpty(children)){
      Ember.run.later(this, function () {
        this.wait(children);
      }, 10);
    } else {
      callback(children);
    }
  },

  loadChildren: function(node){
    var token = $('meta[name="google-access-token"]').attr('content');
    var query = encodeURIComponent('"' + node.get('id') + '" in parents');
    $.get('https://www.googleapis.com/drive/v2/files?q=' + query + '&access_token=' + token, function(data){
      data.items.forEach(function(item){
        var f = App.GoogleFile.create({
          name: item.title,
          id: item.id,
          icon: item.iconLink,
          mime: item.mimeType,
          embed: item.embedLink,
          edit: item.alternateLink,
          children: Ember.ArrayProxy.create({content: []})
        });
        if(item.mimeType === "application/vnd.google-apps.folder"){
          this.loadChildren(f);
        }
        node.get('children').pushObject(f);
      }.bind(this));
    }.bind(this));
  }

});

The Refactored Code



App = Ember.Application.create({
});

App.GoogleFile = Ember.Object.extend({
});

App.ApplicationRoute = Ember.Route.extend({

  model: function(){
    var model = App.GoogleFile.create({
      id: $('meta[name="google-folder-id"]').attr('content');,
      children: Ember.ArrayProxy.create({content: []})
    });

    return new Ember.RSVP.Promise(function(resolve, reject){
      this.loadChildren(model, resolve, reject);
    }.bind(this));
  },

  // The afterModel won't fire until the promise is fullfilled.
  afterModel: function(transition){
    if(transition.targetName == "application.index"){
      this.transitionTo('anotherPlace');
    }
  },

  loadChildren: function(node, resolve, reject){
  var token = $('meta[name="google-access-token"]').attr('content');
    var query = encodeURIComponent('"' + node.get('id') + '" in parents');
    // Don't resolve the promise when the ajax call returns. We have to process the data and decide if we need to make more calls.
    $.get('https://www.googleapis.com/drive/v2/files?q=' + query + '&access_token=' + token, function(data){
      var promises = [];
      data.items.forEach(function(item){
        var f = App.GoogleFile.create({
          name: item.title,
          id: item.id,
          icon: item.iconLink,
          mime: item.mimeType,
          embed: item.embedLink,
          edit: item.alternateLink,
          children: Ember.ArrayProxy.create({content: []})
        });

        if(item.mimeType === "application/vnd.google-apps.folder"){
          // We need to make more ajax calls. Create a new promise which can be resposible for
          // resolving existing promises once it is fullfilled.
          var promise = new Ember.RSVP.Promise(function(resolve, reject){
            this.loadChildren(f, resolve, reject);
          }.bind(this));
          promises.push(promise);
        }
        node.get('children').pushObject(f);
      }.bind(this));

      Promise.all(promises).then(function(){
        resolve(node);
      });

    }.bind(this));
  }

});

Working refactored code snippet on jsbin