How to handle pagination and count with angularjs resources?
Asked Answered
T

4

28

I have to build an angularjs client for an API outputting JSON like this:

{
  "count": 10,
  "next": null,
  "previous": "http://site.tld/api/items/?start=4"
  "results": [
    {
      "url": "http://site.tld/api/items/1.json",
      "title": "test",
      "description": "",
      "user": "http://site.tld/api/users/4.json",
      "creation_datetime": "2013-05-08T14:31:43.428"
    },
    {
      "url": "http://site.tld/api/items/2.json",
      "title": "test2",
      "description": "",
      "user": "http://site.tld/api/users/1.json",
      "creation_datetime": "2013-05-08T14:31:43.428"
    },
    {
      "url": "http://site.tld/api/items/3.json",
      "title": "test3",
      "description": "",
      "user": "http://site.tld/api/users/2.json",
      "creation_datetime": "2013-05-08T14:31:43.428"
    }
  ]
}

How can I make a $resource that maps to this ? If I use isArray=false, I'll get the entire blob as one object, usable for reading, but I can't call .put() on it. If I use isArray, it just doesn't work.

Is there any clean way to do this? Or should I go back to using $http?

Trumpery answered 9/5, 2013 at 15:44 Comment(0)
R
17

You have a couple of options. If you can change the server output you could add the meta info (count, next, previous) as header values instead of adding them in the response body.

Your second option is to transform the response with transformResponse. This is available as a $response configuration in angular v1.1.2 and later (the unstable branch):

var Data = $resource('./data.json',{},{
  list:{isArray:true,method:'get',
    transformResponse: function (data, headers) {
      return JSON.parse(data).results; 
   }}
});

If you don't want to use the unstable branch it is also possible to change the $http which $resource uses:

$http.defaults.transformResponse.push(function(data){
  if(data && data.results){
    return data.results;
  }
});

I've created a plunker with both examples: http://plnkr.co/edit/PmYotP0eo5k41Z6ZCxl4?p=preview

I'm not sure what the best approach for passing on the meta data to the rest of your application (if you need it). You could append it to the first result, or add it as a separate object - maybe not so elegant, but it'll get the job done.

Romilly answered 9/5, 2013 at 17:34 Comment(3)
@joakimbl, Do you have any idea how to get next, previous or count values? I have the same response as mentioned in question, and your solutions are exactly what i need, the only problem is that I use 'count' to render pager, so I need to get it value somehow.Monteverdi
Well, you can get them in the transformResponse function (e.g JSON.parse(data).count) - but I'm not sure how you'd pass them along further. You should probably just use the $http service instead.Romilly
I think this answer doesn't solve the both problems in same time. First: "how to pass pagination information" and the second one :"keep Resource features on gathered objects". So I have written my own answer.Kayseri
U
16

I know this question is little bit old. But I think answer doesn't cover main problem - how to get pagination information and how to keep Resource features for list of objects.

You have basicly two solutions, pass paginator data into headers in transform message or use $http and then instantiate items manually.

1.Transform message

Here i redefine query to put pagination data into headers.

Headers is not an array - it is "headersGetter" which returns header by calling headers('Header-Name') and returns inner object by calling headers(). I have to set header lowercase.

var PAGINATION_TOTAL = 'pagination-total-elements';
var PAGINATION_SIZE = 'pagination-size';

...

.factory('BeerResourceFactory', function($resource, API_URI) {
        return $resource(API_URI + '/beer/:id',
            {'id': '@id'},
            {
              'update': {method: 'PUT'},
              'query' : {method: 'GET', isArray:true, transformResponse : function(data, headers) {
                var jsonData = JSON.parse(data);
                headers()[PAGINATION_TOTAL] = jsonData.totalElements;
                headers()[PAGINATION_SIZE] = jsonData.size;

                return jsonData.content;
              }}
            });
      })

After that I define service which encapsulate this and take pagination from headers. Sudenly we cannot use $promise.then() and retrurn result because promise gets only result as argument and not the headersGetter, so we have to use ordinary callback and create own promise.

.service('beerService', function(BeerResourceFactory, $q) {
    this.query = function(filter) {

          var defer = $q.defer();

          BeerResourceFactory.query(filter, function(result, headers) {
            var promiseResult =  {
              beers: result,
              paging: {
                totalItems: headers(PAGINATION_TOTAL),
                perPage: headers(PAGINATION_SIZE)
              }
            };

            defer.resolve(promiseResult);
          });

          return defer.promise;
    }

2.Using $http and instantiate Resource

When using $http instead of resource, there is problem that you still want to use elements of array as resource instances and be able to call $save / $delete, so it is possible to instantiate them manually. Here you can also use ordinary promise as ussual.

.service('beerService', function($http, BeerResourceFactory, API_URI) {
    this.query = function(filter) {
        return $http.get(API_URI + '/beer', {params: filter})
              .then(function(response) {

                var transformedList = response.data.content.map(function(element) {
                  return new BeerResourceFactory(element);
                });

                return {
                  beers: transformedList,
                  paging: {
                    totalItems: response.data.totalElements,
                    perPage: response.data.size
                  }
                };
              });
         };

I would prefer second solution, because its more simple.

Unfamiliar answered 9/5, 2013 at 15:44 Comment(2)
how would you access header later from your first solution.Cullan
I have a hard time with implementing this because i use $q.all() in a lot of my view controllers. I can't figure out how to grab from the headers with an example like that.Dispensary
P
2

I stumped into this problem as well, and here's what worked for me. Add a response transformer which would take the array result and create a resource object manually, which I assume ngResource would do internally anyway.

  var Plan =  $resource(apiPath + 'plans/:planId/', {planId:'@id'}, {
    query: {
      isArray: false,
      transformResponse: function(response){
        response = angular.fromJson(response);
        response.results = response.results.map(function(plan) {
          return new Plan(plan);
        });
        return response;
      }
    },
  });
Poikilothermic answered 18/4, 2016 at 10:20 Comment(0)
S
0

By now this is even older, but I managed to solve this in a single resource factory:

.factory('BeerResourceFactory', function($resource, API_URI) {
    var resource = $resource(API_URI + '/beer/:id',
        {'id': '@id'},
        {
          'update': {method: 'PUT'},
          'query' : {method: 'GET', isArray:true, transformResponse : function(data) {
            var jsonData = angular.fromJson(data);
            jsonData.beers = jsonData.beers.map(function (beer) {
                return new resource(beer)
            });

            return jsonData.content;
          }}
        });
      return resource;
  })
Shantishantung answered 29/6, 2015 at 13:54 Comment(1)
You do not need to do it like that. If you doesn't need to take pagination info into account. You can do it just by returning JSON.parse(data).content; Look here: bitbucket.org/angular_cz/beerapp-codio/src/f11d63cca45a/src/app/…Kayseri

© 2022 - 2024 — McMap. All rights reserved.