AngularJS retrieve data via AJAX before Directive runs

All we need is an easy explanation of the problem, so here it is.

I’m using AngularUI’s uiMap directives to instantiate a google map. The uiMap directive works great with hard-coded data ({mapOptions} and [myMarkers]); however I run into trouble when I retrieve this data via $http.get() (the directive fires before the AJAX call has finished).

Initially I was executing the GET in my GoogleMaps controller, but when I realised things were happening out of sequence, I moved the GET into the uiMap directive. I’ve got 2 problems with this:

  1. I think this is not the correct way to do this.
  2. The GET also retrieves the data for [myMarkers]
    • The function/directive that creates the markers is ubiquitous in that it is responsible for creating all overlays

So my question is, is there somewhere else in the application where I can retrieve the data (and apply it to scope) before the directive runs?

I read up on $q, and that kind of sounds like what I want, but I’m not sure if I can do it within my controller rather than in the directive (also not sure how $q.defer.resolve() is any different than $http.success()).

EDIT Most of the code I’m using is copy/paste from AngularUI’s doc, but here’s a plunk: http://plnkr.co/edit/t2Nq57

Solution

Based on Andy’s answer, I used a combination of uiMap and uiIf:

<!-- index.html -->
<div
  id="map_container"
  ng-controller="GoogleMaps">

  <div ui-if="mapReady">

    <div
      ng-repeat="marker in markers"
      ui-map-marker="markers[$index]"
      ui-event="{'map-click':'openMarkerInfo(marker)'}"
    ></div>

    <div
      ui-map-info-window="myInfoWindow"
      ng-include="'infobox.html'"
    ></div>

    <div
      id="map_canvas"
      ui-map="myMap"
      ui-options="mapOptions"
    ></div>

  </div>

</div>

Caveat 1 uiIf cannot be in the same element that specifies the controller furnishing its condition (uiIf has higher priority than ngController, so its controller won’t get set before uiIf executes).

Caveat 2 Be sure to use the most recent version of uiIf (the version supplied in the most recent tag, v0.3.2, is out of date). The old one has bug causing a TypeError under certain circumstances.

Caveat 3 jQuery MUST be included before AngularJS (in index.html); else you will receive a TypeError stating that Object [object Object] has no method 'trigger' (or Object [object HTMLDivElement] has no method 'trigger' on Windows). Chrome will allow you to step into the trigger function because Chrome knows about it, but Angular does not (and Angular is throwing the error).

function GoogleMaps( $scope , $http )
{

  var mapDefaults = {
    center:    new google.maps.LatLng(25,-90),//centres on Gulf of Mexico
    zoom:      4,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  };

  $scope.mapOptions = {};
  $scope.mapReady = false;
  $scope.markers = [];

  $http.get('map.json').then(function mapData(response) {

    var map_data = response.data,
      user_defaults = map_data.user.defaults; //{center: [lat,lng], zoom: 15}

    $scope.mapOptions = {
      "center":    (typeof user_defaults.center !== 'undefined') ?
        new google.maps.LatLng(user_defaults.center[0],user_defaults.center[1])
        : mapDefaults.center,
      "zoom":      (typeof user_defaults.zoom !== 'undefined') ?
        parseInt(user_defaults.zoom,10)
        : mapDefaults.zoom,
      "mapTypeId": mapDefaults.mapTypeId
    };

    //working on code to populate markers object

    $scope.mapReady = true;

  });

  // straight from sample on http://angular-ui.github.com/#directives-map
  $scope.addMarker = function($event) { … };
  $scope.openMarkerInfo = function(marker) { … };
  $scope.setMarkerPosition = function(marker, lat, lng) { … };

}//GoogleMaps{}

Drawback uiMap does not currently support rendering makers on domready. I’m looking into an alternative version of uiMapMarker suggested in this GitHub issue / comment.

Solution to this issue: https://stackoverflow.com/a/14617167/758177

Working example: http://plnkr.co/edit/0CMdW3?p=preview

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

You could just delay execution of ui-map until your data is loaded.

HTML:

<div ui-if="loadingIsDone">
  <div ui-map="myMap" ui-options="myOpts"></div>
</div>

JS:

$http.get('/mapdata').then(function(response) {
  $scope.myOpts = response.data;
  $scope.loadingIsDone = true;
});

Method 2

Generally, what you can do is have your directive get set up, start the load and finish in the success. I’m assuming you want to load one piece of data for all instances of your directive. So here’s some psuedo-code for how you might want to attack this:

app.directive('myDelayedDirective', ['$http', '$q', function($http, $q) {

  //store the data so you don't load it twice.
  var directiveData,
      //declare a variable for you promise.
      dataPromise;

  //set up a promise that will be used to load the data
  function loadData(){ 

     //if we already have a promise, just return that 
     //so it doesn't run twice.
     if(dataPromise) {
       return dataPromise;
     }

     var deferred = $q.defer();
     dataPromise = deferred.promise;

     if(directiveData) {
        //if we already have data, return that.
        deferred.resolve(directiveData);
     }else{    
        $http.get('/Load/Some/Data'))
          .success(function(data) {
              directiveData = data;
              deferred.resolve(directiveData);
          })
          .error(function() {
              deferred.reject('Failed to load data');
          });
     }
     return dataPromise;
  }

  return {
     restrict: 'E',
     template: '<div>' + 
          '<span ng-hide="data">Loading...</span>' +
          '<div ng-show="data">{{data}}</div>' + 
        '</div>',
     link: function(scope, elem, attr) {
         //load the data, or check if it's loaded and apply it.
         loadData().then(function(data) {
             //success! set your scope values and 
             // do whatever dom/plugin stuff you need to do here.
             // an $apply() may be necessary in some cases.
             scope.data = data;
         }, function() {
             //failure! update something to show failure.
             // again, $apply() may be necessary.
             scope.data = 'ERROR: failed to load data.';
         })
     }
  }
}]);

Anyhow, I hope that helps.

Method 3

I am not sure if this will help without seeing code, but I ran into this same issue when I was creating my $scope.markers object inside the $http.success function. I ended up creating the $scope.markers = [] before the $http function, and inside the .success function, I populated the $scope.markers array with the return data.

So the $scope object was bound while the directive was compiling, and updated when the data returned.

[UPDATE SUGGESTION]

Have you tried taking advantage resolve in your route?

  function($routeProvider) {
    $routeProvider.
      when(
        '/',{
          templateUrl: 'main.html',
          controller: Main,
          resolve: {
               data: function(httpService){
                   return httpService.get()
               }
          }
      }).
      otherwise({redirectTo: '/'});
  }

I usually put my $http requests in a service, but you could call the $http right from your route:

App.factory('httpService'), function($http){
     return {
       get: function(){
           $http.get(url)
       }
    }
});

Then, in your controller, inject data and set your $scope items to the data.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply