Extending ngResource To Access Metadata

By · Published · javascript, angularjs, howto

AngularJS's built-in ngResource is a great tool for natively supporting REST APIs in your Angular application. But what happens when you need to support something besides a simple call that retrieves a list of JSON objects? You quickly run into the limits of ngResource.

Here's a great case where you might need to do something more complex: paging. Say you want to get a list of objects, and there's 10,000 or so of them. You don't want to send 10,000 objects to your frontend app. You want to send a portion of them, but you still need to indicate to the app that there are more.

Surprisingly, considering how widespread this pattern is in web development, there does not seem to be a native way to accomplish this. But you can extend ngResource. Here's how I did it.

From The Backend

There is some debate on what the proper way to handle paging is for RESTful endpoints. Some people say you should send it as a header. Other say you should send it as part of the data return. I chose the latter. Here's an example of a return I might implement:

{
    "success": true,
    "found": 1500,
    "returned": 30,
    "data": [{...}, {...}, ...]
}

As you can see here, the data is returned in a wrapper structure that indicates how many matches were found and and how many are returned and that we're only returning a subset. So the frontend knows that it can page the results.

Intercepting The Response

The next thing I did was implement an $httpProvider transformer to transform the response.

angular
    .config(['$httpProvider', function($httpProvider) {
        $httpProvider.defaults.transformResponse.push(function(data, headerGetter) {
            try {
                var wrappedResult = angular.fromJson(data);
            } catch (e) {
                return data
            }

            if (wrappedResult.found && wrappedResult.returned && wrappedResult.data) {
                wrappedResult.data.$found = wrappedResult.found;
                wrappedResult.data.$returned = wrappedResult.returned;
                return wrappedResult.data;
            }

            return wrappedResult;
        });
    }]);

What this does is pull out the values and add them (temporarily) to the data array before passing that on ngResource. The try/catch block is to check to be sure we actually got a valid JSON response and not, say, a template or something. And if it's not a JSON structure in a format we recognize (like an endpoint that hasn't been converted to the new structure) it just passes the full data along.

The nice thing about doing this with an transformer at the $http level is that this logic now lives here instead of in your models.

Extending ngResource

So now you have the $found and $returned being passed along to ngResource, but ngResource is still discarding it. Here's where the magic happens. We create a wrapper provider around ngResource that can pull these variables out and make them available.

angular
    .provider('$ourResource', function() {
        this.$get = ['$resource', function($resource) {

            this.defaults = {
                actions: {
                    'get': {
                        method: 'GET'
                    },
                    'save': {
                        method: 'POST'
                    },
                    'query': {
                        method: 'GET',
                        isArray: true
                    },
                    'remove': {
                        method: 'DELETE'
                    },
                    'delete': {
                        method: 'DELETE'
                    }
                }
            };

            var provider = this;

            return function(url, paramDefaults, actions, options) {
                if (provider.defaults.actions) {
                    actions = angular.extend({}, provider.defaults.actions, actions);
                }

                angular.forEach(actions, function(action) {
                    action.interceptor = {
                        response: function(response) {

                            angular.forEach(['$found', '$returned'], function(key) {
                                if (response.data[key]) {
                                    response.resource[key] = response.data[key];
                                }
                            });

                            return response.resource;
                        }
                    }
                });

                var resource = $resource(url, paramDefaults, actions, options);

                return resource;
            };
        }];
    });

Basically what this does is extend $resource to add a interceptor for each action that pulls the variables from the data collection and puts them on the resource itself.

With this, you now have $found and $returned as properties on the resource collection returned. Instead of calling $resource(...) in your factories, you now call $ourResource(...) instead. It's a 1:1 replacement since $ourResource wraps and extends $resource.

Now, with that done, you can do something like this in your views:

Found {{ items.$found }}. Showing {{ items.$returned }}

<div ng-repeat="item in items">
    ...
</div>

Theoretically you could even implement the entire thing in your $ourResource wrapper by implementing transformResponse in the inner angular.forEach loop. That might even be a better idea, but I like this approach.

Thanks goes to this JSFiddle that put me on the right path to solving this.

( Comments )

Did something I wrote help you out?

That's great! I don't earn any money from this site - I run no ads, sell no products and participate in no affiliate programs. I do this solely because it's fun; I enjoy writing and sharing what I learn.

All the same, if you found this article helpful and want to show your appreciation, here's my Amazon.com wishlist.


Related Posts

Creating a simple predicate builder with AngularJS

Multiple Calibre Servers under Mac OS X


comments powered by Disqus