How to handle Content-Range for pagination on Restangular? - angularjs

The API i am using returns header "Content-range" for lists :
Request : GET /users
Response contains : Content-Range: 0-49/1337
What is the correct way to generalize the retrieval (and parsing) of this header value with Restangular ?
Do i have to make my own function ?
Is there a existing method that can "catch" every response, check if the header is there and then append the parsed values (i.e : limit, range, total_count, page number) to Restangular returned object ?

Restangular has addresponseinterceptor for catching responses. You can catch your response there and get response.headers.
You should return a restangularized element in the interceptor otherwise you cannot get any data in your controllers or services...
So an example can be like this...
RestangularProvider.addResponseInterceptor(function (data, operation, what, url, response, deferred) {
var data = response.data;
var contentRange = response.headers("Content-Range");
if (contentRange) {
data.limit = contentRange.limit;
}
return data;
});
As you see you can add Restangular response interceptor on any angular config blog by injection RestangularProvider

After a few iterations on #wickY26 solution here is a technique that worked well for me :
Prerequisite
As mentioned in the question the API response contains this header :
Content-Range: 0-9/150
Which mean : The returned list will start at the 1st element (starting at 0) until the 9th on a total of 150.
On the other side the request has to include a "range=x-y" :
https://api.myapp.com/contents/?range=10-20
Code below will only work inside those contraints.
First step
Make Restangular grab all incoming responses from API. If we have a "Content-Range" header then append a pagination object with all important informations regarding pagination (from, to, total, numPage, currentPage) :
angular.module('myapp')
.config(dataConfigResponse);
dataConfigResponse.$inject = ['RestangularProvider'];
function dataConfigResponse(RestangularProvider) {
RestangularProvider.addResponseInterceptor(function (data, operation, what, url, response, deferred) {
var responseData = response.data;
var contentRange = response.headers('Content-Range');
if (contentRange) {
var rangeFields = contentRange.split(/\s|-|\//);
var paginationFrom = parseInt(rangeFields[0]) + 1;
var paginationTo = parseInt(rangeFields[1]) + 1;
var paginationTotal = parseInt(rangeFields[2]);
var paginationSubTotal = parseInt(paginationTo - paginationFrom);
responseData.pagination = {
from: paginationFrom,
to: paginationTo,
total: paginationTotal,
numPages: Math.ceil(paginationTotal / paginationSubTotal),
currentPage: Math.ceil(paginationFrom / paginationSubTotal)
};
}
return responseData;
});
}
Second step
Make Restangular intercept all outgoing requests to the API. When the action is a getList we append "?range=10-20" to the next query
angular.module('myapp')
.config(dataConfigRequest);
dataConfigRequest.$inject = ['RestangularProvider'];
function dataConfigRequest(RestangularProvider) {
RestangularProvider.addFullRequestInterceptor(function (element, operation, what, url, headers, params, httpConfig) {
if (operation === 'getList') {
if (Object.keys(params).length) {
params.range = ((params._page - 1) * params._perPage) + '-' + (params._page * params._perPage - 1);
delete params._page;
delete params._perPage;
}
}
});
}
Third step
Now the controller which is displaying the list.
function dashboardContentListCtrl($scope, dataService) {
// Pagination starting values
$scope.maxSize = 6;
$scope.currentPage = 1;
$scope.limit = 20;
$scope.pageChanged = function () {
$scope.params = {_page: $scope.currentPage, _perPage: $scope.limit};
dataService.getAllContents($scope.params).then(function (contents) {
$scope.pagination = contents.pagination;
$scope.contents = contents.plain();
}).catch(function () {
$scope.contents = [];
});
};
$scope.pageChanged();
On my app the pagination view component is angular-ui pagination directive, looking like this :
<pagination class="pagination-sm"
boundary-links="true"
ng-hide="pagination.total < limit"
total-items="pagination.total"
items-per-page="limit"
ng-model="currentPage"
max-size="maxSize"
rotate="false"
ng-change="pageChanged()"></pagination>

Related

Angular ng-table loading from server with pagination

I am trying to figure out how to populate an ng-table and apply the total value for the params. It appears there are examples that are showing almost what I'm doing - but not exactly and the total never gets set.
$scope.tableParams = new NgTableParams({page: 1, count: 25},
{
counts: [],
getData: function(params)
if (CompaniesView.ViewInitialized)
return CompaniesView.RetrieveCompanies($scope, $http, function ()
{
params.total($scope.RequestFilter.TotalCompanies);
return $scope.TableParams.data;
});
}
});
My RetrieveCompanies function retrieves he data - and will call a callback function on completion. I was under the impression at this point I could set the params.total, but is not working. I see that examples are doing similar, except they are performing a jQuery API operation directly. I am not sure why this does not work. I would have thought anytime setting the total would cause the table pagination controls to update - but it appears it has to be done within the detData call. But if you are making an async call how can we have the total set in the call since it won't have data until the async completes - which means the
getData call has already finished.
Probably missing some concept here -- but I am not an expert when it comes to jQuery or angular.
Peter
Update:
RetrieveCompanies function
RetrieveCompanies: function (scope, http,callback)
{
scope.IsLoading = true;
var Data =
{
AuthoirzationType: TokenRetrieval.SessionAuth,
SessionId: activeSession
};
var tokenRequest =
{
params: Data
};
performVeilabilityTokenRetrieval(http, tokenRequest,
function (response)
{
if (true)
{
if (!response.HasError)
{
scope.RequestFilter.State = scope.selectedState.Value
scope.RequestFilter.regionsearch = scope.selectedRegion.id;
scope.RequestFilter.checkadminStaff = scope.selectedAdmin.value;
//tableParams
scope.RequestFilter.Page = scope.tableParams.page();
scope.RequestFilter.PageCount = scope.tableParams.count();
var config =
{
headers:
{
Authorization: 'Bearer ' + response.RequestToken
},
params: scope.RequestFilter
}
http.get(CompaniesView.Companies_api_urls.loadCompaniesRequest, config)
.then(
function (response)
{
scope.RequestFilter.TotalCompanies = response.data.companieslistTotal;
scope.TableParams.data = response.data.companies;
if (callback != null) callback();
},
function (data, status, headers, config) //error
{
scope.IsLoading = false;
}
);
}
else
{
scope.request.requestToken = null;
//CreateExpenseView.PrepareRESTResults(data.RestResults);
}
}
else
{
scope.request.requestToken = null;
scope.request.error = data.replace(/\"/g, "");
}
});
}
Ok; finally found what was wrong. The ng-table getData operation requires a promise to be returned. My function was not configured as a deferred promise and once I realized this I put in place the required modification for my data retrieval to return a promise. One big point -- when I wrapped my call with a when - I completed with a done - and getData is operating off a then (from the promise). Once those changes were in place the pagination (total) operation worked.
Peter

Restangular GET request returns 100 records only

I am using a Restangular library in my angularjs application.I want to retrieve all registered user's information from rest api.Whenever I make Restangular GET request to do so it retrieves only 100 records while I have around 250+ users for my website.I've tried using
Restangular.all('url').getList({limit:200,offset:0})
.then(function (success) {
//some code
});
This was the wayout mentioned here but it isn't working for me.
Found solution after some time
RestFullResponse.all('url').getList().then(function (success) {
var headers = success.headers();
var currentpage = headers['x-pager-current-page'];
var lastpage = headers['x-pager-last-page'];
for(currentpage; currentpage<=lastpage; currentpage++) {
var param = {
"page_entries": 100,
"page":currentpage,
"offset": (currentpage-1)*this.page_entries
};
RestFullResponse.all('url').getList(param).then(function (success) {
personList = personList.concat(success['data']);
});
}
});

Angular nested http requests

Guys i woulde like to nest http requests to flickr api, my goal is get (using flickr.photos.search) ids of photos (parametrized via 'text'), then make a second call on flick.photos.getSizes and retrieve direct link to source image.
Here is my code, but any other ideas would be nice from you. The problem is that PhotoSearchService return then all $http calls are made. Any better ideas ?
Service:
angular.module('flickrGalleryApp').factory('PhotoSearchService', function ($resource) {
const apiKey = '&api_key=my api key';
const apiUrl = 'https://api.flickr.com/services/rest/?method=';
const method = 'flickr.photos.search';
const format = '&format=json';
const callback = '&nojsoncallback=1';
return $resource(null,null, {
search : {url:apiUrl+method+apiKey+format+callback,method: 'GET', isArray: false}
});
});
Controller:
$scope.items = PhotoSearchService.search({text:'girl'}, function(response){
var array = response.photos.photo;
const apiKey = '&api_key=my api key';
const apiUrl = 'https://api.flickr.com/services/rest/?method=';
const method = 'flickr.photos.getSizes';
const format = '&format=json';
const callback = '&nojsoncallback=1';
var result = [];
for(var i = 0 ; array.length; i++){
var id = array[i].id;
var url = apiUrl+method+apiKey+format+callback + '&photo.id=' + id;
console.log(url);
$http.get(url).then(function(resp){
var array2 = resp.data.sizes.size;
for(var j = 0; j < array2.length ;j++){
if(array2[j].label.indexOf('Square') > 0){
result.push(array2[j].source);
}
}
return result;
}, function(err){
})
}
},function(error){
});
html:
<ul>
<li ng-repeat="item in items">
<span>{{item.source}}</span>
</li>
</ul>
Since you make an $http.get call for each photo id returned by the first call, you have to await for all the calls to complete then return the result.
The trick to do this is to create a defer object and resolve it when the result array is full.
Take a look at $q to see how you can pull this off. Just keep in mind that you will return a promise which means you have to implement a success or error callback to get the resulting array

angularjs resource limit changes after first call

Problem description
Im using the angular resource to get data from my server. I've extended it a bit to make sure all of my resources have security headers.
Problem is that on the second get request and on, my get requests are sent with limit=0, and only the first get request is sent correctly (with limit=12).
Code part
This is my base resource factory (for making sure all resource contain the keys and everything):
app.factory('SecuredFactory', function($resource){
var DEFAULT_ACTIONS = {
'get': {method:'GET'},
'query': {method:'GET', isArray:true},
};
var DEFAULT_PARAMS = {
'limit': 12,
'format': 'json'
};
for(var key in DEFAULT_ACTIONS){
DEFAULT_ACTIONS[key]['headers'] = <headers object>;
}
var securedResource = function(url, paramDefaults, actions){
for (var attrname in actions) {
DEFAULT_ACTIONS[attrname] = actions[attrname];
}
for (var attrname in paramDefaults) {
DEFAULT_PARAMS[attrname] = paramDefaults[attrname];
}
var defaultResource = $resource(url, DEFAULT_PARAMS, DEFAULT_ACTIONS);
return defaultResource;
};
return securedResource;
});
And this is an example of how I creat a specific factory out of the secured one:
app.factory('QuestionFactory', function(SecuredFactory, Constants){
var url = Constants.SERVER_URL + 'question/';
var Task = SecuredFactory(url);
return Task;
});
And this is finally how I use it, for example:
// filtering example (not important for this matter):
var filtering = {author: "Daniel"};
var contents = [];
var resource = QuestionFactory;
resource.get(filtering, function (res) {
// success fetching
$scope.contents = $scope.contents.concat(res['objects']);
}
// failed fetching
, function (err) {
}
);
The requests
first request:
question?format=json&limit=12&offset=0
second request and on:
question?format=json&limit=0&offset=0
My problem was that the DEFAULT_PARAMS variable was declared as global. I didn't realize that invoking the secured factory with {limit: 0} will override the global, therefore changing the limit to 0 for ALL of my resources.
Changing the securedFactory to a service and moving the "globals" into the returned function solved it. Had to add new ofcourse before every securedService call.

Cancelling a request with a $http interceptor?

I'm trying to figure out if it is possible to use a $http interceptor to cancel a request before it even happens.
There is a button that triggers a request but if the user double-clicks it I do not want the same request to get triggered twice.
Now, I realize that there's several ways to solve this, and we do already have a working solution where we wrap $http in a service that keeps track of requests that are currently pending and simply ignores new requests with the same method, url and data.
Basically this is the behaviour I am trying to do with an interceptor:
factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.finally(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
}
}
}])
When I look at the API for $http interceptors it does not seem to be a way to achieve this. I have access to the config object but that's about it.
Am I attempting to step outside the boundaries of what interceptors can be used for here or is there a way to do it?
according to $http documentation, you can return your own config from request interceptor.
try something like this:
config(function($httpProvider) {
var cache = {};
$httpProvider.interceptors.push(function() {
return {
response : function(config) {
var key = createKey(config);
var cached = cache[key];
return cached ? cached : cached[key];
}
}
});
}
Very old question, but I'll give a shot to handle this situation.
If I understood correctly, you are trying to:
1 - Start a request and register something to refer back to it;
2 - If another request takes place, to the same endpoint, you want to retrieve that first reference and drop the request in it.
This might be handled by a request timeout in the $http config object. On the interceptor, you can verify it there's one registered on the current request, if not, you can setup one, keep a reference to it and handle if afterwards:
function DropoutInterceptor($injector) {
var $q = $q || $injector.get('$q');
var dropouts = {};
return {
'request': function(config) {
// I'm using the request's URL here to make
// this reference, but this can be bad for
// some situations.
if (dropouts.hasOwnProperty(config.url)) {
// Drop the request
dropouts[config.url].resolve();
}
dropouts[config.url] = $q.defer();
// If the request already have one timeout
// defined, keep it, othwerwise, set up ours.
config.timeout = config.timeout || dropouts[config.url];
return config;
},
'requestError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
},
'response': function(response) {
delete dropouts[response.config.url];
return response;
},
'responseError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
}
};
}

Resources