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:
I think this is not the correct way to do this.
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
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;
});
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.
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.
Related
I am trying to bind data to $scope within a callback function and display this in an html element.
Below is my angular code:
gAutocomplete.controller('geocoder', ['gdata', function($scope, gdata){
var geocoder = L.mapbox.geocoder('mapbox.places');
geocoder.query('New York', assign_geocode2());
function assign_geocode2() {
function assign_geocode(err, data) {
console.log(data);
$scope.lat = data.latlng[0];
$scope.lng = data.latlng[1];
console.log($scope.lat)
}
return assign_geocode;
};
}])
Below is HTML:
</div>
<div class="spacer50"></div>
<div class="center-block" style="width:600px" ng-cloak data-ng- controller='geocoder'>
{{"Chosen lat/long are"}} {{$scope.lat}} {{$scope.lng}}
</div>
I can see the controller gets executed, callback function is called and values are written to console.log. However, they are not propogated to HTML element. What could be happening?
Update
I am not using $timeout as below and getting errors that $timeout is not a function. i know I am using an intermediate tmp variable, but when I use $timeout in the closure, I still have the same issue.
gAutocomplete.controller('geocoder', ['$scope', 'gdata', '$timeout', function($scope, $timeout, gdata) {
var tmp = {}
var geocoder = L.mapbox.geocoder('mapbox.places');
geocoder.query('New York', assign_geocode2(tmp));
function assign_geocode2(tmp) {
function assign_geocode(err, data) {
tmp.lat = data.latlng[0],
tmp.lng = data.latlng[1]
}
return assign_geocode;
}
$timeout(function() {
$scope.lat = tmp.lat,
$scope.lng = tmp.lng,
console.log($scope)},0);
}
])
You're changing scope values from a non-angular event handler. This means you need to notify angular that, "hey, I've updated things, take note pls". AFAIK the ideal way of taking care of this is running the callback inside a $timeout call.
function assign_geocode(err, data) {
$timeout(() => {
console.log(data);
$scope.lat = data.latlng[0];
$scope.lng = data.latlng[1];
console.log($scope.lat)
});
}
Running this inside $timeout will cause angular to run a digest cycle and update scope values. You don't need to do this from events initiated by Angular, because it already knows its in a digest cycle. For example, services like $http take care of this for you.
Scope is the glue between application controller and the view. During the template linking phase the directives set up $watch expressions on the scope. The $watch allows the directives to be notified of property changes, which allows the directive to render the updated value to the DOM.
...
{{"Chosen lat/long are"}} {{lat}} {{lng}}
...
Example :
http://plnkr.co/edit/5TJJkYf21LlwPyyKjgTv?p=preview
https://docs.angularjs.org/guide/scope
I can find bits and pieces of how to solve this, but no concrete way to make it work.
I have an asynchronous call to a server to fetch data in AngularJS and wish to store it in a variable. This variable then needs to be accessible to all the directives in the app, but they obviously all need to wait for the variable to be assigned before they can use it. I'm also using TypeScript and its export functionality to spin directives from their own functions.
Controller
export class MainController{
fundData: Object;
constructor(scope, FundService) {
FundService.fetchData('some_param').then(d => {
let data = d[0],
fundProps = data.properties_pub;
this.fundData = {
'isin': data.clientCode,
'nav': fundProps.nav.value,
'nav_change': fundProps.nav_change.value.toFixed(2),
'nav_change_direction': change,
'total_aum': fundProps.net_asset.value.toFixed(2)
};
scope.ctrl = this;
});
}
}
Directive
class OverviewController {
scope: ng.IScope;
constructor(scope){
scope.$watch('data', newVal => {
console.log(newVal);
});
}
}
OverviewController.$inject = ['$scope'];
export function overview(): ng.IDirective {
return {
restrict : "C",
controller : OverviewController,
controllerAs : "overview",
template : require("../templates/overview"),
bindToController :{
data: '='
}
}
}
HTML
<div ng-controller="MainController">
<div class="overview" data="ctrl.fundData"></div>
</div>
Bootstrap Process
let module = angular.module(MODULE_NAME,[])
.controller('MainController', ['$scope','FundService', MainController])
.service('FundService', FundService)
.directive('overview', overview);
Things I've Tried:
$rootScope
I can set something static and share it, so this works:
$rootScope.data = 2;
This doesn't:
someFunction().then(data => { $rootScope.data = data });
Maybe there's something about promises in $rootScope I don't understand.
Setting in controller
I can set the result of the call to a variable in the controller, set that to an attribute in the directive, and then bind the attribute to its controller, but this doesn't work either, even if I use $watch on the variable.
What I would do is fetch the data, store it in a service (which I think you are already doing) and then broadcast an event when the data in the service is updated. Here's an example (in raw javascript)
module.service('DataService', function(rootScope) {
...
var data;
services.setData = function(newData) {
data = newData;
rootScope.$broadcast('DataUpdated');
};
...
});
And then in your directives all you would need to do is listen for the 'DataUpdated' event:
scope.$on('DataUpdated', ...);
Hope that helps!
I'm trying to hold a global MainCtrl controller that serves the navigation menus. From time to time these menu items should be updated by various controllers.
Now I thought I might just bind the navigation links to the controller, and update the controller variable as follows:
<div ng-controller="MainCtrl">
<li ng-repeat="nav in navigations">
{{nav.label}}
</li>
</div>
<div ng-view></div> <!-- renders different controllers, eg testController.js -->
app.controller('MainCtrl', ['$scope', 'navigationService', function($scope, navigationService) {
//binding the property of the service
$scope.navigations = navigationService.navigations;
}]);
app.service('navigationService', function() {
return {
navigations: null
};
});
But, when calling the service and updating the navigations variable inside, nothing is changed in the view. Why?
angular.module('test').controller('testController', ['$scope', '$http', 'navigationService', function($scope, $http, navigationService) {
$http.get(url)
.success(function(data) {
navigationService.navigations = data.some.navigations; //assume json data exists
});
}]);
How can I achieve this two-way databinding, forcing a view update from one controller to another?
You are returning a primitive from service. A primitive doesn't have inheritance.
Return an object instead:
app.service('navigationService', function() {
var nav ={}; // object that will be returned
function init(){
$http.get(url)
.success(function(data) {
// modify object returned from service rather than reassign a primitive value
nav.items = data.some.navigations; exists
});
}
init();//make request to load the data
return { // can add more properties if needed
nav: nav
};
});
Then in controller:
$scope.navigations = navigationService.nav;
// will initially be {} and later will inherit items property
In view
<div ng-repeat="item in navigations.items">
angular internal watches will pick up the changes now made to the object and render view accordingly
After using Angular for more than 2 years, I discovered, whenever you want that functionality with multiple binding from different services/controllers/directives, ALWAYS use json property, and NEVER ovverride variable instance:
I would replace that:
$scope.navigations = navigationService.navigations;
with that:
var state = {
navigations: []
};
$scope.state = state;
state.navigations = navigationService.navigations; // i prefer such syntax
// or
$scope.state.navigations = navigationService.navigations;
Why? Probably because of Angular automatic $watch()/$watchCollection() functions, which are bind to variable changes.
You need to use the $rootscope and broadcast to and keep eye on broadcast
Say your data is changed from x controller, so here you can broadcast like this
$rootScope.$broadcast('menu-need-to-refresh');
In your main controller keep eye, like this
$scope.$on('menu-need-to-refresh', function (event, data) {
$scope.menus= service.newMenu();
});
Hope it will help you
I solved a similar problem simply by using $scope.$watch
ex:
$scope.$watch(
function(){return navigationService.navigations;},
function(newVal, oldVal){$scope.navigations = newVal;}
)
this code is not tested, but you get the gist
Update : #charlietfl + #Dmitri Algazin method is more elegant as it takes advantage of javascript itself, by using references, and avoid using two watchers in controller + ngRepeat (watchCollection in this directive will do the work).
My original answer :
You should watch for changes in the service from MainCtrl using $scope.$watch :
app.controller('MainCtrl', ['$scope', 'navigationService', function($scope, navigationService) {
//binding the property of the service
$scope.$watch(
function(){
return navigationService.navigations;
}
, function(newValue, oldValue){
if(newValue !== oldValue){
$scope.navigations = newValue;
}
})
}]);
I'm trying to retrieve a list of options from our database and I'm trying to use angular to do it. I've never used services before but I know that's going to be the best way to accomplish what I want if I'm going to use data from my object in other controllers on the page.
I followed a couple tutorials and put together a factory that makes an http request and returns the data. I've tried several ways of doing it, but for some reason nothing is happening. It's like it never runs the factory function and I can't figure out why.
Factory:
resortModule= angular.module('resortApp',[]);
resortModule.factory('locaService',['$http', function ($http){
var locaService= {};
locaService.locations = {};
var resorts = {};
locaService.getLocations=
function() {
$http.get('/url/url/dest/').success(function (data) {
locaService.locations = data;
});
return locaService.locations;
};
return locaService;
//This is a function I would like to run in addition to the first one so multiple variables would be stored and accessible
/*getResorts:
function(destination) {
$http.get('/url/url/dest/' + destination.id).success(function (data) {
resorts = data;
});
return resorts;
}*/
}]);
resortModule.controller('queryController',['$scope', 'locaService', function($scope, locaService) {
$scope.checkConditional= function (){
if($("#location").val() == ""){
$("#location").css('border','2px solid #EC7C22');
}
};
$scope.selectCheck= function (){
$("#location").css('border','2px solid #ffffff');
$(".conditional-check").hide();
};
$scope.resort;
$scope.locations= locaService.getLocations();
}]);
I just want the data to be returned and then assigned to the $scope.locations to be used for ng-options in the view. Then I want my other function to run on click for the next field to be populated by the variable resort. How would I do this? Any help would be great! Thanks!
$http service returns a promise, and your function should return that promise. Basically your getLocations function should be something like the following
locaService.getLocations=
function() {
return $http.get('/url/url/dest/');
};
Then in your controller you should retrieve the options using this promise:
locaService.getLocations()
.then(
function(locations) // $http returned a successful result
{$scope.locations = locations;}
,function(err){console.log(err)} // incase $http created an error, log the returned error);
Using jquery in controllers or manipulating dom elements in controllers is not a good practice, you can apply styles and css classes directly in views using ng-style or ng-class.
Here is an example how all it should look wired up:
resortModule= angular.module('resortApp',[]);
resortModule.factory('locaService',['$http', function ($http){
var locaService= {
locations: {}
};
var resorts = {};
locaService.getLocations= function() {
return $http.get('/url/url/dest/');
};
return locaService;
//This is a function I would like to run in addition to the first one so multiple variables would be stored and accessible
/*getResorts:
function(destination) {
$http.get('/url/url/dest/' + destination.id).success(function (data) {
resorts = data;
});
return resorts;
}*/
}]);
resortModule.controller('queryController',['$scope', 'locaService', function($scope, locaService) {
/* Apply these styles in html using ng-style
$scope.checkConditional= function (){
if($("#location").val() == ""){
$("#location").css('border','2px solid #EC7C22');
}
};
$scope.selectCheck= function (){
$("#location").css('border','2px solid #ffffff');
$(".conditional-check").hide();
};
*/
$scope.resort;
locaService.getLocations()
.then(
function(locations) // $http returned a successful result
{$scope.locations = locations;}
,function(err){console.log(err)} // incase $http created an error, log the returned error);
}]);
I'm confused, I have this module which routes to different controllers:
var mainModule = angular.module('lpConnect', []).
config(['$routeProvider', function ($routeProvider) {
$routeProvider.
when('/home', {template:'views/home.html', controller:HomeCtrl}).
when('/admin', {template:'views/admin.html', controller:AdminCtrl}).
when('/connect', {template:'views/fb_connect.html', controller:MainAppCtrl}).
otherwise({redirectTo:'/connect'});
}]);
and a Common service like so:
mainModule.factory('Common', ['$rootScope', '$http', function (scope, http) {
var methods = {
changeLanguage:function (langID) {
http.get('JSON/langs/' + langID + '/captions.json').success(function (data) {
scope.lang = data;
});
},
initChat:function () {
console.log(scope); // full object
console.log(scope.settings); // undefined
}
};
//initiate
http.get('JSON/settings/settings.json').success(function (data) {
scope.settings = data;
methods.changeLanguage(scope.settings.lang);
});
return methods;
}]);
the app loads and gets (through XHR) the settings object, and I can see the settings reflects in my DOM. (captions for example)
Now when I call the initChat method from my HomeCtrl I get an undefined value when trying to access the scope.settings property ... what's strange is that when I log the scope I can see the settings object ... What am I missing?
Update: I found out that what I'm doing wrong is calling my method directly from the controller body:
function HomeCtrl($scope, $location, Common) {
...
Common.initChat()
...
}
if I change the call to be triggered by a click all works fine, but I do need this code to run when the page loads, What is the right approach?
It's a simple problem, I think: You're calling initChat in your scope before the $http call retrieves scope.settings.
Couple of things.
http is async and that is your main problem (as Andy astutely pointed out)
ng-init is not recommended for production code, initializing in controllers is better
initializing your scope.settings = {} or a decent default may help you, once xhr is done then your settings will be available.
hope this helps
--dan