CouchCoder

Search


Latest Posts


Recent Comments


July 2017
M T W T F S S
« Apr    
 12
3456789
10111213141516
17181920212223
24252627282930
31  
CouchCoder

Containers for Promises, and DRY Controllers

An idea on how to create fluid and slim controllers that are easy to maintain, DRY, and succinct, by wrapping and tracking them in container objects.

MerottMerott

The Problem

In AngularJS and most other SPA frameworks, it’s common for controllers to fetch data from asynchronous services. This is often handled using promises, along with some sort of a boolean flag such as a loading property to indicate that data loading is in progress. We may also have an error property to hold any error information.

Our common controller looks like this:

class CommonController {
  constructor(blogService) {
    this.blogService = blogService;
    this.refresh();
  }

  refresh() {
    this.comments = null;
    this.error = null;
    this.pending = true;

    this.blogService.getComments().then((comments) => {
      this.comments = comments;
    }, (error) => {
      this.error = error;
    }).finally(() => {
      this.pending = false;
    });
  }
}

And here is a simple template:

<div ng-controller="CommonController as common">
  <div ng-if="common.pending">Loading...</div>
  <div ng-if="common.comments">{{common.comments}}</div>
  <div ng-if="common.error">{{common.error}}</div>
</div>

That’s pretty standard, right? Well, I think there is far too much boilerplate code, and it only gets worse when you have more than one or two sets of asynchronous data to fetch. I like to have my controllers slim, succinct, and free of so many inline anonymous functions being passed around, so there must be a better way of doing this.

A Solution

We can simplify our controller logic by extracting promise handling to some sort of a container class that can be reused across controllers. I’m going to create an Awaited class, which accepts a promise in its constructor function as follows:

class Awaited {
  constructor(promise) {
    this.promise = promise;
    this.pending = true;

    promise.then((result) => {
      this.value = result;
    }, (error) => {
      this.error = error;
    }).finally(() => {
      this.pending = false;
    });
  }
}

Using the above Awaited class, our controller can be simplified to this:

class BetterController {
  constructor(blogService) {
    this.blogService = blogService;
    this.refresh();
  }

  refresh() {
    this.comments = new Awaited(this.blogService.getComments());
  }
}

We have just reduced 10 lines of refresh() code to a one-liner. Our template looks only slightly more verbose, but I think it actually makes it easier to read, especially if we have to fetch multiple values from asynchronous services:

<div ng-controller="BetterController as better">
    <div ng-if="better.comments.pending">Loading...</div>
    <div ng-if="better.comments.value">{{better.comments.value}}</div>
    <div ng-if="better.comments.error">{{better.comments.error}}</div>
</div>

Our Awaited object could do even more, such as keeping track of the latest status of the promise by registering a 3rd callback in the then call. Our controller does not change at all, but we could access the status property in our template to provide status updates.

class Awaited {
  constructor(promise) {
    this.promise = promise;
    this.pending = true;

    promise.then((result) => {
      this.value = result;
    }, (error) => {
      this.error = error;
    }, (status) => {
      this.status = status;
    }).finally(() => {
      this.pending = false;
    });
  }
}

An improvement idea

The above is already an achievement, but we could do something to make the API even more succinct and fluid. If we have access to the Promise prototype, we could augment our promise objects with a function that will return a Awaited object.

AngularJS allows augmenting of its built-in services using decorators. Unfortunately though, the AngularJS $q implementation of Promise does not expose its prototype, and hence it’s not possible to augment it, at least not without a workaround that might be considered a dirty hack. I’m still going to show you how…

We can use Object.getPrototypeOf() inside of a decorator for $q, to get access to the Promise prototype via an instance of the $q promise:

function decoratePromise($delegate) {
  // this may feel like a clumsy workaround, but it does work, for now...
  let promisePrototype  = Object.getPrototypeOf($delegate.when());

  promisePrototype.await = function() {
    return new Awaited(this);
  };

  return $delegate;
}

angular.module('app', []).decorator('$q', decoratePromise);

Our awesome controller now looks like this:

class AwesomeController {
  constructor(blogService) {
    this.blogService = blogService;
    this.refresh();
  }

  refresh() {
    this.comments = this.blogService.getComments().await();
  }
}

As I said, it might be a bit clumsy the way we’re augmenting Promise, but it’s the cleanest way I found. If you have better suggestions, I’d love to read them in the comments below!

I'm a web developer who enjoys coding on the couch as much as I enjoy developing applications as a profession. I owe much of my success as a developer to the very people who blog and share, and this is my attempt to give something back to the community. I'm an advocate of writing clean, concise, well-structured code, and I also love to experiment with new technologies, trying to get my hands dirty with anything and everything.

Comments 0
There are currently no comments.