2

I'm trying to create an array of objects from the return of a $resource query as shown in this SO question: link. However, I keep getting the same list of Resources and other elements. I have a plunk: here (You have to open the developer console to see the output.)

var app = angular.module('plunker', ['ngResource']);


app.factory('NameResource', function($resource) {

  var url = 'data.json';
  var res = $resource(url, null, {

    query: {
      method: 'GET',
      isArray: true,
      transformResponse: function(data, headersGetter) {
        var items = angular.fromJson(data);
        var models = [];
        angular.forEach(items, function(item) {
          models.push(item);
        });
        console.log("models: ", models);
        return models;
      }
    }
  });

  return res;
});

app.controller('MainCtrl', function($scope, NameResource) {
  $scope.names = NameResource.query();
  console.log('Inside controller: ', $scope.names);

  setTimeout(function(){console.log('after some time names is:', $scope.names)}, 3000);
});

What am I doing wrong? Or have I misunderstood something. Also what is the difference between the two? It seems to work very similar for me. When will it cause an issue?

Community
  • 1
  • 1
sathishvj
  • 1,394
  • 2
  • 17
  • 28

1 Answers1

5

Resource.query returns an array (because of the isArray flag you created) with two properties, $promise which is the promise which when resolved will "copy" all the response values to the array returned from Resource.query magically updating the view and $resolved which is a flag telling if $promise was resolved already, to answer your question there's actually some additional transformation happening, the data returned from your transformation will actually go through another transform (which can't be disabled) and this is where your each object is transformed into a Resource instance.

So this is what you're expecting to happen:

promise
    .then(function (rawData) {
        // this is where your transformation is happening
        // e.g. transformResponse is called with rawData
        // you return your transformed data
    })
    .then(function (transformedData) {
        // raw data has gone through 1 transformation
        // you have to decide what to do with the data, like copying it to
        // some variable for example $scope.names
    })

But Resource is doing the following:

promise
    .then(function (rawData) {
        // this is where your transformation is happening
    })
    .then(function (transformedData) {
        // Resource is adding this handler and doing the
        // 'copy' to array operation here for you,
        // but it will actually create a Resource instance 
        // in the process with each item of your array!
    })
    .then(function (transformedDataV2) {
       // raw data has gone through 2 transformations here!             
    })

The additional transformation is where the magic happens and is where the Resource instance is created, if we take a look at the source code these are the lines which take care of this transformation, I'll copy them here:

if (action.isArray) {
  value.length = 0;
  forEach(data, function(item) {
    if (typeof item === "object") {
      value.push(new Resource(item));
    } else {
      // Valid JSON values may be string literals, and these should not be converted
      // into objects. These items will not have access to the Resource prototype
      // methods, but unfortunately there
      value.push(item);
    }
  });
}

data is the data returned by your first transformation and as seen above it'll pass the typeof item === 'Object' check so value which is the array returned by Resource.query is updated with a new Resource item (not with item). You were worried about this strange Resource object, let's analyze the Resource constructor:

function Resource(value) {
  shallowClearAndCopy(value || {}, this);
}

It's just copying each of the properties of the object value to this (this is the new Resource instance), so now we're dealing with Resource objects and not plain array objects

will it cause an issue?

I'm sure it will, if the transform function you define is a little bit more complex like having each object actually be an instance of something else whose __proto__ has some methods, e.g. a Person object instead of a plain object then the methods defined in Person.prototype won't be visible to the result of the whole operation since each object won't be a Person instance but a Resource instance! (see this error in this plunkr, make sure to read the comments and also look at the error raised in the console because of the undefined method)

Mauricio Poppe
  • 4,817
  • 1
  • 22
  • 30
  • Well explained. So OP has to unwrap Persons in the controller? What is the solution for transforming response as an array of Persons? – Uluk Biy Mar 07 '15 at 05:51
  • Thank you for that answer. It clarifies for me what's happening. If I may ask a little more, is there a way to automatically create just an Object and not necessarily a Person/Vehicle/etc. instance each time? Something that will make it generic and reduce the code? Maybe a function in underscore.js like clone, extend, extendOwn ...? (I tried all of them, but it didn't work.) – sathishvj Mar 07 '15 at 07:24
  • @UlukBiy I'm no angular expert but I'd just use the `$http` service without the additional transformation done by `$resource` which I find unnecessary imho, then do whatever I want with the data like transforming it to an array of Persons – Mauricio Poppe Mar 07 '15 at 18:18
  • @sathishvj I don't really understand what you mean with generic, if you just want a plain object use `$http` instead – Mauricio Poppe Mar 07 '15 at 18:21
  • Maybe you mean something like this: static toInstance(obj: T, json: string) : T { var jsonObj = JSON.parse(json); if (typeof obj["fromJSON"] === "function") { obj["fromJSON"](jsonObj); } else { for (var propName in jsonObj) { obj[propName] = jsonObj[propName] } } return obj; } – gmesorio Jul 03 '16 at 22:47