How can I extend the constructor of an AngularJS resource ($resource)?
Asked Answered
S

5

15

I have a model, defined using $resource, that I am successfully loading.

Each loaded instance is, as promised, an instance of the class I defined.

(The example below is from the Angular docs. In it, User.get results in an object that is an instanceof User.)

var User = $resource('/user/:userId', {userId:'@id'});

However, imagine each User comes over the wire like this:

{
  "username": "Bob",
  "preferences": [
    {
      "id": 1,
      "title": "foo",
      "value": false
    }
  ] 
}

I defined a Preference factory that adds valuable methods to Preference objects. But when a User loads, those preferences aren’t Preferences, naturally.

I attempted this:

User.prototype.constructor = function(obj) {
  _.extend(this, obj);
  this.items = _.map(this.preferences, function(pref) {
    return new Preference(pref);
  });
  console.log('Our constructor ran'); // never logs anything
}

But it has no effect and never logs anything.

How can I make each item in my Users’ preferences array an instance of Preference?

Salvador answered 9/5, 2013 at 0:10 Comment(2)
Do I need to load things outside of Angular and then create a new User myself for each loaded user, after taking care of preferences?Salvador
I think that you can use a response interceptor in your resource, and modify the response as per your needings. If interested I have a code that achieve something similar.Clef
E
12

$resource is a simple implementation, and lacks in things like this.

User.prototype.constructor won't do anything; angular doesn't try to act like it's object oriented, unlike other libraries. It's just javascript.

..But luckily, you have promises and javascript :-). Here's a way you could do it:

function wrapPreferences(user) {
  user.preferences = _.map(user.preferences, function(p) {
    return new Preference(p);
  });
  return user;
}

var get = User.get;
User.get = function() {
  return get.apply(User, arguments).$then(wrapPreferences);
};
var $get = User.prototype.$get;
User.prototype.$get = function() {
  return $get.apply(this, arguments).$then(wrapPreferences);
};

You could abstract this into a method which decorates any of a resource's methods: It takes an object, an array of method names, and a decorator function.

function decorateResource(Resource, methodNames, decorator) {
  _.forEach(methodNames, function(methodName) {
    var method = Resource[methodName];
    Resource[methodName] = function() {
      return method.apply(Resource, arguments).$then(decorator);
    };
    var $method = Resource.prototype[methodName];
    Resource.prototype[methodName] = function() {
      return $method.apply(this, arguments).$then(decorator);
    };
  });
}
decorateResource(User, ['get', 'query'], wrapPreferences);
Ecumenicist answered 9/5, 2013 at 2:48 Comment(3)
I think where you have var methodName in the function at the bottom, you probably meant var method and then in the next line Resource[methodName] instead.Powerful
Good explanation. Probably there's a mistype in parameters order between decorateResource declaration and its invocation.Hierolatry
Looking again at 1st block of code, shouldn't it be: var $get = User.prototype.get; ?Hierolatry
X
5

You can do this by overriding the built-in resource actions to transform the request and response (See transformRequest and transformResponse in the docs.):

var m = angular.module('my-app.resources');
m.factory('User', [
          '$resource',
  function($resource) {

    function transformUserFromServer(user) {
      // Pass Preference directly to map since, in your example, it takes a JSON preference as an argument
      user.preferences = _.map(user.preferences, Preference);
      return user;
    }

    function transformUserForServer(user) {
      // Make a copy so that you don't make your existing object invalid
      // E.g., changes here may invalidate your model for its form, 
      //  resulting in flashes of error messages while the request is 
      //  running and before you transfer to a new page
      var copy = angular.copy(user);
      copy.preferences = _.map(user.preferences, function(pref) {
        // This may be unnecessary in your case, if your Preference model is acceptable in JSON format for your server
        return {
          id: pref.id,
          title: pref.title,
          value: pref.value
        };
      });

      return copy;
    }

    function transformUsersFromServer(users) {
      return _.map(users, transformUserFromServer);
    }

    return $resource('/user/:userId', {
        userId: '@id'
      }, {
        get: {
          method: 'GET',
          transformRequest: [
            angular.fromJson,
            transformUserFromServer
          ]
        },
        query: {
          method: 'GET',
          isArray: true,
          transformRequest: [
            angular.fromJson,
            transformUsersFromServer
          ]
        },
        save: {
          method: 'POST',
          // This may be unnecessary in your case, if your Preference model is acceptable in JSON format for your server
          transformRequest: [
            transformUserForServer,
            angular.toJson
          ],
          // But you'll probably still want to transform the response
          transformResponse: [
            angular.fromJson,
            transformUserFromServer
          ]
        },
        // update is not a built-in $resource method, but we use it so that our URLs are more RESTful
        update: {
          method: 'PUT',
          // Same comments above apply in the update case.
          transformRequest: [
            transformUserForServer,
            angular.toJson
          ],
          transformResponse: [
            angular.fromJson,
            transformUserFromServer
          ]
        }
      }
    );
  };
]);
Xylograph answered 24/10, 2014 at 17:18 Comment(0)
H
3

I was looking for a solution to the same problem as yours. I came up with the following approach.
This example is based on Offers instead of Users, as domain entity. Also, please note here's a trimmed down version of the whole thing, which in my case spans over some files:

Domain entity custom class:

function Offer(resource) {
    // Class constructor function
    // ...
}

angular.extend(Offer.prototype, {
    // ...

    _init: function (resource) {
        this._initAsEmpty();

        if (typeof resource == 'undefined') {
            // no resource passed, leave empty
        }
        else {
            // resource passed, copy offer from that
            this.copyFromResource(resource);
        }
    },

    copyFromResource: function (resource) {
        angular.extend(this, resource);
        // possibly some more logic to copy deep references
    },

    // ...
});

Classic angular custom resource:

var offerResource = $resource(/* .. */);

Custom repository, passed to controller by a service factory:

function OfferRepository() {  
    // ...
}

angular.extend(OfferRepository.prototype, {
    // ...

    getById: function (offerId, success, error) {

        var asyncResource = offerResource.get({
            offerId: offerId

        }, function (resource) {
            asyncOffer.copyFromResource(resource);

            (success || angular.noop)(asyncOffer);

        }, function (response) {
            (error || angular.noop)(response);

        });

        var asyncOffer = new offerModels.Offer(asyncResource);

        return asyncOffer;
    },

    // ...
});

Most noticeable parts are:

  • the custom entity class, that is able to construct/fill itself starting from a resource instance (possibly with deep copy capabilities, e.g. Positions in an Offer)
  • the custom repository class, which wraps the resource. That does not returns the classic async resource answer, but instead returns an custom entity instance, and later on it fills that with the resource just loaded.
Hierolatry answered 3/7, 2013 at 23:57 Comment(0)
W
2

Attempting to modify the constructor property of the prototype object won't do what you expect anyhow, please take a look at the very nice post here.

To really understand what is going on, one should look at the source code of the ngResource module - there are a lot of things at work there, but what's important is that the $resource factory returns a plain JavaScript function (really, what else). Invoking this function with the documented parameters returns a Resource constructor object, which is defined privately in resourceFactory.

As you may recall, AngularJS services are singletons, meaning that calling $resource will return the same function every time (in this case, resourceFactory). The important takeaway is that every time this function is evaluated, a new Resource constructor object is returned, meaning that you can prototype your own functions on it safely, without worrying that this will pollute all Resource instances globally.

Here is a service that you can use much as the original $resource factory, while defining your own custom methods that will be available on all of its instances:

angular.module('app').factory('ExtendedResourceFactory', ['$resource',
  function($resource) {                                                        
    function ExtendedResourceFactory() {
      var Resource = $resource.apply(this, arguments);

      Resource.prototype.myCustomFunction = function() {
        ...
      };

      return Resource;
    }

    return ExtendedResourceFactory;
  }
]);

Inside myCustomFunction you have access to the data returned from the server so you can use this.preferences and return whichever custom class you want to build.

Wardship answered 12/2, 2015 at 9:53 Comment(1)
this didn't work for me or I missed something. I added a new question that is very related #36921994Bromley
R
1

transformResponse does the job. Consider example (I wanted to use Autolinker to format response content).

return $resource('posts/:postId', {
    postId: '@_id'
}, {
    get : {
        transformResponse : function(data) {
            var response = angular.fromJson( data );
            response.content = Autolinker.link(response.content);
            return response;
        }
    },
    update: {
        method: 'PUT'
} });
Roeser answered 10/1, 2015 at 22:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.