0

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) {
Anadi Misra
  • 1,925
  • 4
  • 39
  • 68
  • have you logged out what vacancy contain? and also have you debuged and checked that you are getting data in service? – Jorawar Singh Aug 13 '16 at 16:02
  • There is a call made to the service and that gives a success (well 304 now) response, so the data definitely is populated in job_post. Not sure why result does not get populated. undefined it says when I log to console before returning. Quite a noob with JS and Angular, so can't even figure out a good failing test for this, else would have shared that test too – Anadi Misra Aug 13 '16 at 16:06
  • 1
    `return vacancies/result` is executed **before** the `.then()` calls: [Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference](https://stackoverflow.com/questions/23667086/why-is-my-variable-unaltered-after-i-modify-it-inside-of-a-function-asynchron) – Andreas Aug 13 '16 at 16:08
  • yes that's the issue move return statement inside then() after your loop. – Jorawar Singh Aug 13 '16 at 16:13
  • Nada champs! moving it to inside the then block didn't help either ... – Anadi Misra Aug 13 '16 at 16:19

3 Answers3

1

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').

Pankaj Parkar
  • 134,766
  • 23
  • 234
  • 299
  • Okay so basically `$scope.data.vacancy = CareersService.getVacancy($stateParams.job_id);` or `$scope.data.vacancies = CareersService.getVacancies();` both exectue without waiting for the results to be populated? I thought because I',m processing the results under then block in service that would not be the case. – Anadi Misra Aug 13 '16 at 16:12
  • On a curious note, `$scope.data.vacancies = CareersService.getVacancies();` does end up rendering the index view with a list vacancies fetched in first batch, so still a bit confused. – Anadi Misra Aug 13 '16 at 16:14
  • @AnadiMisra here you go, I added explanation for that as well. – Pankaj Parkar Aug 13 '16 at 16:22
  • Now that change gives me '`angular.self.js?body=1:13921 TypeError: Cannot read property 'then' of undefined at new (http://localhost:3000/assets/site/jobs.self.js?body=1:68:60)` which is exactly the line where I'm calling then .... – Anadi Misra Aug 13 '16 at 16:33
  • Have You returned promise of Restangular? Like I shown in answer – Pankaj Parkar Aug 13 '16 at 16:51
  • yes I have, added update to the question. I'm a bit more confused now between promise, deferred and callbacks :-) – Anadi Misra Aug 14 '16 at 06:44
  • What confusion you're having? I explained everything, what else you want? – Pankaj Parkar Aug 14 '16 at 07:34
  • Lol! Naah it's my noon-ness on play here, finally managed writing a test using Jasmine and I realised I was not returning the Promise object :-), thanks for you help – Anadi Misra Aug 14 '16 at 08:13
  • Np, that happens when developer develop a code. Please to hear that my answer helped, Thanks ;) – Pankaj Parkar Aug 14 '16 at 08:17
0

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;
  });

}])
Arun Ghosh
  • 7,634
  • 1
  • 26
  • 38
0

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;
        })           
      }
Jorawar Singh
  • 7,463
  • 4
  • 26
  • 39