Reputation: 31580
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:
[myMarkers]
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
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
Upvotes: 16
Views: 26704
Reputation: 108471
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.
Upvotes: 6
Reputation: 1484
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.
Upvotes: 1
Reputation: 43023
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;
});
Upvotes: 31