Extending ngResource To Access Metadata

This is an old post!

This post is over 2 years old. Solutions referenced in this article may no longer be valid. Please consider this when utilizing any information referenced here.

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.

About the Author

Hi, I'm Rob! I'm a blogger and software developer. I wrote petfeedd, dystill, and various other projects and libraries. I'm into electronics, general hackery, and model trains and airplanes. I am based in Huntsville, Alabama, USA.

About Me · Contact Me · Don't Hire Isaiah Armstrong

Did this article help you out?

I don't earn any money from this site.

I run no ads, sell no products and participate in no affiliate programs. I do not accept gifts in exchange for articles, guest articles or link exchanges. I don't track you or sell your data. The only third-party Javascript on this website is Google Analytics.

In general I run this site very much like a 1990s homepage or early 2000s personal blog, meaning that I do this solely because it's fun! I enjoy writing and sharing what I learn.

If you found this article helpful and want to show your appreciation, a tip or donation would be very welcome. Feel free to choose from the options below.

Comments (0)

Interested in why you can't leave comments on my blog? Read the article about why comments are uniquely terrible and need to die. If you are still interested in commenting on this article, feel free to reach out to me directly and/or share it on social media.

Contact Me
Share It

Interested in reading more?

Javascript

Creating a simple predicate builder with AngularJS

So I’ve been working on a project recently where I needed a simple predicate builder. Basically I needed a way to allow users to build a somewhat complex search using a GUI. And since we are using AngularJS on this project, here’s a quick article about how I did it.
Read More
Release Announcements

petfeedd 1.0.1 Released

petfeedd users, I am proud to announce the beta release of petfeedd 1.0.1. This release has no major changes in it and is solely about addressing security issues in many of the underlying libraries used by petfeedd. To install it or upgrade from previous versions, you can simply run: docker pull peckrob/petfeedd:latest
Read More
Release Announcements

petfeedd Version 1.0 Now Available

After five beta releases and months of testing, I am happy to announce petfeedd Version 1.0 is now available. All changes from the beta branch have been merged in and the release is now available on Docker Hub. To install it or upgrade from Version 0.2, you can simply run: docker pull peckrob/petfeedd:latest And restart. It should perform all the upgrades needed for version 1.0.
Read More