Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with:

Applying A Cached Response To An AngularJS Resource

Posted by Ben Nadel

Earlier this week, I demonstrated my AngularJS class, DeferredWithUpdate.js, as a way to apply cached responses to an AngularJS deferred value. As a follow up to that, I wanted to quickly demonstrate how to apply a cached response to an AngularJS $resource reference.


 
 
 

 
  
 
 
 

The AngularJS $resource module provides an abstraction for communicating with a remote API. Basically, you define what kind of actions can be performed on a remote resource and what kind of data is expected to be returned and AngularJS takes care of everything else.

This is nice; but, what's really cool is how AngularJS handles the return value. Unlike a Deferred / Promise value, the $resource module returns Array and Object references. At first, these objects are empty. But, when the server returns with data, AngularJS subsequently "hydrates" these empty objects with the deserialized data. This allows the $resource response to be injected into the Controller's $scope before the server has responded.

In this demo, I wanted to take a look at applying a cached response to these naked Array and Object $resource responses without messing up the object references. To do this, I've created a service object that encapsulates my $resource instance and injects cached values before returning the $resource:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  • <title>Applying A Cached Response To An AngularJS Resource</title>
  • </head>
  • <body ng-controller="ListController">
  •  
  • <h1>
  • Applying A Cached Response To An AngularJS Resource
  • </h1>
  •  
  • <p>
  • You have {{ friends.length }} friend(s).
  • </p>
  •  
  • <ul>
  • <li ng-repeat="friend in friends">
  • {{ friend.id }} : {{ friend.name }}
  • </li>
  • </ul>
  •  
  •  
  • <!--Load AngularJS and the Resource module. -->
  • <script type="text/javascript" src="./angular.min.js"></script>
  • <script type="text/javascript" src="./angular-resource.min.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var Demo = angular.module( "Demo", [ "ngResource" ] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the demo UI.
  • Demo.controller(
  • "ListController",
  • function( $scope, friendService ) {
  •  
  • // Get the list of friends from the server. This
  • // returns an AngularJS resource which can be injected
  • // directly into the scope.
  • $scope.friends = friendService.query();
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I provide the access to the API.
  • Demo.service(
  • "friendService",
  • function( $resource, applyCacheToResource ) {
  •  
  • // Define our AngularJS resource (which makes the
  • // HTTP requests to our server for us).
  • var resource = $resource( "./api.cfm" );
  •  
  • // Imagine that we have some locally cached data that
  • // we've stored from a previous request.
  • var cachedResponse = [
  • {
  • id: 3,
  • name: "Joanna"
  • }
  • ];
  •  
  •  
  • // Provide an API for the controllers.
  • this.query = function() {
  •  
  • // Get the resource reference (at this point,
  • // it is an empty array or object reference).
  • var results = resource.query();
  •  
  • // Before we return the resource, let's inject
  • // our own cache. Since the Resource *always*
  • // updates on the next "tick", we know that we
  • // are not going to corrupt the true response
  • // from the server.
  • return(
  • applyCacheToResource( results, cachedResponse )
  • );
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I apply a cached response to an existing resource without
  • // breaking the original resource reference.
  • Demo.value(
  • "applyCacheToResource",
  • function( resource, cache ) {
  •  
  • // Check to see what type of value we're dealing with.
  • // If it's an array, we want to splice-in the cache;
  • // if it's an object, we want to extend the keys.
  • if ( angular.isArray( resource ) ) {
  •  
  • resource.splice.apply(
  • resource,
  • [ 0, 0 ].concat( cache )
  • );
  •  
  • } else {
  •  
  • angular.extend( resource, cache );
  •  
  • }
  •  
  • // Return the updated resource (for easy of use).
  • return( resource );
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, I have an array of friends that is getting rendered in the main UI (user interface). The friend data is being retrieved from the server-side API using an AngularJS resource. My data access layer - friendService - applies a cached value to the response before returning it to the controller.

The cached value is being applied to the $resource with another helper method: applyCacheToResource(). In order to maintain the correct object references, the cached value is being used to "hydrate" - rather than replace - the $resource response. This allows (potentially dirty) data to be rendered early without messing up the normal $resource request lifecycle.




Reader Comments

I have a question about:

resource.splice.apply(resource,[0,0].concat(cache));

It's not clear to my exactly why you are using
"[0,0].concat(cache)" instead of "resource.concat(cache)"

Reply to this Comment

Please let me rephrase:

It's not clear to my exactly why you are using
"[0,0].concat(cache)" instead of
"[].concat(cache)" or
"[0].concat(cache)" or something else.

Am I missing something?

Reply to this Comment

@Peter,

No problem.

Since other people may have the same question, I'll just expand on the question:

I can't use concat() because it returns a new array; and, I need to inject the cached content into the existing array reference. The splice() method, on the other hand, can augment/mutate an array in-place, which is what I need to do.

Now, in core JavaScript, the .splice() method has the signature:

splice( index, deleteCount [ insert1, insert2 ... insertN ] )

I don't want to delete any values - I want to to inject my cached collection, which would be the:

[ insert1, insert2 ... insertN ]

... part of the signature above. Now, in order to get the signature correct, I also need to supply the "index" and "deleteCount" part of the signature.

That's why I use .concat() to build the "signature" arguments:

[ 0, 0 ].concat( cache ) ==> [ 0, 0, cache1, cache2, ... cacheN ]

Once I have the right arguments, I can then use the .apply() method to use those as the invocation arguments for the .splice() method.

Hope that helps for anyone else.

Reply to this Comment

Ben,
This is a great example and coming in very handy, however, I do have a question.

In my current code, I'm doing something like this:
$scope.barriers = Barrier.query(function() {
//success
}, function() {
//fail
});

How can I get a promise returned when the resource data has been loaded so I know that I have all the data?

Reply to this Comment

Why do you use resource.splice.apply(resource, ....) not just resource.splice(...) ?

Reply to this Comment

If there's an updated data with id = 3 in the response, then there will be two objects with id = 3, am I right?

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.