I have a quite hard time to build a (maybe non-trivial) directive for an SPA. Basically I need to be able to pour data into the directive from any controller and the directive is supposed to show a live graph.
I've read this post and I would like to use a shared object on the isolated scope of this directive anyway.
So I tried to do smth like this:
Wrapping template:
<div ng-controller="WrappingCtrl">
<timeline-chart d3API="d3API"><timeline-chart>
</div>
In the 'wrapping' controller:
$scope.d3API = {};
$scope.d3API.options = {}; //for d3Config
$scope.d3API.currentValue = 3; //asynchronous!!!
Finally to use the shared object d3API in the directive's link method I tried e.g. this:
//in the directive:
scope: { //nice, but does it help??
d3API: '='
}
and:
var data = [1, 2];
var updateTimeAxis = function() {
var newValue;
if (data.length) {
newValue = (data[data.length - 1] !== scope.d3API.currentValue) ? scope.d3API.currentValue : data[data.length - 1];
data.push(newValue);
} else {
console.warn('problem in updateTimeAxis: no data length');
}
};
To gain some simplicity for this question I've created a fiddle, note, that none of both are working:
http://jsfiddle.net/MalteFab/rp55vjc8/3/
http://jsfiddle.net/MalteFab/rp55vjc8/5/
The value in the directive's template is not updated - what am I doing wrong? Any help is appreciated.
Your fiddle mostly works, you just need to update your controller to use $timeout:
app.controller('anyCtrl', function($scope, $timeout) {
show('anyCtrl');
$scope.bound = {};
$timeout(function() {
$scope.bound.says = 'hello';
}, 200);
});
Forked fiddle: http://jsfiddle.net/wvt1f1zt/
Otherwise no digest occurs so angular doesn't know something changed. Based on what you're actual problem is, I'm assuming you're not using timeout vs $timeout, but if your coding style is to intermix angular with "normal" javascript, you may be running into the same kind of scenario.
A good article for reference for telling angular about what your doing is here: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
Related
Here is the code snippet about an Angular Controller. I'm trying to learn Angular from this github project
The questionable part is located in function addStock. I have already defined $scope.watchlist in the initilizations part, however, if I remove the re-declaration inside $scope.addStock function, $scope.watchlist will not be populated with right values. Can any Angular experts point me out?
If you want to see my full project code - here is the link.
angular.module('stockCatApp')
.controller('WatchlistCtrl', function ($scope, $routeParams, $modal, WatchlistService, CompanyService) {
// Initializations
$scope.companies = CompanyService.query();
$scope.watchlist = WatchlistService.query($routeParams.listId);
$scope.stocks = $scope.watchlist.stocks;
$scope.newStock = {};
var addStockModal = $modal({
scope: $scope,
template: 'views/templates/addstock-modal.html',
show: false
});
$scope.showStockModal = function () {
addStockModal.$promise.then(addStockModal.show);
};
$scope.addStock = function () {
//The following line needs to be put here again in order to retrieve the right value
$scope.watchlist = WatchlistService.query($routeParams.listId);
///////////////////////////////////////////////////////////////////////
$scope.watchlist.addStock({
listId: $routeParams.listId,
company: $scope.newStock.company,
shares: $scope.newStock.shares
});
addStockModal.hide();
$scope.newStock = {};
};
});
Can you do a console.log($scope.watchlist); after both of your $scope binds to see if the data and its type match?
$scope.watchlist = WatchlistService.query($routeParams.listId);
console.log($scope.watchlist);
$scope.watchlist.addStock({
listId: $routeParams.listId,
company: $scope.newStock.company,
shares: $scope.newStock.shares
});
console.log($scope.watchlist);
Feel free to post the logs.
It appears as if you are calling $scope.addStock() from a modal. In which case the modal does not necessarily inherit the parent scope. It depends on which versions of everything you are using. It looks like you are using angular-strap (I looked in your bower.json). If you look in their $modal source code you will find this:
https://github.com/mgcrea/angular-strap/blob/master/src/modal/modal.js
var scope = $modal.$scope = options.scope && options.scope.$new() || $rootScope.$new();
So, when you are opening your modal you are creating a new scope. You might be able to access the original scope using $parent though.
I'm dealing with an app that manages users login. Like in many apps, i want to change the header when the user logs in.
I've a main file (index.html) which uses ng-include to include the header.html
I found two solutions (i'm new to angular, so both may be wrong):
1) use a $rootScope.broadcast()
So when the user logs in I broadcast (the auth.js, it's inside a factory) a message that is intercepted by the controller in the header.
the auth.js
$rootScope.$broadcast('logged',user);
the controller.js
$scope.$on('logged', function(evnt, message){
$scope.user = message;
});
the header.html
<div class="header" ng-controller="GcUserCtrl as gcUserCtrl">
...
<li><a ng-show="user" href="#">User: {{user.name}}</a></li>
2) set a $rootScope variable
As far as I understood $rootScope is the root of all the scope (the naming is quite smart) and all the $scope have access to it.
the auth.js
$rootScope.user=user;
the heaeder.html (no controller is needed here)
<div class="header">
...
<li><a ng-show="user" href="#">User: {{user.name}}</a></li>
Now, what's the correct way to handle it?
the first seems a bit more expensive since the broadcast may have to do many checks.
the second .. well, I'm not a fan of global variables..
EDIT
3) use service
after the comment of alex I add this options, even if I'm not able to make it working. (here the plunkr)
it does not work without events
index.html
...
<ng-include src="'header.html'"></ng-include>
...
header.html
as for the number 1)
controller.js
.controller('GcUserCtrl', ['$scope','my.auth','$log', function ($scope, auth, $log) {
$scope.user = auth.currentUser();
}]);
my.auth.js
.factory('my.auth', ['$rootScope', '$log', function ($rootScope, $log, localStorageService) {
var currentUser = undefined;
return {
login: function (user) {
currentUser = user;
...
},
...
currentUser: function () {
return currentUser;
}
};
}]);
The problem here is that the controller is called only the first time and nothing happens after the login.
As I stated earlier you will want to use a Service which will store the user's information. Attach user information to this service where ever you are authenticating the user. If you have questions about the best way to authenticate that would be a seperate question but you may want to look into using a Login Factory that does the actual authentication (and any authorization). You can then inject the login Service into that factory. I have created a Plunker here as a reference.
var app = angular.module('myApp', []);
app.service('SessionService', function () {
this.attachUser = function(userId, fName){
this.userId = userId;
this.fName = fName;
}
});
app.controller('MainCtrl', function($scope, SessionService){
// You will want to invoke attachUser some other way (perhaps on authentication), this is for test purposes only
SessionService.attachUser(1234, 'John');
$scope.userName = SessionService.fName;
});
The code above is an example of your Service. This will act as a Session handler and store important information about the user. The controller MainCtrl can then invoke properties in the SessionService using dependency injection. The part I mentioned at the beginning of this post, SessionService.attachUser(userId, fName) would most likely live in a login factory.
The reason this is the best choice is because it decouples your application. It puts the session (which is really what you are storing in global variables) in a place that is designated to store that data. It makes it maintainable. You do not need to find every occurrence of $rootScope, for instance.
EDIT:
New plunker uses rootScope broadcast/on to capture changes
Events are the preferred way to communicate that action needs to be taken by something else. That an action occurred that something else might be interested in action against. It also reduces scope pollution as you mentioned.
The comment about using a service in this case is only partially accurate. All of the login logic could, and should, be put into a single service specific to logging and logging out. That service would then broadcast the event when a login occurs.
module.service('LoginHelper', function($rootScope) {
this.loginUser = function(username, password) {
// on success
$rootScope.broadcast('loggedIn', logginUserData)
}
this.logout = function() {
// on success
$rootScope.broadcast('loggedOut')
}
})
The logged in data should be stored and accessible by the service.
Alternatively, $emit could be used on $rootScope. You would then only be able to watch for the 'loggedIn' event on the $rootScope by there would be marginally less overhead.
Avoid watches
An event would be the appropriate way to go for this kind of requirement, like how alex has pointed out. A plunk demonstrating an example: http://plnkr.co/edit/v6OXjOXZzF9McMvtn6hG?p=preview
But for this particular scenario, I don't think the "angular way" is the "way". Given the nature of how $broadcast and/or $emit works (i.e. the default way events work in angular) I would avoid them...(Read the docs to understand why). In short, these mechanisms are meant to trigger listeners (attached to some scope) up/down the scope heirarchy. You don't really need all that. (Ref code for $emit)
I'd normally rely on other event propagation mechanisms (considering this pattern of requirement).
app.controller('MainCtrl', function($scope, SessionService, $document){
// You will want to invoke attachUser some other way (perhaps on authentication), this is for test purposes only
$scope.isLoggedIn = false;
$document.bind('$loggedin', function(){
$scope.isLoggedIn = true;
$scope.user = SessionService.fName;
});
$scope.logout = function() {
SessionService.attachUser(null, null);
$scope.isLoggedIn = false;
};
});
app.controller('LoginCtrl', function($scope, SessionService, $document){
$scope.doLogin = function() {
console.log('doLogin');
SessionService.attachUser(1234, $scope.username);
var doc = $document[0];
var evt = new Event('$loggedin');
doc.dispatchEvent(evt);
};
});
Plunk
Of course, when you are done with that view, always cleanup. Handle the $destroy event on that controller's scope and unbind the event handler...
$scope.$on('$destroy', function() {
$document.unbind('$loggedin');
};
Refer MDN for more on how to trigger events using DOM.
Update: [24 Sep]
Here is a small directive setup which demonstrates the point:
app.directive('ngNest', function($parse, $compile, $timeout){
var end = false;
var level = 0;
var fnPostLink = function(scope, element, attrs) {
//console.log('start:', ++fnPostLink.count);
var lvl = attrs.level;
if(!lvl) {
throw 'Level not specified';
}
var create = document.createElement.bind(document);
var level = parseInt(lvl);
var count = 0;
var div = create('div');
div.setAttribute('ng-controller', 'DummyCtrl');
var cls = function() {
return 'margin ' + (count % 2 ? 'even' : 'odd');
//return 'margin even';
};
div.setAttribute('class', cls());
var node = div;
while(count++ < level - 1) {
var child = create('div');
child.setAttribute('ng-controller', 'DummyCtrl');
child.setAttribute('class', cls());
node.appendChild(child);
node = child;
}
node.setAttribute('ng-controller', 'FinalCtrl');
node.innerHTML = 'foo';
var $new = $compile(div)(scope);
var el = element;
el.append($new);
};
fnPostLink.count = 0;
var fnPreLink = function(scope, element, attrs) {
//console.log('prelink');
};
var api = {
link: {
post: fnPostLink,
pre: fnPreLink
},
template: '<div></div>',
scope: {},
restrict: 'E',
replace: true
};
return api;
});
It simply nests divs attaching a controllers to it. I am attaching these two controllers:
app.controller('DummyCtrl', function($scope){
});
app.controller('FinalCtrl', function($scope, $document){
$scope.$on('$myEvt', function(){
console.log('$myEvt', $scope.$id, new Date().getTime());
});
$document.bind('$myEvt', function(){
console.log('$myEvt', $scope.$id, new Date().getTime());
});
});
FinalCtrl is added to the tail; DummyCtrl is added to the rest.
In the html template I do something like:
<ng-nest level="10"></ng-nest>
There is also in the html file a nested markup which is manually put there...
Entire code may be found here: https://gist.github.com/deostroll/a9a2de04d3913f021f13
Here are the results I've obtained running from my browser:
Live reload enabled.
$broadcast 1443074421928
$myEvt 14 1443074421929
$myEvt 19 1443074421930
DOM 1443074426405
$myEvt 14 1443074426405
$myEvt 19 1443074426405
You can notice the difference in the ticks when I've done $broadcast. I have done a $broadcast on $rootScope; hence angular walks down the scope tree depth-first and triggers those listeners attached the respective scopes, and, in that order...The stuff in $emit & $broadcast source code also validates this fact.
I'm trying to create a custom compile function, to make it easier to dynamically add HTML to a page.
The argument htmlStr is the incoming HTML to compile. The argument value is a variable that can be added to the scope. The argument compiledHTMLFunc is a function that will be executed with the compiled object. Here's my code:
function compileHTML (htmlStr, value, compiledHTMLFunc)
{
var $injector = angular.injector (["ng", "angularApp"]);
$injector.invoke (function ($rootScope, $compile)
{
$rootScope.value = value;
var obj = angular.element (htmlStr);
var obj2 = $compile (obj)($rootScope);
if (compiledHTMLFunc != null)
compiledHTMLFunc (obj2);
});
}
Here's how I use the function:
compileHTML ("<button class = \"btn btn-primary\">{{ value }}</button>", "Ok", function (element)
{
$(document.body).append (element);
});
Whenever I try to compile the following HTML, the inline {{ value }} doesn't get compiled. Even if I simply change it to {{ 1+1 }}. Why is this?
Update: I dunno why I didn't create a fiddle earlier, here's an example: http://jsbin.com/vuxazuzu/1/edit
The problem appears to be pretty simple. Since you invoke compiler from outside of angular digest cycle you have to invoke it manually to boost the process, for example by wrapping compiledHTMLFunc into $timeout service call:
function compileHTML (htmlStr, scope, compiledHTMLFunc) {
var $injector = angular.injector(["ng", "angularApp"]);
$injector.invoke(function($rootScope, $compile, $timeout) {
$rootScope = angular.extend($rootScope, scope);
var obj = $compile(htmlStr)($rootScope);
if (compiledHTMLFunc != null) {
$timeout(function() {
compiledHTMLFunc(obj);
});
}
});
}
compileHTML('<button class="btn btn-primary">{{value}}</button>', {value: 'Ok'}, function(element) {
angular.element(document.body).append(element);
});
I also improved your code a little. Note how now compileHTML accepts an object instead of single value. It adds more flexibility, so now you can use multiple values in template.
Demo: http://plnkr.co/edit/IAPhQ9i9aVVBwV9MuAIE?p=preview
And here is your updated demo: http://jsbin.com/vuxazuzu/2/edit
I have this...
<script> var num = 22;</script>
Then inside of the controller block...
<span>{{somenumber}}</span>
In the controller...
$scope.somenumber = num;
This all works as expected.
How would I go about having it all update if the value of the num variable changes? So, I'd have some code (from socket.io or AJAX) change num to 65. Right now, it still says 22.
I'd take a look at this
num is a primitive type (Number). So When you're assigning it to the $scope you're copying it. What you need to do is reference it instead. I'd fix it the following way.
<script>var value = {num: 22} </script>
$scope.value = value;
<span> {{value.num}} </span>
If your ajax call is not through $http.(outside angular - wherever you set value.num) you'll need to invoke a digest cycle. The easiest way to do that is in an angular service like $timeout.
Think of the scope as
$scopeHAS model instead of $scopeAS model
You could use $watch followed by $apply:
Controller
$scope.somenumber = num;
$scope.$watch(function() {
return num;
}, function(newValue, oldValue) {
$scope.somenumber = newValue;
});
// fake external change to the 'num' variable
setTimeout(function() {
num = 33;
$scope.$apply();
}, 3000);
Here's a working example: http://plnkr.co/edit/rL20lyI1SgS6keFbckJp?p=preview
If your external change is happening outside the scope of a single controller, I would use $rootScope inside a run callback:
angular.module('exampleApp', []).run(function($rootScope) {
// same as above, just with $rootScope
});
I'm building custom tree directive:
<ul tree="treeOptions">
<li>{{ item.code + ' - ' + item.name }}</li>
</ul>
In javascript:
$scope.myItems = [];
$scope.treeOptions = {
data: 'myItems',
...
}
In directive:
(function (angular) {
'use strict';
angular.module('tree', []).
directive('tree', ['$compile', '$document', function ($compile,
$document) {
return {
restrict: 'A',
scope: { treeOptions: '=tree' }, //Isolated scope
compile: function (elem, attrs) {
//...
return function (scope, elem, attrs) {
//...
scope.$parent.$watchCollection(scope.treeOptions.data,
function (newItems, oldItems) {
var addedItems = _.difference(newItems, oldItems);
var removedItems = _.difference(oldItems, newItems);
//but newItems and oldItems always the same
//...
}
);
}
};
}
};
} ]);
})(angular);
I'm using lodash ( _ ) to find differences between new and old items.
The problem is newItems and oldItems are always the same, even after new items are pushed to parent scope's myItems array. What am I missing?
So, this is definitely an issue in the angular framework. I'm sure they will get around to fixing it sooner or later, but in the mean time if you need to get your code to work I was able to put together a sample that works quite well. The core is to not use the default old/new elements:
var oldWorkingItems = scope.$parent[attrs.testDirective].slice(0);
scope.$parent.$watchCollection(attrs.testDirective,
function (newItems, oldItems) {
console.log('NEW Items:' + newItems);
console.log('Old Items:' + oldWorkingItems);
For the full example as well as my reproduction of the error, see the following Plunkr: http://plnkr.co/edit/R9hQpRZqrAQoCPdQu3ea?p=preview. By the way, the reason this is called so many times is because it is inside an ng-repeat, but that was my way to force the use of "$parent". Anyways, hope this helps some!
Edit - It really annoyed me how many times the directive was being run in the ng-repeat so I wrote another plunker (http://plnkr.co/edit/R9hQpRZqrAQoCPdQu3ea?p=preview) that uses a single ng-repeat:
<div ng-repeat="element in [1]">
<div test-directive="testCollection"></div>
</div>
This only calls the directive twice (why twice, I'm still not sure).
As Gruff Bunny pointed out in the comments, this is an open issue with AngularJS up to the current version (1.2.13). The workaround for now is to use $watch( , true) or do as drew_w suggested.