odoe

Keeping Promises

Blog Post created by odoe on Jun 17, 2015

esri-promises.jpg

 

Maybe you've heard of them, but weren't quite sure what they are. You've undoubtedly been using them in your ArcGIS API for JavaScript development this whole time and didn't even know it. Maybe they are the bane of your existence and increasing gray hairs. Love them or hate them, but learn how to use them. Promises, they flow like cracks in the wall.

 

You can read more about EcmaScript 2015 Promises here. Here is the spec, if you are so inclined to read it. When I say you've probably been working with them all this time and didn't even know, let's look at the Retrieve data from a web server guide in the ArcGIS JS API docs. See all the references to the stuff like doSomthing().then(function(){}). That's a Promise. The Promises used in the JS API are based on the Dojo Promise, which has been around longer than most Promise implementations. That is why it doesn't have all the methods defined above. But it works just as well. This Promise module is just the API for a Promise. In the ArcGIS API for JavaScript, we are typically dealing with Promises via dojo/Deferred. Some might say that Promises are the monads of asynchronous programming.

 

Hiding in plain sight

If you look throughout the ArcGIS JS API documentation, you will see that Deferred is the return type for plenty of methods. Just look at the methods on the map. But why? A Promise is used to work with some sort of asynchronous activity. It could be using the QueryTask to make some requests that could take a few milliseconds or a couple of seconds, it's a roll of the network dice sometimes. The point being is that if we didn't use a Promise implementation to handle these asynchronous requests, your application would spend most of it's time just sitting there, frozen in fear, waiting for responses. A Promise says, "look, I promise I'll be back, one way or another I'm coming back, but keep fighting the good fight. Do you what you have to do and just wait for my triumphant return!" It's a brave little worker.

 

While a Promise is working, once it has accomplished it's task, it will resolve with it's result or if something goes wrong, it will reject it and hopefully give you a reason. Thus, when you are the one wielding the power of a Promise and you find yourself using deffered.resolve(), you should also take care to figure out when you should use deferred.reject() and handle these errors.

 

You want to play a prank on your users? Zoom to the map on click, but then zoom back to where they were before they clicked. It will drive them nuts.

 

require(["esri/map", "dojo/domReady!"], function(Map) { 
  var map = new Map("map", {
    center: [-118, 34.5],
    zoom: 8,
    basemap: "topo"
  });
  map.on('click', function(e) {
    var pt = map.extent.getCenter();
    map.centerAndZoom(e.mapPoint, 12);
    map.centerAndZoom(pt, 8);
  });
});

 

Look at the sample on JSBIN.

 

Wait a second! That doesn't seem to work. It's not very consistent and not very funny. What's happening is that the centerAndZoom method is comprised of a smooth zoom, a gradual, almost animation like effect from one zoom level to the next. That's not an instantaneous action. Somewhere in there, you need to wait for the zoom to finish and then you can go back to where you were. That's why centerAndZoom returns a Deferred. This means we can rewrite the above like this.

 

require(["esri/map", "dojo/domReady!"], function(Map) { 
  var map = new Map("map", {
    center: [-118, 34.5],
    zoom: 8,
    basemap: "topo"
  });
  map.on('click', function(e) {
    var pt = map.extent.getCenter();
    map.centerAndZoom(e.mapPoint, 12).then(function() {
      map.centerAndZoom(pt, 8);
    });
  });
});

 

JSBIN here.

 

That is much better. And absolutely hilarious.

 

Chain it up

One thing you can o with Promises is chain the results. Let's look at this sample from the docs. It performs a query and displays information on the page. The bulk of the work is done here.

 

function execute () {
  query.text = dom.byId("stateName").value;
  queryTask.execute(query, showResults);
}
function showResults (results) {
  var resultItems = [];
  var resultCount = results.features.length;
  for (var i = 0; i < resultCount; i++) {
    var featureAttributes = results.features[i].attributes;
    for (var attr in featureAttributes) {
      resultItems.push("<b>" + attr + ":</b>  " + featureAttributes[attr] + "<br>");
    }
    resultItems.push("<br>");
  }
  dom.byId("info").innerHTML = resultItems.join("");
}

There's nothing wrong with that, but it could get a little tricky if you wanted to say, omit some attributes from being displayed or change the DOM elements being created. It just seems like an awful lot of work in little spot. Well, you can chain the Promise returned from a QueryTask like below.

 

function execute () {
  query.text = dom.byId("stateName").value;
  queryTask.execute(query).then(function(results) {
    // get the attributes
    return results.features.map(function(x) {
      return x.attributes;
    }).shift(); // since we know there is only one result, return first attribute
  }).then(function(attributes) {
    // Create the DOM strings
    return Object.keys(attributes).map(function(key) {
      return "<b>" + key + ":</b>  " + attributes[key] + "<br>";
    });
  }).then(function(x) {
    // Join the DOM strings
    return x.join("");
  }).then(function(elements) {
    // update the DOM
    dom.byId("info").innerHTML = elements;
  }).otherwise(function() {
    alert("Something went completely wrong");
  });
}

 

JSBIN here.

 

As you can see, as long as you keep returning a result in the functions used in the then method, you can chain them. For demonstration purposes, I chained it a little more than I normally would, but you can now easily distinguish what parts of the chain are doing what work. So you can add a new piece to the chain to do extra work or modify an existing chunk to fix it. Notice the otherwise method. This will capture any errors that occur. For example, try searching for CaliforniaFun and see what happens.

 

Fun fact. You can return a Promise in a Promise chain, so maybe inside a then function, you need to do some async requests, maybe merge with another data source, just return a Promise and continue the chain. Enjoy your magic functions. This is actually a more useful way of chaining Promises, to actually chain async requests. Check out this post from Sitepen for more details.

 

Done for now, I Promise

As you can see, Promises and their implementation in Deferred is pretty powerful. They are great tools for use with asynchronous tasks. One thing to remember is that Promises will execute right away. You just get to defer when the results get handled. If you want to try some other techniques that will defer execution until you are ready, you can try out RxJS or look at something like Folktales data.task which implements a Future monad. But I'll leave those goodies for you to explore.

 

For more geodev tips and tricks, check out my blog.

Outcomes