Using Promises and Services to load Async JS in AngularJS
I haven't had the time to post on my blog for a long while. Been pretty busy. Had to so much to learn and so much to implement. Its a new year, 2015. Lots of goals... need my grind face on. So today's post is about Promises and http dependent services in AngularJS application.
I am a fan of the www.ionicframework.com . It's build on AngularJs and Cordova. It's got pretty cool UI components, JS and CSS. They also got a couple cool CLI utilities for building hybrid cordova apps.
Back to business.
Todays #codefunk is about wrapping XHR calls and non-blocking JS scripts using Promises and services. The problem was, when building a recent location based app, I had to use the HTML5 geolocation API and Google Maps. These, all require external scripts to function meaning, they make Ajax calls to other domains to load the scripts needed.
I needed to get the device current location and load google maps. Usually, Ill just do something like
add the google maps URI in a script tag either at the <head> tag or before the closing </body> tag.
When I do try and navigate to pages that need these resources, I might get undefined objects because the scripts aren't loaded into the DOM yet, even thou angularjs has the view rendered.
So you get errors like ….. is not defined, or ...cannot find property of undefined...something like that from the browser's console.
So first solution. Fix for loading Google Maps .
// Google async initializer needs global function, so we use $window angular.module('GoogleMapsInitializer') .factory('Initializer', function($window, $q){ //Google's url for async maps initialization accepting callback function var asyncUrl = 'https://maps.googleapis.com/maps/api/js?callback=', mapsDefer = $q.defer(); //Callback function - resolving promise after maps successfully loaded $window.googleMapsInitialized = mapsDefer.resolve; // removed () //Async loader var asyncLoad = function(asyncUrl, callbackName) { var script = document.createElement('script'); //script.type = 'text/javascript'; script.src = asyncUrl + callbackName; document.body.appendChild(script); }; //Start loading google maps asyncLoad(asyncUrl, 'googleMapsInitialized'); //Usage: Initializer.mapsInitialized.then(callback) return { mapsInitialized : mapsDefer.promise }; })
source from : http://codereview.stackexchange.com/questions/59678/simple-async-google-maps-initializer-with-angularjs
Seemed to work well. So to in my map service/ factory, I have something like
//var MapApp = angular.module('GoogleMapsInitializer', []); //MapApp.factory('Initializer', Initializer.mapsInitialized .then(function(){ var myLatLng = new google.maps.LatLng(scope.center.lat, scope.center.lon); var mapOptions = { zoom: 17, center: myLatLng, mapTypeId: google.maps.MapTypeId.ROADMAP, panControl: true, zoomControl: true, mapTypeControl: true, scaleControl: false, streetViewControl: false, navigationControl: true, disableDefaultUI: true, overviewMapControl: true }; scope.map = new google.maps.Map(element[0], mapOptions); scope.myLocation = new google.maps.Marker({ position: myLatLng, map: scope.map, title: "My Location" }); google.maps.event.addDomListener(element[0], 'mousedown', function(e) { e.preventDefault(); return false; }); infowindow = new google.maps.InfoWindow(); });
HINT:: remember to pass in necessary dependecies.
2nd Solution, needs a background knowledge on Promises ($q)
Alot of my ng-services for the app, especially $http request required the latitude, longitude of the device current position to be passed in as parameters. I use ngCordova, which already has the $cordovaGeolocation.getCurrentPosition() returning a promise. Using that alone didnt quite work for me because I needed the promise returned from $cordovaGeolocation.getCurrentPosition(posOption) to resolve before I sent off my XHR ($http) request to my server.
So here is what I did. Have you used AngularJS UI router? Check it out. It as a feature called resolve on the $stateProvider.state method. You can read up what resolve does on the AngularJS UI router docs.
So I had a resolve property like this.
//code for resolve on $state
$stateProvider .state('app', { url: "/app", abstract: true, resolve: { geoPosition: function ($q, locationsService) { var q = $q.defer(); var i = locationsService.geoLocationInit(); i.then( function(position) { q.resolve(position); // $scope.position=position; // $scope.base = { // lat: position.latitude, // lon: position.longitude // }; }, function(e) { console.log("Error retrieving position " + e.code + " " + e.message); } ); return q.promise; } } })
this way its, when $q in the above code gets resolved, my controller gets initialized. That means Ill have most likely always have the values for my geoCoordinates in my services and controllers as an argument/ dependency to be injected into my controller as geoPosition .
Well leme start from the top pretty quick so you have an idea of how I got the whole thing working.
Ive got a service whose geoLocationInit method returns a promise that gets fufilled when the $cordovaGeolocation.getCurrentPosition() promise gets resolved. So any other controllers, services that require geo position coordinates need to wait for locationsService.geoLocationInit() to get resolved. Now, I believe a Promise can only be fufilled / resolved once. Any other calls to the a fufilled promise, gets its resolved value,
so here's what's really going on in summarized code..
//in a service as a method of the returned object ///..app.factory('locationsService',..... geoLocationInit: function (posOption) { var q = Q.defer(); var self = this; posOption = posOption || ls_def_pos_option; $cordovaGeolocation.getCurrentPosition(posOption) .then(function (position) { self.setMyLocation(position.coords); q.resolve(position.coords); }); return q.promise; }, //......................../// //in another service or controller locationsService.geoLocationInit() .then(function (geoPromise) { locationData.lon = self.getMyLocation().longitude; locationData.lat = self.getMyLocation().latitude; return q.resolve($http.get('/api/v1/position?' + $.param(locationData))); }, function () { return q.reject(); });
HINT:: Always pass in your necessary dependencies
This way, no matter when the promise is resolved, any callbacks attached to its .then() method will get the resolved value ie. in my case the geolocation coordinates
Hey, I might miss or get a few things wrong.. Please drop ur comments.. #codeFunk is all about posting samples of code , suggestions and link from other answers to help get a problem fixed.














