dylanfprice / angular-gm

AngularJS Google Maps Directives
MIT License
197 stars 47 forks source link

Crashes AngularJS when used offline #26

Closed endareceo closed 10 years ago

endareceo commented 10 years ago

I tried bundling this in a PhoneGap app, but offline the Google Maps library isn't available. This causes angular-gm to crash the entire angular application.

Error: [$injector:unpr] Unknown provider: angulargmContainerProvider <- angulargmContainer at hasOwnPropertyFn (file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:60:12) at file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:2964:19 at Object.getService as get at file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:2969:45 at getService (file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:3086:39) at invoke (file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:3107:13) at Object.instantiate (file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:3141:23) at $get (file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:5554:28) at update (file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular-route.js:831:32) at Object.$get.Scope.$broadcast (file:///Users/bram/Documents/Phonegap/bpost/www/lib/angularjs/angular.js:10207:28)

joe1chen commented 10 years ago

I ran into a similar issue and was looking into loading Google Maps asynchronously as described here:

http://stackoverflow.com/questions/11217002/how-to-asyncronously-load-a-google-map-in-angularjs

dylanfprice commented 10 years ago

Hi Bram,

Can you make a repro of your issue? Best would be a jasmine test, but a plunkr/jsfiddle would also be fine.

Dylan

joe1chen commented 10 years ago

It's really easy to repro this issue. Simply comment out the line that loads the google maps api.

AngularGM should not try to initialize when google maps is not loaded.

endareceo commented 10 years ago

That's correct, just excluding the google maps library will do. Or run the app offline.

dylanfprice commented 10 years ago

Ok, I just pushed out version 0.3.1, which guards the initialization of angulargmDefaults in a function so no google.maps.XXX references get made when the module script is being read.

Beyond that, I can't guard every reference to google.maps at runtime--AngularGM requires google maps and doesn't make any sense without it. So the way to approach your problem is to conditionally include the module:

angular.module('myModule', []);

if (window.google != null && window.google.maps != null) {
  angular.module('myModule').requires.push('AngularGM');
}

You will also have to manually guard any of your own references to google.maps or angulargm services.

Let me know if that works for you.

endareceo commented 10 years ago

Thanks, I'll try it. I actually use AngularJS and angular-gm in a PhoneGap app. In this context it makes sense to test for offline usage.

joe1chen commented 10 years ago

@endareceo We are also using angular-gm in a PhoneGap app. Due to this bug, we ended up having to switch out from using angular-gm to using our own custom google maps code.

We based our custom code on angular-gm's reuse of map insances One of the biggest problems solved by angular-gm was a huge memory leak when creating maps over and over. Angular-gm solves that memory leak by re-using map instances.

The link I posted above as well as this gist https://gist.github.com/gbakernet/828536 were both useful in our custom implementation. I rewrote the loader to use more of angularjs $q instead of using jquery's implementation. Here's what our code looks like:

AngularJS service:

'use strict';

angular.module('myApp').
  factory('googleMaps', function ($window, $q) {
    var promise;
    var maps = {};

    function addMap(mapId, map) {
      if (!(map instanceof google.maps.Map)) {
        throw 'map not a google.maps.Map: ' + map;
      } else if (mapId in maps) {
        throw 'already contain map with id ' + mapId;
      }
      maps[mapId] = map;
    }

    function getMap(mapId) {
      return maps[mapId];
    }

    function removeMap(mapId) {
      if (mapId in maps) {
        delete maps[mapId];
      }
    }

    function clear() {
      maps = {};
    }

    function createMap($element, options) {
      if (!($window.google && $window.google.maps)) {
        return null;
      }

      var mapId = options.id;
      delete options.id;

      var map = getMap(mapId);
      if (!map) {
        // Set center
        var lat = options.center[0];
        var lng = options.center[1];
        options.center = new google.maps.LatLng(lat, lng);

        // Set type
        options.mapTypeId = google.maps.MapTypeId.ROADMAP;

        map = new google.maps.Map($element[0], options);
        addMap(mapId, map);
      } else {
        $element.replaceWith(map.getDiv());
      }
      return map;
    }

    function load(version, apiKey, language) {
      if ( promise ) {
        return promise;
      }

      // Deferred
      var mapsDeferred = $q.defer();

      // Default Parameters
      var params = angular.extend({sensor: false }, apiKey ? {key: apiKey} : {} , language ? {language: language} : {});

      var resolve = function () {
        mapsDeferred.resolve($window.google && $window.google.maps ? $window.google.maps : false);
      };

      // If google.maps exists, then Google Maps API was probably loaded with the <script> tag
      if( $window.google && $window.google.maps ) {
        resolve();
      }
      else if ($window.google && $window.google.load) {
        $window.google.load('maps', version || 3, {'other_params': $.param(params) , 'callback' : resolve});
      }
      else {
        var callbackName = 'onLoadGoogleMapsComplete';
        var script = $window.document.createElement('script');
        script.type = 'text/javascript';
        script.src = 'http://maps.googleapis.com/maps/api/js?v=' + version + '&' + $.param(params) + '&callback=' + callbackName;
        $window[callbackName] = function() {
          resolve();
        };
        $window.document.body.appendChild(script);
      }

      promise = mapsDeferred.promise;

      return promise;
    }

    return {
      load: load,
      addMap: addMap,
      getMap: getMap,
      removeMap: removeMap,
      createMap: createMap,
      clear: clear
    };

  });

Then here's our custom maps directive:

'use strict';

angular.module('myApp')
  .directive('maps', function (googleMaps) {
    return {
      restrict: 'A',
      scope:{}, // Isolate scope
      link: function postLink(scope, element, attrs) {

        // Evaluate attributes
        var gmOptions = scope.$eval(attrs.maps);

        scope.map = null;

        var version = 3;
        var apiKey = 'your_google_maps_key';

        googleMaps.load(version, apiKey).then(function() {
          scope.map = googleMaps.createMap(element, gmOptions);

          if (scope.map) {
            // Do more stuff with map here.
          }
        });
      }
    };
  });

And the maps html:

<div maps="{ id: 'map', zoom: 3, center: [37.77493, -122.419416], disableDefaultUI: true }" id="map"></div>

Note that the directive takes the attributes from HTML evaluates them into gmOptions, then passes them into the googleMaps service when creating the map.

googleMaps.load() returns a promise that is resolved when the google maps javascript has been loaded. Thus, createMap() is not called until google maps javascript has been loaded. Note that there are no references to google.maps.* classes until after load() is resolved. This solves the offline problem.

The other benefit of this async loading is that angularjs loads much faster without blocking on the google maps js. Previously maps js would be loaded even if the current page is not using a map. Now, maps are loaded on demand, only when needed.

It would be nice if this approach were adopted by angular-gm at some point in the future.

endareceo commented 10 years ago

Thanks dylanfprice, 0.3.1 solves my issues.

ffabreti commented 9 years ago

@joe1chen, I'm using Ionic and angular-gm doesn't react well when offline. Thank you so much for sharing your code, will give it a try.