Can't seem to get my head over the concept of 'promise' in AngularJS, I am using the restangular library to fetch a resource over REST, however I always get null results. Here's the code
.service('CareersService', [ 'Restangular', '$sce', function(Restangular, $sce){
var vacancies = [];
var result;
this.getVacancies = function() {
Restangular.all('job_posts').getList({active: 'true'}).then(function(job_posts){
job_posts.forEach(function(job_post){
vacancies.push(_.pick(job_post,['id','title','min_experience','max_experience','location']));
})
})
return vacancies;
}
this.getVacancy = function(job_id){
Restangular.one('job_posts',job_id).get().then(function(job_post){
result = _.pick(job_post, 'title','min_experience','max_experience','location','employment_type','description');
var safe_description = $sce.trustAsHtml(result.description);
var emp_type = _.capitalize(result.employment_type);
_.set(result, 'description', safe_description);
_.set(result, 'employment_type', emp_type);
});
return result;
}
}]).controller('DetailsCtrl', ['$scope' ,'$stateParams', 'CareersService' ,function($scope, $stateParams, CareersService) {
$scope.data.vacancy = { title: 'Loading ...', contents: '' };
$scope.data.vacancy = CareersService.getVacancy($stateParams.job_id);
}])
and then in view
<div class="container">
<a ui-sref="careers" class="btn btn-primary">Show All</a>
<div class="row">
<h2>{{ data.vacancy.title }}</h2>
<p>{{ data.vacancy.min_experience }}</p>
<p>{{ data.vacancy.max_experience }}</p>
<p>{{ data.vacancy.location }}</p>
<p>{{ data.vacancy.employment_type }}</p>
<p ng-bind-html="data.vacancy.description"></p>
</div>
</div>
Am I missing something in the way to use promises?
Update
here's the updated code thanks to all the help I got here,
this.getVacancies = function() {
Restangular.all('job_posts').getList({active: 'true'}).then(function(job_posts){
job_posts.forEach(function(job_post){
vacancies.push(_.pick(job_post,['id','title','min_experience','max_experience','location']));
})
return vacancies;
})
}
this.getVacancy = function(job_id){
Restangular.one('job_posts',job_id).get().then(function(job_post){
vacancy = _.pick(job_post, 'title','min_experience','max_experience','location','employment_type','description');
...
return vacancy;
});
}
}])
And in controllers
CareersService.getVacancy($stateParams.job_id).then(function (vacancy){
$scope.data.vacancy = vacancy;
});
and
CareersService.getVacancies().then(function (vacancies){
$scope.data.vacancies = vacancies;
});
I now get the error
Cannot read property 'then' of undefined
At the line
CareersService.getVacancies().then(function(vacancies) {
Restangular makes an API call over a http, and once it make a call it returns underlying promise object. And inside .then function of it you can get the data responded by API.
So here you are making an async call and considering it to happen it in synchronous way like you can see you had returned result/vacancies array from Restangular call, in that way result/vacancies is always going to be empty.
In such you should return a promise from a service method. And return appropriate formatted data from promise so that you can chain that promise in controller as well(by retrieving a data).
Service
this.getVacancies = function() {
//returned Restangular promise
return Restangular.all('job_posts').getList({
active: 'true'
}).then(function(job_posts) {
job_posts.forEach(function(job_post) {
vacancies.push(_.pick(job_post, ['id', 'title', 'min_experience', 'max_experience', 'location']));
});
//return calculated result
return vacancies;
})
}
this.getVacancy = function(job_id) {
//returned Restangular promise
return Restangular.one('job_posts', job_id).get().then(function(job_post) {
result = _.pick(job_post, 'title', 'min_experience', 'max_experience', 'location', 'employment_type', 'description');
var safe_description = $sce.trustAsHtml(result.description);
var emp_type = _.capitalize(result.employment_type);
_.set(result, 'description', safe_description);
_.set(result, 'employment_type', emp_type);
//returned result to chain promise
return result;
});
}
As I said now you can easily chain promise inside controller by having .then function over service method call.
CareersService.getVacancy($stateParams.job_id).then(function(result){
$scope.data.vacancy = result;
});
Update
The syntax without .then would work, but you need to make small change in it by adding .$object after a method call.
$scope.data.vacancy = CareersService.getVacancy($stateParams.job_id).$object;
$object is property which added inside promise object by Restangular. While making an API call, at that time it makes $scope.data.vacancy value as a blank array ([]) and once server respond with response, it fills that object with response received by server. Behind the scene it only updates the value of $object property which automatically update $scope.data.vacancy value.
Same behaviour is there in $resource of ngResource.
I wanted to also put down that when you're chaining promise, that time you have to explicitly handle error case. Whereas in current code you haven't handle such failure condition. So I'd suggest you to go for that as well by adding error function inside Restangular REST API call. and do use $q.reject('My error data, this can be object as well').
You are making a async call to get the result. So you need either a callback or promise to handle this. One option with minimum code change is to make the service to return promise and in the controller get the result via then
.service('CareersService', ['Restangular', '$sce', function(Restangular, $sce) {
var vacancies = [];
var result;
this.getVacancies = function() {
Restangular.all('job_posts').getList({
active: 'true'
}).then(function(job_posts) {
job_posts.forEach(function(job_post) {
vacancies.push(_.pick(job_post, ['id', 'title', 'min_experience', 'max_experience', 'location']));
})
})
return vacancies;
}
this.getVacancy = function(job_id) {
return Restangular.one('job_posts', job_id).get().then(function(job_post) {
result = _.pick(job_post, 'title', 'min_experience', 'max_experience', 'location', 'employment_type', 'description');
var safe_description = $sce.trustAsHtml(result.description);
var emp_type = _.capitalize(result.employment_type);
_.set(result, 'description', safe_description);
_.set(result, 'employment_type', emp_type);
return result;
});
}
}]).controller('DetailsCtrl', ['$scope', '$stateParams', 'CareersService', function($scope, $stateParams, CareersService) {
$scope.data.vacancy = {
title: 'Loading ...',
contents: ''
};
CareersService.getVacancy($stateParams.job_id).then(function(result) {
$scope.data.vacancy = result;
});
}])
You are doing return outside of then try to move it inside in then function
this.getVacancies = function() {
Restangular.all('job_posts').getList({active: 'true'}).then(function(job_posts){
job_posts.forEach(function(job_post){
vacancies.push(_.pick(job_post,['id','title','min_experience','max_experience','location']));
})
return vacancies;
})
}
Related
I have 2 arrays, sports and leagues and I want those arrays to become resolve before anything on the page, I will paste all of my code regarding those arrays I just mentioned. I need to do this due to an issue I am having with the Angular filters.
this is my html
<div ng-repeat="sport in sportsFilter = (sports | filter:query)">
<div ng-if="sport.leagues.length">
<!--first array-->
{{sport.name}}
</div>
<div ng-repeat="league in sport.leagues">
<!--second array-->
{{league.name}}
</div>
</div>
controller
.controller('SportsController', function($scope, $state, AuthFactory,
SportsFactory, Sports) {
$scope.sports = [];
$scope.sportPromise = Sports;
AuthFactory.getCustomer().then(function(customer) {
$scope.customer = customer;
SportsFactory.getSportsWithLeagues(customer).then(function(sports) {
$ionicLoading.hide();
if (sports.length) {
$scope.sportPromise = Sports;
$scope.sports = sports;
}else {
AuthFactory.logout();
}
}, function(err) {
console.log(err);
});
}
});
$scope.isSportShown = function(sport) {
return $scope.shownSport === sport;
};
});
and here de app.js so far, I thought with this I was resolving the arrays, actually the must important is the array named leagues, but still is giving me troubles
.state('app.sports', {
url:'/sports',
views:{
menuContent:{
templateUrl:'templates/sportsList.html',
controller:'SportsController',
resolve: {
Sports: function(SportsFactory, AuthFactory, $q) {
var defer = $q.defer();
AuthFactory.getCustomer().then(function(customer) {
SportsFactory.getSportsWithLeagues(customer).then(function(sports) {
var sportLeagues = _.pluck(sports, 'leagues'),
leaguesProperties = _.chain(sportLeagues).flatten().pluck('name').value();
console.log(leaguesProperties);
defer.resolve(leaguesProperties);
});
});
return defer.promise;
}
}
}
}
})
UPDATE:
the page is loading and I my filter is getting the array leagues empty, so is not searching thru to it, so I need that array to load first than the filters.
Here's how this could work at a high-level, including avoiding some mistakes you are making.
Mistakes:
You don't need to use $q.defer when the API you are using is already returning a promise. Just return that promise. What you are doing is called an deferred anti-pattern.
resolve is used when you need to do something (like authentication) before you are hitting a particular state. You are under-using the resolve by not resolving the customer, and instead doing this in the controller.
resolve property of $stateProvider can accept other resolves as parameters.
With these out of the way, here's how it could work:
.state('app.sports', {
resolve: {
customer: function(AuthFactory){
return AuthFactory.getCustomer();
},
sports: function(SportsFactory, customer){
return SportsFactory.getSportsWithLeagues(customer);
},
leagues: function(sports){
var leagueProperties;
// obtain leagueProperties from sports - whatever you do there.
return leagueProperties;
}
}
});
Then, in the controller you no longer need AuthFactory - you already have customer:
.controller('SportsController', function($scope, customer, sports, leagues){
$scope.sports = sports;
})
As per request in the comments of New Dev's answer the same only using array notation so that the code remains minifiable:
.state('app.sports', {
resolve: {
customer: ['AuthFactory', function(AuthFactory){
return AuthFactory.getCustomer();
}],
sports: ['SportsFactory', 'customer', function(SportsFactory, customer){
return SportsFactory.getSportsWithLeagues(customer);
}],
leagues: ['sports', function(sports){
var leagueProperties;
// obtain leagueProperties from sports - whatever you do there.
return leagueProperties;
}]
}
});
A little explanation to go with that, when you minify this:
function (AuthFactory) {
return AuthFactory.getCustomer();
}
You get something like this:
function (_1) {
return _1.getCustomer();
}
Now it will try to inject _1 which is not defined so the code will fail. Now when you minify this:
['AuthFactory', function(AuthFactory){
return AuthFactory.getCustomer();
}]
You'll get this:
['AuthFactory', function(_1){
return _1.getCustomer();
}]
And that will keep working because now _1 is assigned to AuthFactory because angular injects the first parameter in the function with the first string in the array.
Reference: https://docs.angularjs.org/guide/di (see: inline array notation)
I'm newbie to AngularJs, and I'm in the process of writing my first unit test; to test the service I wrote a test that simply returns a single Json object. However, everytime I run the test I get the error stated in the title. I don't know what exactly is causing this! I tried reading on $apply and $digest and not sure if that's needed in my case, and if yes how; a simple plunker demo would be appreciated.
here is my code
service
var allBookss = [];
var filteredBooks = [];
/*Here we define our book model and REST api.*/
var Report = $resource('api/books/:id', {
id: '#id'
}, {
query: {
method: 'GET',
isArray: false
}
});
/*Retrive the requested book from the internal book list.*/
var getBook = function(bookId) {
var deferred = $q.defer();
if (bookId === undefined) {
deferred.reject('Error');
} else {
var books= $filter('filter')(allBooks, function(book) {
return (book.id == bookId);
});
if (books.length > 0) {
deferred.resolve(books[0]);//returns a single book object
} else {
deferred.reject('Error');
};
};
return deferred.promise;
};
test
describe('unit:bookService', function(){
beforeEach(module('myApp'));
var service, $httpBackend;
beforeEach(inject(function (_bookService_, _$httpBackend_) {
service = _bookService_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/api/books/1").respond(200, {
"book": {
"id": "1",
"author": "James Spencer",
"edition": "2",
.....
}
});
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should return metadata for single report', function() {
service.getBook('1').then(function(response) {
expect(response.length).toEqual(1);
});
$httpBackend.flush();// error is in this line
});
});
error
Error: No pending request to flush !
at c:/myapp/bower_components/angular-mocks/angular-mocks.js:1439
at c:/myapptest/tests/bookTest.js:34
libs version
AngularJS v1.2.21
AngularJS-mock v1.2.21
I don't see where you're actually issuing a Report.query(). The getBook function just returns an unresolved promise that will never be resolved because nothing in the function is async.
You need to call Report.query via the book function with the promise resolved in the .then() (in the book function). After that, flush the http backend in the service.getBook().then() and do the expect.
I have a controller that looks like this:
angular.module('my_app')
.controller('QueryCtrl', ['$scope', 'Query', function ($scope, Query) {
$scope.onSubmit = function() {
var json = $.post('http://162.243.232.223:3000/api/query', { 'input': 'my_input' });
console.log(json);
$scope.queries = json;
};
}
])
And a view that looks like this:
<div ng-controller="QueryCtrl">
<form ng-submit="onSubmit()" >
<textarea ng-model="query_box" name="my_input"></textarea>
<button type="submit">Submit</button>
</form>
{{ queries }}
<ul ng-repeat="query in queries">
<li>{{query}}</li>
</ul>
</div>
The problem is that, when I click the submit button, the javascript console successfully logs the correct json object, which has a property
responseJSON
You can take a closer look at the object I want by looking typing
$.post('http://162.243.232.223:3000/api/query', { 'input': 'my_input' });
into the javascript console yourself and checking out the object.
However, in the view, when I print out the "queries" object, it appears to not have a responseJSON attribute, but only a readyState attribute.
Where is the rest of my json object?
Try something like this:
$scope.onSubmit = function() {
var json = $.post('http://162.243.232.223:3000/api/query', { 'input': 'my_input' });
json.done(function(result){
$scope.result = result;
$scope.$apply();
})
console.log(json);
$scope.queries = json;
};
}
<div>{{result}}</div>
var projectangular.module('my_app')
.controller('QueryCtrl', ['$scope', 'Query', function ($scope,$http, Query) {
$scope.onSubmit = function() {
$http.post('http://162.243.232.223:3000/api/query', { 'input': 'my_input' }).then(
//sucess
function(response){
angular.copy(response ,$scope.queries);
console.log(json);
},
//error
function(){
alert("cant post");
});
};
}
])
As CAT commented, what you got back from .post is a promise object. You will have to wait for post request to complete (fail or succeed). Following syntax may be little off. I just typed it on the fly.
angular.module('my_app').controller('QueryCtrl', ['$scope', 'Query', function ($scope, Query) {
$scope.onSubmit = function() {
var json = $.post('http://162.243.232.223:3000/api/query', { 'input': 'my_input' }).then(function(response){
console.log(response.data);
$scope.queries = response.data;
$scope.$apply();
}, function(response){
console.log(response.data);
$scope.queries = response.data;
$scope.$apply();
});
};
}])
That's because of the promise system and how the objects are printed in the console, they are printed by reference and not by value. Try doing console.log(JSON.stringify(json)) which is a string and not an object and you will see that you are missing the the responseJSON attribute. you responseJSON attribute is probably attached to the object at a future time but the console.log(Object) will print it's current value, even tho that value was added after you used console.log.
I have a property in the scope that has an id of external object, also I have a filter that expands this id into a full object like this:
{{ typeId | expandType }}
Filter:
.filter('expandType', ['TypeService', function (tsvc) {
return function (id) {
return tsvc.types.get({ id: id });
}
}])
where tsvc.types.get() is normal resource get method with added cache option.
.factory('TypeService', ['$resource', function ($resource) {
var typeResource = $resource('/api/types/:id', { id: '#id' }, {
get: { method: 'GET', cache: true, params: { id: '#id' } }
});
return {
types: typeResource
}
}])
As I understand angular runs additional digest after the fist one just to make sure that nothing changed. But apparently on the next digest the filter is returning a different object and I get the infdig error (digest is executed in infinite loop).
I hoped that if the resource is cached it will return the same object from cache all the time. I can confirm that there is only one trip to server while executing get() so the cache is working.
What can I do to make it work and use the filter to expand ids to full objects?
Although possible, it is usually not a good idea to bind promises to the view. In your case, filters are reevaluated on every digest, and quoting from https://docs.angularjs.org/api/ng/service/$http:
When the cache is enabled, $http stores the response from the server in the specified cache. The next time the same request is made, the response is served from the cache without sending a request to the server.
Note that even if the response is served from cache, delivery of the data is asynchronous in the same way that real requests are.
To clarify, ngResource uses $http internally.
You can still use the filter calling it from your controller:
app.filter('expandType', function ($http) {
return function (id) {
return $http.get('data.json');
};
});
app.controller('MainCtrl', function ($scope, expandTypeFilter) {
var typeId = 'hello';
expandTypeFilter(typeId).success(function (data) {
$scope.expandedTypeId = data[typeId];
});
});
Plunker: http://plnkr.co/edit/BPS9IY?p=preview.
With this approach, if the only reason you were caching the response was to avoid repeated calls to the server, you can now stop caching it so that it gets fresh data later on, but that depends on your needs, of course.
I really wanted to use a filter because it was used all over the app and I didn't want to clutter my controllers. At this point the solution I came out with looks as follows:
.filter('expandType', ['TypeService', function (tsvc) {
var cache = {};
return function (id) {
if (!id) {
return '';
}
var type = cache[id];
if (!type) {
tsvc.types.get({ id: id }).$promise.then(function (data) {
cache[id] = data;
});
cache[id] = {}
return cache[id];
}
else {
return type;
}
}
}])
I'm using typeahead with the angular.js directive but my function to populate the autocomplete makes an asynchronous call and I can't return it to populate the autocomplete. Is there anyway to make it work with this asynchronous call?
Can I assume that you are using the typeahead of Bootstrap 2.x ?
If so, in the documentation, the description of the source field of typeahead()'s options is this:
The data source to query against. May be an array of strings or a
function. The function is passed two arguments, the query value in the
input field and the process callback. The function may be used
synchronously by returning the data source directly or asynchronously
via the process callback's single argument.
You can definitely pass in an async function as the source attr. The source function could be something like:
function someFunction(query, process) {
someAsyncCall(...query or so... , function(res) { // success callback
process(res);
}, function(err) { // error callback
process(null);
});
}
Update:
If you are using Angular Bootstrap's typeahead, it should be even easier. According to Angular Bootstrap's docs(http://angular-ui.github.io/bootstrap/), you can just return a promise for the typeahead function. Some example from the docs:
$scope.getLocation = function(val) {
return $http.get('http://maps.googleapis.com/maps/api/geocode/json', {
params: {
address: val,
sensor: false
}
}).then(function(res){
var addresses = [];
angular.forEach(res.data.results, function(item){
addresses.push(item.formatted_address);
});
return addresses;
});
};
A simpler one could be:
$scope.getSomething= function(query) {
var promise = $http.get('...some url...', {
params: {
queryName: query
}
});
return promise;
};
Or you can build your own promise:
$scope.getSomething= function(query) {
var deferred = $q.defer();
someAsyncCall(...query or so... , function(res) { // success callback
deferred.resolve(res);
}, function(err) { // error callback
deferred.reject(err);
});
return deferred.promise;
};
Actually, many services like $http are just returning promises when you call them.
More about promise in AngularJS: https://docs.angularjs.org/api/ng/service/$q