1

I've noticed that most questions relating to this subject are with regards to an alternative for the jQuery $(document).ready function in angular, which is angular.element($document).ready however i want a testable/best practice alternative to this.

I am currently injecting Bing Maps, which needs to have loaded before the code in my controller is executed.

currently i wrap the controller code in the document ready:

angular.element($document).ready(function() {
    self.map = new Microsoft.Maps.Map(document.getElementById('map'), {
        credentials: $scope.credentials,
        enableClickableLogo: false,
        enableSearchLogo: false,
        showDashboard: false,
        disableBirdseye: true,
        allowInfoboxOverflow: true,
        liteMode: true,
        minZoom: 2
    });

    $scope.$watch('zoom', function (zoom) {
        self.map.setView({animate: true, zoom: zoom});
    });

    if ($scope.onMapReady) {
        $scope.onMapReady({ map: self.map });
    }

});

Which works, but i'm unable to test it, so i assume this is incorrect usage. I tried setting a variable in the directive, of $scope.loaded = true; as i read that if the directive link function is hit the DOM must be loaded. I then tried replacing the document ready with:

$scope.$watch('loaded', function () {
    self.map = new Microsoft.Maps.Map(document.getElementById('map'), {
       credentials: $scope.credentials,
       enableClickableLogo: false,
       enableSearchLogo: false,
       showDashboard: false,
       disableBirdseye: true,
       allowInfoboxOverflow: true,
       liteMode: true,
       minZoom: 2
    });

    if ($scope.onMapReady) {
        $scope.onMapReady({ map: self.map });
    }
});

$scope.$watch('zoom', function (zoom) {
    self.map.setView({animate: true, zoom: zoom});
});

the 'loaded' watch works as expected but naturally the zoom is hit on load and thats before the map is set. I feel like i could change the document ready to a $timeout function but that seems to be a workaround rather than the correct solution, is there a best practice alternative to angular.element($document).ready that works in the same way but allows me to test it's contents successfully?

gardni
  • 1,384
  • 2
  • 24
  • 51
  • What's the context for this code? Usually Angular app should be bootstrapped when the document is ready, so this wrapper is at least useless inside of a controller. – Estus Flask Jun 21 '17 at 11:14
  • @estus bing maps is called like so: `` which once loaded gives us access to `Microsoft` in the above snippet which apparently takes long enough to only exist in a controller within the doc ready – gardni Jun 21 '17 at 11:23
  • Please, provide a way to replicate the issue as fiddle/plunk, if it's possible without exposing your API key or something. Generally the answer will be 'you should do ready only once with angular.bootstrap', so it's all about interacting with particular script. If there are events on this API that can be hooked up to make sure it's ready, consider using them instead. – Estus Flask Jun 21 '17 at 11:41
  • @estus i think this pretty much replicates the problem, not exactly but it shows a map working with doc ready, and the other fails. https://plnkr.co/edit/j3aJ7OR2UD7eNyfagVOu?p=preview we do as you say with our main app in terms of doc ready bootstrap, however we only bootstrap body, and the script call lives within the head, could this be an issue? – gardni Jun 21 '17 at 12:50
  • Plunker doesn't work well today, but I had a chance to check it in action. No, the issue is more complicated. Initial API script loads more of them and they aren't loaded/completed at the moment when the app is initialized, even though these scripts are loaded without `async` attribute. I had a similar race condition [here](https://stackoverflow.com/questions/41655123/gtm-randomly-skips-initial-pageview-in-single-page-app) and ended up with delaying app initialization with window `load` event. – Estus Flask Jun 21 '17 at 17:09

1 Answers1

5

Generally Angular application is already bootstrapped on document ready. This is default behaviour for automatic bootstrapping with ng-app, and manual bootstrapping with angular.bootstrap should be performed on ready as well.

The question is specific to current case (Microsoft's Bing Maps API). Considering that ready is suggested by Microsoft, a developer is on his/her own with better alternatives.

<script src="https://www.bing.com/api/maps/mapcontrol"></script>

is loaded synchonously, but it triggers a number of dependencies to load which aren't loaded yet at the moment when initial document ready is triggered. Actually, it requires ready inside another ready in order to complete the initialization, this is exactly what the original code and Microsoft example show, and it doesn't look very good.

In order to avoid race conditions application bootstrap can be postponed to the moment when all prerequisites will be loaded, i.e. window load event instead of document ready. It may provide considerable delay but it guarantees that scripts that the application relies on were loaded, regardless of how their transport is performed:

angular.element(window).on('load', () => {
  angular.bootstrap(document.body, ['app']
});

The alternative that API provides to control initialization process is global callback function:

<script src="https://www.bing.com/api/maps/mapcontrol?callback=globalCallbackName"></script>

A callback can be packed with a service instead of relying on <script>:

angular.module('bingMaps', [])
.factory('bingMapsLoader', ($q, $window, $document, $timeout) => {
  var script = document.createElement('script');
  script.src = 'https://www.bing.com/api/maps/mapcontrol?callback=bingMapsCallback';
  script.async = true;

  $document.find('body').append(script);

  return $q((resolve, reject) => {
    $window.bingMapsCallback = resolve;
    $timeout(reject, 30000);
  });
});

bingMapsLoader promise can be chained to guarantee that API was initialized, put into router resolver, etc.

Additionally, controller constructor is executed before directive is being compiled. Whether third-party APIs are used or not, it is correct to move all DOM-specific code to pre/post link function in Angular 1.4 and lower and to controller $onInit or $postLink hook in Angular 1.5 or higher:

app.controller('FooController', function (bingMapsLoader) {
  this.$postLink = () => {
    bingMapsLoader.then(() => this.mapsInit());
  };

  this.mapsInit = () => {
    Microsoft.Maps.Map(...);
  };
  ...
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • i took a slightly different approach in the end, but the main principle was based on your answer, packing the callback in a service and using $window.callback. fully testable and working correctly, thanks for your effort! – gardni Jun 23 '17 at 10:57