AngularJs: How to pass scope from within $http.get, into directive? - angularjs

I'm having trouble trying to populate a vector map from data in my database, using angular & directives.
I have an angular dashboard webpage that needs to display an svg vector map of the United States populated with data from my database. I'm following this tutorial, and everything works fine, however the tutorial passes hard coded values to the map. I need to modify this to accept database values, & that's where I'm having trouble. How can I make a call to my database and pass those values back to map, using a directive?
I'm still new to angular & this is my first experience with directives, but it appears as if the $http.get call happens after the directive, so I'm returning my db data too late. Here's my code:
App.js
Here, I'm using both directives for this map functionality. This all works fine:
var app = angular.module('dashboardApp', ['ngRoute', 'angularjs-dropdown-multiselect']);
app.controller('dashboardController', DashboardController);
app.directive('svgMap',function ($compile) {
return {
restrict: 'A',
templateUrl: '/Content/Images/Blank_US_Map.svg',
link: function (scope, element, attrs) {
var regions = element[0].querySelectorAll('path');
angular.forEach(regions, function (path, key) {
var regionElement = angular.element(path);
regionElement.attr("region", "");
regionElement.attr("dummy-data", "dummyData");
$compile(regionElement)(scope);
})
}
}
});
app.directive('region', function ($compile) {
return {
restrict: 'A',
scope: {
dummyData: "="
},
link: function (scope, element, attrs) {
scope.elementId = element.attr("id");
scope.regionClick = function () {
alert(scope.dummyData[scope.elementId].value);
};
element.attr("ng-click", "regionClick()");
element.attr("ng-attr-fill", "{{dummyData[elementId].value | map_color}}");
element.removeAttr("region");
$compile(element)(scope);
}
}
});
DashboardController.js
This is where my problem is. Here, I'm returning data from my database via an $http.get. I currently have my $scope.createDummyData function outside of this http.get & it works fine. If I place that code inside my $http.get however, data doesn't populate. And that's my problem:
var DashboardController = function ($scope, $http) {
$http.get('/Account/GetDashboardDetails')
.success(function (result) {
//need to place my $scope.createDummyData inside here
})
.error(function (data) {
console.log(data);
});
var states = ["AL", "AK", "AS", "AZ", "AR", "CA", "CO", "CT", "DE", "DC", "FM", "FL", "GA", "GU", "HI", "ID", "IL",
"IN", "IA", "KS", "KY", "LA", "ME", "MH", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM",
"NY", "NC", "ND", "MP", "OH", "OK", "OR", "PW", "PA", "PR", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VI", "VA",
"WA", "WV", "WI", "WY"];
$scope.createDummyData = function () {
var dataTemp = {};
angular.forEach(states, function (state, key) {
dataTemp[state] = { value: Math.random() }
});
$scope.dummyData = dataTemp;
};
$scope.createDummyData();
};
HTML
Finally, here's my html. I don't feel anything here pertains to my issue, but I included it anyway, just in case:
<div class="container">
<div svg-map class="col-md-8 col-sm-12 col-xs-12" style="height:350px;"></div>
<p>
<button ng-click="createDummyData()" class="btn btn-block btn-default">Create Dummy Data</button>
</p>
<div class="regionlist">
<div ng-repeat="(key,region) in dummyData">
<div>{{key}}</div>
<div>{{region.value | number}}</div>
</div>
</div>
</div>
How can I go about populating my map, via database data?
Thanks

The link function of the directive executes before the $http.get XHR receives a response. One approach is to use $watch to wait for the data:
app.directive('region', function ($compile) {
return {
restrict: 'A',
scope: {
dummyData: "="
},
link: function (scope, element, attrs) {
scope.elementId = element.attr("id");
scope.regionClick = function () {
alert(scope.dummyData[scope.elementId].value);
};
element.attr("ng-click", "regionClick()");
element.attr("ng-attr-fill", "{{dummyData[elementId].value | map_color}}");
element.removeAttr("region");
//Use $watch to wait for data
scope.$watch("::dummyData", function(value) {
if (value) {
$compile(element)(scope);
};
});
}
}
});
In the above example, the watch expression uses a one-time binding so that the $compile statement executes only once. If one wants the element to be re-compiled on every change of the data, it gets more complicated.
For more information on $watch, see AngularJS $rootscope.Scope API Reference - $watch

Not sure if this is what you're looking for exactly but might help.
I would create a service to grab the data (I'm assuming the HTTP to /Account/GetDashboardDetails returns a list of states) then call the service in your controller. Once the call is finished, you then call your create dummyDataFunction.
// Service
angular
.module('dashboardApp')
.service('$dataService', dataService);
dataService.$inject = ['$http']
function dataService() {
var service = {
getData : getData
}
return service;
//////////
function getData() {
return $http.get('/Account/GetDashboardDetails')
.then(function (result) {
return result;
},function(error) {
return error;
})
}
}
// Controller
var DashboardController = function ($scope,$dataService) {
$scope.dummyData = [];
activate();
/////////
function activate() {
$dataService.getData()
.then(function(states){
var dataTemp = {};
angular.forEach(states, function (state, key) {
dataTemp[state] = { value: Math.random() }
});
$scope.dummyData = dataTemp;
},function(error) {
console.log(error);
});
}
};

It doesn't matter if the link function of your svgMap directive is called way before you ever make an HTTP get request.
What you have is that the svgMap directive is dynamically creating region directives which are bound to dummyData from the outer controller scope.
Being bound to dummyData means Angular has already registered watchers for the bound property. Literally, this also means that this binding exists even before the properties are actually set on the scope elsewhere, i.e. the parent controller.
I suspect that the $scope.dummyData is not being assigned the returned data from the server properly.
From the code in your question, you are calling the success handler on the get request and getting the response back in the result variable. However, you've not mentioned how you are getting the data from the response. So, I reckon you might have mixed up the response from .then() callback with the success() callback:
In case of the .success() callback:
$http.get('/Account/GetDashboardDetails')
.success(function (result) {
$scope.dummyData = result // data exists in the `result` itself
})
In case of the .then() success callback:
$http.get('/Account/GetDashboardDetails')
.then(function (response) {
$scope.dummyData = response.data; // data is wrapped in `data`
})
If the above is not the case and you're sure enough that the returned data from the server is assigned to the scope properly, then try wrapping that portion of logic inside $timeout(). This triggers $scope.$apply() and ensures that changes in the $scope.dummyData are reflected properly in the view.
var DashboardController = function ($scope, $http, $timeout) {
$http.get('/Account/GetDashboardDetails')
.success(function (result) {
$timeout(function(){
/* Do something here */
$scope.dummyData = result;
});
})

Related

How do I store a variable in my controller whose value I get after DOM has loaded?

angular.element(document).ready(function () {
vm.myLocation= document.getElementById('myLocation').innerHTML;
});
How do I store this so that I can use this later?
edit ---
I found a guide here (http://jasonwatmore.com/post/2014/02/15/AngularJS-Reverse-Geocoding-Directive.aspx) that lets me find the location of a user based on lat/long.
I am trying to get the location name value (ie. Seattle, WA, USA) that is loaded so that I can use that when creating a new user post and push it to the DB when user submits the post. Not sure what is the best way of doing this.
If we modify the provided directive slightly, we can actually make it two-way bind back to your controller so you can then use the resulting value.
Let's look at the modified directive first, and then I'll go over the new pieces one by one:
return {
restrict: 'E',
template: '<div>{{location}}</div>',
scope: {
location: '=',
onLocationFound: '&'
},
link: function(scope, element, attrs) {
var geocoder = new google.maps.Geocoder();
var latlng = new google.maps.LatLng(attrs.lat, attrs.lng);
function setLocation(location) {
console.log("setting location", location);
scope.$apply(function() {
scope.location = location;
scope.onLocationFound({
location: location
});
});
}
geocoder.geocode({
'latLng': latlng
}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results[1]) {
setLocation(results[1].formatted_address);
} else {
setLocation('Location not found');
}
} else {
setLocation('Geocoder failed due to: ' + status);
}
});
},
replace: true
}
Using isolate scope, we can provide a sort of API to the consumers of our directive and use that to bind back to our controller:
scope: {
location: '=',
onLocationFound: '&'
}
This little addition allows us to provide two new attributes location and on-location-found that can be used in concert to do pretty much exactly what you want.
The = means that that value will be two-way bound, and the & is an expression that will be evaluated when we want it, giving us a way to signal the consumer of our directive.
Inside our directive instead of writing directly to the DOM, we change our template to bind to the location property instead.
template: '<div>{{location}}</div>'
And then whenever the API call is finished, we can update the scope value, as well as execute our expression function like so:
scope.$apply(function() {
scope.location = location;
scope.onLocationFound({
location: location
});
});
The scope.$apply is needed to kick off a digest loop and ensure the DOM get's updated.
The call to onLocationFound will execute our expression, and pass in whatever parameters we provide as key/value pairs in the form of an object.
The result is that we can then use it in markup like this:
There is a a lot you can do with directives and I encourage you to read up on the documentation to really get a better understanding of what is happening.
I've taken the liberty of putting this all together in a working snippet below.
(function() {
function reverseGeocodeDirective() {
return {
restrict: 'E',
template: '<div>{{location}}</div>',
scope: {
location: '=',
onLocationFound: '&'
},
link: function(scope, element, attrs) {
var geocoder = new google.maps.Geocoder();
var latlng = new google.maps.LatLng(attrs.lat, attrs.lng);
function setLocation(location) {
console.log("setting location", location);
scope.$apply(function() {
scope.location = location;
scope.onLocationFound({
location: location
});
});
}
geocoder.geocode({
'latLng': latlng
}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results[1]) {
setLocation(results[1].formatted_address);
} else {
setLocation('Location not found');
}
} else {
setLocation('Geocoder failed due to: ' + status);
}
});
},
replace: true
}
}
function MyCtrl() {
var _this = this;
_this.location = "Nowhere";
_this.locationFound = function(location) {
_this.message = "Location was found";
};
}
angular.module('my-app', [])
.directive('reverseGeocode', reverseGeocodeDirective)
.controller('myCtrl', MyCtrl);
}());
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="http://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
<div ng-app="my-app" ng-controller="myCtrl as ctrl">
<reverse-geocode lat="40.730885" lng="-73.997383"
location="ctrl.location"
on-location-found="ctrl.locationFound(location)">
</reverse-geocode>
<h3><code>ctrl.location = </code>{{ctrl.location}}</h3>
<h3><code>ctrl.message = </code>{{ctrl.message}}</h3>
</div>
Can you try broadcasting the changes from your directive and add a watch in your controller for this change ?
Something similar to https://stackoverflow.com/a/25505545/3131696.
More info on broadcasting, http://www.dotnet-tricks.com/Tutorial/angularjs/HM0L291214-Understanding-$emit,-$broadcast-and-$on-in-AngularJS.html

How do you get an Angular directive to auto-refresh when loading data asynchronously?

I've been creating a number of small directives and using hard-coded arrays for testing while I build out functionality. Now that I've got some of that done, I went back and created a service to load the data from a website via JSON; it returns a promise and when it's successful I update the property my template is based off of. Of course, as soon as I did that my directive stopped rendering correctly.
What is the preferred way of binding my directive to asynchronously loaded data so that when the data finally comes back my directive renders?
I'm using Angular 1.4.7.
Here's a simple example of my hard-coded version.
angular
.module('app', []);
angular.module('app').controller('test', function(){
var vm = this;
vm.inv = 'B';
vm.displayInv = function () {
alert('inv:' + vm.inv);
};
});
angular.module('app')
.directive('inventorytest', function () {
return {
restrict: 'E',
template: '<select ng-model="ctrl.selectedOption" ng-options="inv.code as inv.desc for inv in ctrl.invTypes"></select>{{ctrl.sample}}. Selected: {{ctrl.selectedOption}}',
scope: { selectedOption: '='},
bindToController: true,
controller: function () {
this.invTypes = [
{ code: 'A', desc: 'Alpha' },
{ code: 'B', desc: 'Bravo' },
{ code: 'C', desc: 'Charlie' },
];
this.sample = 'Hello';
},
controllerAs: 'ctrl'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="app" ng-controller="test as vm">
<inventorytest selected-option='vm.inv'></inventorytest>
<br/>
Controller: {{vm.inv}}
</div>
My service is essentially just a thin wrapper around an $http call, ex:
return $http({ method: 'GET', url: 'https://myurl.com/getinfo' });
And I had tried modifying my code to do something like:
this.invTypes = [ { code: '', desc: '' }];
ctrService.getInfo()
.then(function successCallback(response) {
this.invTypes = response.data;
}, function errorCallback(response) {
// display error
});
Like I said, that doesn't work since it seems Angular isn't watching this property.
Within the callback this has a different context and isn't what you want it to be.
You need to save a reference to the controller this and use that within any callbacks
// store reference to `this`
var vm = this;
vm.invTypes = [ { code: '', desc: '' }];
ctrService.getInfo()
.then(function successCallback(response) {
// must use reference to maintain context
vm.invTypes = response.data;
}, function errorCallback(response) {
// display error
});

triggerHandler causing Error: [$rootScope:inprog] $apply already in progress error - AngularJS

I am trying to trigger the click of a button when a key is pressed. I'm doing this using the triggerHandler function, but this is causing the error above. I'm thinking I must have created some kind of circular reference/loop, but I can't see where.
This is my HTML:
<button id="demoBtn1" hot-key-button hot-key="hotKeys.primaryTest" class="po-btn primary-btn" type="submit" ng-click="btnFunction()"></button>
Here's my controller:
.controller('BtnCtrl', function ($scope) {
$scope.checkKeyPress = function ($event, hotKeyObj) {
for(var key in hotKeyObj) {
if($event.keyCode == hotKeyObj[key].keyCode) {
var id = hotKeyObj[key].btnId;
hotKeyObj[key].funcTriggered(id);
}
}
}
$scope.clickFunction = function(id) {
var currentButton = angular.element(document.getElementById(id));
currentButton.triggerHandler("click");
}
$scope.btnFunction = function() {
console.log("clickFunction1 has been triggered");
}
$scope.hotKeys = {
'primaryTest': {
keyCode: 49,
keyShortcut: "1",
label: "Button",
btnId: 'demoBtn1',
funcTriggered: $scope.clickFunction
},
// more objects here
}
}
})
And my directive is here:
.directive("hotKeyButton", function() {
return {
controller: 'BtnCtrl',
scope: {
hotKey: '='
},
transclude: true,
template: "<div class='key-shortcut'>{{hotKey.keyShortcut}}</div><div class='hotkey-label'>{{hotKey.label}}</div>"
};
})
It's a bit of a work in progress, so I suspect there might be small errors in places, but I'm primarily interested in the logic running from the keypress to btnFunction being triggered. The error fires on the currentButton.triggerHandler("click") line.
Can anyone see what I've done? Thanks.
Since you have a problem with $apply in progress - you can just wrap your triggerHandler call into $timeout wrapper - just to make everything you need in another $digest-cycle, like this:
$scope.clickFunction = function(id) {
var currentButton = angular.element(document.getElementById(id));
$timeout(function () {
currentButton.triggerHandler("click");
});
}
After this everything will work OK.
Also don't forget to $inject $timeout service into your BtnCtrl.
Also i'm not sure you need to define controller property for your directive, but it's not a main case.
Demo: http://plnkr.co/edit/vO8MKAQ4397uqqcAFN1D?p=preview

Load Angular Directive Template Async

I want to be able to load the directive's template from a promise. e.g.
template: templateRepo.get('myTemplate')
templateRepo.get returns a promise, that when resolved has the content of the template in a string.
Any ideas?
You could load your html inside your directive apply it to your element and compile.
.directive('myDirective', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
//Some arbitrary promise.
fetchHtml()
.then(function(result){
element.html(result);
$compile(element.contents())(scope);
}, function(error){
});
}
}
});
This is really interesting question with several answers of different complexity. As others have already suggested, you can put loading image inside directive and when template is loaded it'll be replaced.
Seeing as you want more generic loading indicator solution that should be suitable for other things, I propose to:
Create generic service to control indicator with.
Manually load template inside link function, show indicator on request send and hide on response.
Here's very simplified example you can start with:
<button ng-click="more()">more</button>
<div test="item" ng-repeat="item in items"></div>
.throbber {
position: absolute;
top: calc(50% - 16px);
left: calc(50% - 16px);
}
angular
.module("app", [])
.run(function ($rootScope) {
$rootScope.items = ["One", "Two"];
$rootScope.more = function () {
$rootScope.items.push(Math.random());
};
})
.factory("throbber", function () {
var visible = false;
var throbber = document.createElement("img");
throbber.src = "http://upload.wikimedia.org/wikipedia/en/2/29/Throbber-Loadinfo-292929-ffffff.gif";
throbber.classList.add("throbber");
function show () {
document.body.appendChild(throbber);
}
function hide () {
document.body.removeChild(throbber);
}
return {
show: show,
hide: hide
};
})
.directive("test", function ($templateCache, $timeout, $compile, $q, throbber) {
var template = "<div>{{text}}</div>";
var templateUrl = "templateUrl";
return {
link: function (scope, el, attr) {
var tmpl = $templateCache.get(templateUrl);
if (!tmpl) {
throbber.show();
tmpl = $timeout(function () {
return template;
}, 1000);
}
$q.when(tmpl).then(function (value) {
$templateCache.put(templateUrl, value);
el.html(value);
$compile(el.contents())(scope);
throbber.hide();
});
},
scope: {
text: "=test"
}
};
});
JSBin example.
In live code you'll have to replace $timeout with $http.get(templateUrl), I've used the former to illustrate async loading.
How template loading works in my example:
Check if there's our template in $templateCache.
If no, fetch it from URL and show indicator.
Manually put template inside element and [$compile][2] it.
Hide indicator.
If you wonder what $templateCache is, read the docs. AngularJS uses it with templateUrl by default, so I did the same.
Template loading can probably be moved to decorator, but I lack relevant experience here. This would separate concerns even further, since directives don't need to know about indicator, and get rid of boilerplate code.
I've also added ng-repeat and run stuff to demonstrate that template doesn't trigger indicator if it was already loaded.
What I would do is to add an ng-include in my directive to selectively load what I need
Check this demo from angular page. It may help:
http://docs.angularjs.org/api/ng.directive:ngInclude
````
/**
* async load template
* eg :
* <div class="ui-header">
* {{data.name}}
* <ng-transclude></ng-transclude>
* </div>
*/
Spa.Service.factory("RequireTpl", [
'$q',
'$templateCache',
'DataRequest',
'TplConfig',
function(
$q,
$templateCache,
DataRequest,
TplConfig
) {
function getTemplate(tplName) {
var name = TplConfig[tplName];
var tpl = "";
if(!name) {
return $q.reject(tpl);
} else {
tpl = $templateCache.get(name) || "";
}
if(!!tpl) {
return $q.resolve(tpl);
}
//加载还未获得的模板
return new $q(function(resolve, reject) {
DataRequest.get({
url : "/template/",
action : "components",
responseType : "text",
components : name
}).success(function(tpl) {
$templateCache.put(name, tpl);
resolve(tpl);
}).error(function() {
reject(null);
});
});
}
return getTemplate;
}]);
/**
* usage:
* <component template="table" data="info">
* <span>{{info.name}}{{name}}</span>
* </component>
*/
Spa.Directive.directive("component", [
"$compile",
"RequireTpl",
function(
$compile,
RequireTpl
) {
var directive = {
restrict : 'E',
scope : {
data : '='
},
transclude : true,
link: function ($scope, element, attrs, $controller, $transclude) {
var linkFn = $compile(element.contents());
element.empty();
var tpl = attrs.template || "";
RequireTpl(tpl)
.then(function(rs) {
var tplElem = angular.element(rs);
element.replaceWith(tplElem);
$transclude(function(clone, transcludedScope) {
if(clone.length) {
tplElem.find("ng-transclude").replaceWith(clone);
linkFn($scope);
} else {
transcludedScope.$destroy()
}
$compile(tplElem.contents())($scope);
}, null, "");
})
.catch(function() {
element.remove();
console.log("%c component tpl isn't exist : " + tpl, "color:red")
});
}
};
return directive;
}]);
````

AngularJS Passing Variable to Directive

I'm new to angularjs and am writing my first directive. I've got half the way there but am struggling figuring out how to pass some variables to a directive.
My directive:
app.directive('chart', function () {
return{
restrict: 'E',
link: function (scope, elem, attrs) {
var chart = null;
var opts = {};
alert(scope[attrs.chartoptions]);
var data = scope[attrs.ngModel];
scope.$watch(attrs.ngModel, function (v) {
if (!chart) {
chart = $.plot(elem, v, opts);
elem.show();
} else {
chart.setData(v);
chart.setupGrid();
chart.draw();
}
});
}
};
});
My controller:
function AdListCtrl($scope, $http, $rootScope, $compile, $routeParams, AlertboxAPI) {
//grabing ad stats
$http.get("/ads/stats/").success(function (data) {
$scope.exports = data.ads;
if ($scope.exports > 0) {
$scope.show_export = true;
} else {
$scope.show_export = false;
}
//loop over the data
var chart_data = []
var chart_data_ticks = []
for (var i = 0; i < data.recent_ads.length; i++) {
chart_data.push([0, data.recent_ads[i].ads]);
chart_data_ticks.push(data.recent_ads[i].start);
}
//setup the chart
$scope.data = [{data: chart_data,lines: {show: true, fill: true}}];
$scope.chart_options = {xaxis: {ticks: [chart_data_ticks]}};
});
}
My Html:
<div class='row-fluid' ng-controller="AdListCtrl">
<div class='span12' style='height:400px;'>
<chart ng-model='data' style='width:400px;height:300px;display:none;' chartoptions="chart_options"></chart>
{[{ chart_options }]}
</div>
</div>
I can access the $scope.data in the directive, but I can't seem to access the $scope.chart_options data.. It's definelty being set as If I echo it, it displays on the page..
Any ideas what I'm doing wrong?
UPDATE:
For some reason, with this directive, if I move the alert(scope[attrs.chartoptions]); to inside the $watch, it first alerts as "undefined", then again as the proper value, otherwise it's always undefined. Could it be related to the jquery flot library I'm using to draw the chart?
Cheers,
Ben
One problem I see is here:
scope.$watch(attrs.ngModel, function (v) {
The docs on this method are unfortunately not that clear, but the first argument to $watch, the watchExpression, needs to be an angular expression string or a function. So in your case, I believe that you need to change it to:
scope.$watch("attrs.ngModel", function (v) {
If that doesn't work, just post a jsfiddle or jsbin.com with your example.

Resources