Angular - extending $resource subobject with custom methods
Asked Answered
L

4

37

In most cases the result of <custom-resource>.query() method is an array, which can be easily extended with some methods (business logics) with the following (factory) code:

var Data = $resource('http://..');
Data.prototype.foo = function() {return ...};

this is perfect for using with ng-repeat / ng-class, like so:

<tr ng-repeat="item in responseData" ng-class="{warning: item.foo()}">..</tr>

My problem is that every list response is encapsulated in an object which, besides the actual list, has some meta-properties (sorting info etc), so the final object returned is like this:

{ order_field: "name", items: [{..}, {..},{..}] }

Now, how do I make the same thing as previously with ng-repeat/ng-class?

<tr ng-repeat="item in responseData.items" ng-class="????">..</tr>

the previous method won't work as the "foo" method is defined on responseData and NOT on item object

Is there any way to directly extend the base class used for instantiating objects on the list?

Thanks!

Landlubber answered 16/6, 2013 at 14:37 Comment(0)
J
53

I've found that problem before, and the solution seems to be transformResponse, as John Ledbetter says in the other answer.

Anyway, if you need to keep the entire object, and also having the array in 'items' filled with instances of the resource, you might be able to do it with the following trick:

Taking the example from John's answer, and modifying it a bit:

angular.module('foo')

  .factory('Post', ['$resource', function($resource) {

    var Post = $resource('/api/posts/:id', { id: '@id' }, {
      query: {
        method: 'GET',
        isArray: false, // <- not returning an array
        transformResponse: function(data, header) {
          var wrapped = angular.fromJson(data);
          angular.forEach(wrapped.items, function(item, idx) {
             wrapped.items[idx] = new Post(item); //<-- replace each item with an instance of the resource object
          });
          return wrapped;
        }
      }
    });

    Post.prototype.foo = function() { /* ... */ };

    return Post;
  }]);
Jumbled answered 17/6, 2013 at 14:11 Comment(1)
I think this is a solid solution. It doesn't feel perfect to me, but it's the best I've seen :)Disposition
D
9

If you're using angular-resource 1.1.5 (which as far as I can tell actually works fine with angular 1.0.7), there is a transformResponse option you can specify when overriding $resource methods:

angular.module('foo')
  .factory('Post', ['$resource', function($resource) {

    var Post = $resource('/api/posts/:id', { id: '@id' }, {
      query: {
        method: 'GET',
        isArray: true,
        transformResponse: function(data, header) {
          var wrapped = angular.fromJson(data);
          return wrapped.items;
        }
      }
    });

    Post.prototype.foo = function() { /* ... */ };

    return Post;
  }]);

If you do this, you no longer have to manually pull the items out of the wrapped response, and each item will be an instance of Post that has access to the .foo method. You can just write:

<tr ng-repeat="post in posts" ng-class="{warning: post.foo()}">..</tr>

The downside to this is that you lose access to any of the outer fields in your response that aren't inside items. I'm still struggling to figure out a way to preserve that metadata.

Disposition answered 16/6, 2013 at 15:34 Comment(3)
thanks, but this is not really what I'm looking for. As I'm writing both angular-js frontend and C#-backend, I could simply avoid wrapping the list in the object, but the point is I need that metadata too.Landlubber
@Landlubber I understand, I'm in the same situation -- I'd like to be able to support paging/sorting with my API without having to manually 'unbox' all the resources. Be interested to see if there are any good answers to this.Disposition
I add the metadata back by adding a closure variable function(){ var meta = {}; var Product = $resource = {tranformResponse : function(data) { meta.prop = data.prop;} } Product._meta = meta; return product; }Iapetus
F
4

This is an old question, but I just ran into this issue myself. gargc's solution is the right approach, but there is an improvement. transformResponse accepts an array that gets passed to the $http service. Rather than completely replace the transform function, you can append your transform to the defaults to just make the updates you need:

angular.module('foo')
    .factory('Post', function ($resource, $http) {
        var Post = $resource('/api/posts/:id', { id: '@id' }, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: $http.defaults.transformResponse.concat(function(data, header) {
                    angular.forEach(data.items, function(item, idx) {
                        data.items[idx] = new Post(item);
                    });
                    return data;
                })
            }
        });

        Post.prototype.foo = function() { /* ... */ };

        return Post;
    });
Frere answered 4/1, 2015 at 18:25 Comment(0)
S
0

You could put the metadata in a header. I always put paging data there. That way your query will still return an array, which I believe is a good thing. Queries should return arrays of data, not single data.

Scharff answered 22/11, 2015 at 20:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.