11

I have a service like

app.factory('geolocation', function ($rootScope, cordovaReady) {
    return {
        getCurrentPosition: cordovaReady(function (onSuccess, onError, options) {

            navigator.geolocation.getCurrentPosition(function () {
                var that = this,
                    args = arguments;

                if (onSuccess) {
                    $rootScope.$apply(function () {
                        onSuccess.apply(that, args);
                    });
                }
            }, function () {
                var that = this,
                    args = arguments;
                if (onError) {
                    $rootScope.$apply(function () {
                        onError.apply(that, args);
                    });
                }
            }, options);
        }),
        getCurrentCity: function (onSuccess, onError) {
            this.getCurrentPosition(function (position) {
                var geocoder = new google.maps.Geocoder();
                geocoder.geocode(options,function (results, status) {
                    var city = address_component.long_name;
                });
            });
        }
    }
});

And I want to do from a controller something like

function MainCtrl($scope, geolocation) {
   geolocation.getCurrentCity(function(city){
       $scope.city = city;
   });
};

The getCurrentPosition works fine and the city is determined too, however I don't know how to access the city in controller.

What happens? When getCurrentCity is called, it calles getCurrentPosition to determine the gps coords. This coords are passed as arguments to the onSuccess method right? So this is quite the same I want to do in the getCurrentCity method, but I don't know how. Ones the async geocoder retrieved the city, I want to apply the new data to the onSuccess method.

Any ideas?

DarkLeafyGreen
  • 69,338
  • 131
  • 383
  • 601

4 Answers4

29

You are dealing with callbacks and asynchronous request. So you should use $q service. Just inject it in your service with $rootScope and cordovaReady dependency. And add the promises to your function like this

getCurrentCity: function () {
    var deferred = $q.defer(); 
    this.getCurrentPosition(function (position) {
      var geocoder = new google.maps.Geocoder();
      geocoder.geocode(options,function (results, status) {
        var city = address_component.long_name;
        $rootScope.$apply(function(){
          deferred.resolve(city);
        });
      });
    });
    return deferred.promise;
}

And in your controller, do the following to handle the promise.

function MainCtrl($scope, geolocation) {
   geolocation.getCurrentCity().then(function(result) {  //result === city
     $scope.city = result;
     //do whatever you want. This will be executed once city value is available
   });     
};
Rishabh Singhal
  • 1,173
  • 10
  • 22
  • Fixed my problem, I had a filter that was causing the loop. – Ryan Smith May 30 '14 at 17:01
  • The first approach in the above controller implementation of setting a $scope property to a promise does not work in Angular 1.3 any more. Promises are not unwrapped any more, so you have to use the second approach (setting the $scope property in the then handler) – jbandi Jan 14 '15 at 18:39
  • @jbandi: You seem to suggest that something has changed. I didn't get what's the exact change. Do you mind clarifying it for me? I will update the answer accordingly. – Rishabh Singhal Jul 29 '15 at 21:00
  • The code snipped above contains the line `$scope.city = geolocation.getCurrentCity();`. This line assigns a promise directly to the scope and relies on Angular to "unwrapp" this promise, this means that wehn the promise is resolved, Angular will update $scope.city to the value that was the result of the promise. This feature of Angular of "unwrapping" promises on the $scope is no longer present in Angular 1.3 and above (see https://docs.angularjs.org/guide/migration) – jbandi Aug 05 '15 at 08:17
  • You can read more about the reasons for removing the "promise unwrapping" feature here: http://michalostruszka.pl/blog/2013/12/05/angular-promises-unwrapping/ and here https://github.com/angular/angular.js/commit/5dc35b527b3c99f6544b8cb52e93c6510d3ac577 – jbandi Aug 05 '15 at 08:17
  • 1
    @jbandi I have edited the code to reflect the current angular practise. Is that correct? – Rishabh Singhal Aug 05 '15 at 10:09
5

Try this

function MainCtrl($scope, geolocation) {
   $scope.city = null;
   geolocation.getCurrentCity(function(city){
       $scope.city = city;
       if(!$scope.$$phase) {
            $scope.$digest($scope);
        }
   });
};

Sometimes, the watcher is not always called when instanciating the controller, by forcing a digest event, if the scope is not in phase, you can go to where you want to go, I believe. Let me know if I didn't understand your question.

Oh and I don't know if i read the code properrly but it seems that you're not calling the onSuccess callback in your function: replace your getCurrentCity function by this:

getCurrentCity: function (onSuccess, onError) {
        this.getCurrentPosition(function (position) {
            var geocoder = new google.maps.Geocoder();
            geocoder.geocode(options,function (results, status) {
                var city = address_component.long_name;
                if(typeof onSuccess === 'function') {
                     onSuccess(city);
                }
            });
        });
    }
Abdoul Sy
  • 580
  • 3
  • 10
4

Just call your onSuccess method in the service instead of handling the result there.

getCurrentCity: function (onSuccess, onError) {
    this.getCurrentPosition(function (position) {
        var geocoder = new google.maps.Geocoder();
        geocoder.geocode(options, onSuccess);
    });
}

And in your controller, parse the results and assign the city:

function MainCtrl($scope, geolocation) {
   geolocation.getCurrentCity(function(results, status){
       // Parse results for the city - not sure what that object looks like
       $scope.city = results.city;
   });
};
Terry
  • 14,099
  • 9
  • 56
  • 84
3

Correct me if I'm wrong, but the presented solution won't work completely anymore, since newer Angular versions (>1.2!?) do no longer unwrap $q promises automatically.

Thus:

$scope.city = geolocation.getCurrentCity();

will always be a promise. So you will always have to use:

geolocation.getCurrentCity().then(function(city) {
    $scope.city = city;
}
Daniel
  • 597
  • 11
  • 19