I have a click event that happens outside the scope of my custom directive, so instead of using the "ng-click" attribute, I am using a jQuery.click() listener and calling a function inside my scope like so:
$('html').click(function(e) {
scope.close();
);
close() is a simple function that looks like this:
scope.close = function() {
scope.isOpen = false;
}
In my view, I have an element with "ng-show" bound to isOpen like this:
<div ng-show="isOpen">My Div</div>
When debugging, I am finding that close() is being called, isOpen is being updated to false, but the AngularJS view is not updating. Is there a way I can manually tell Angular to update the view? Or is there a more "Angular" approach to solving this problem that I am not seeing?
The solution was to call...
$scope.$apply();
...in my jQuery event callback.
Why $apply should be called?
TL;DR:
$apply should be called whenever you want to apply changes made outside of Angular world.
Just to update #Dustin's answer, here is an explanation of what $apply exactly does and why it works.
$apply() is used to execute an expression in AngularJS from outside of
the AngularJS framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the AngularJS framework we need to perform proper scope life cycle of
exception handling, executing watches.
Angular allows any value to be used as a binding target. Then at the end of any JavaScript code turn, it checks to see if the value has changed.
That step that checks to see if any binding values have changed actually has a method, $scope.$digest()1. We almost never call it directly, as we use $scope.$apply() instead (which will call $scope.$digest).
Angular only monitors variables used in expressions and anything inside of a $watch living inside the scope. So if you are changing the model outside of the Angular context, you will need to call $scope.$apply() for those changes to be propagated, otherwise Angular will not know that they have been changed thus the binding will not be updated2.
Use
$route.reload();
remember to inject $route to your controller.
While the following did work for me:
$scope.$apply();
it required a lot more setup and the use of both .$on and .$broadcast to work or potentially $.watch.
However, the following required much less code and worked like a charm.
$timeout(function() {});
Adding a timeout right after the update to the scope variable allowed AngularJS to realize there was an update and apply it by itself.
Related
I'm starting with AngularJS and I have a question related to the way a method is invoked when setting a new controller.
Let's say I have a route configured like this:
$routeProvider.when('/myApp/:id', {controller: 'MyAppCtrl'});
What's the difference between these 2 controller codes, regarding the execution context and the $scope life cycle?
How many times each alternative runs after the partial is loaded?
.
app.controller('MyAppCtrl',function($scope,$routeParams){
$scope.$on('$routeChangeSuccess', function(){
$scope.data = getNewData($routeParams.id);
});
function getNewData(id){
...
}
});
And:
app.controller('MyAppCtrl',function($scope,$routeParams){
$scope.data = getNewData($routeParams.id);
function getNewData(id){
...
}
});
Thank you very much.
In my opinion, I would use resolve in route config instead of your 2 options
back to your question.
I believe controller only execute once after the partial is loaded.
and these 2 cases are pretty much doing the same thing. The first one relies on event, which is an extra step comparing with the 2nd one.
$on assigns a listener to an event. Meaning You could trigger $on manually by sending $broadcast('routeChangeSucess').
The second code is run one time, once The partial is loaded.
So using $on for a Controller load dosn't do You any good
I am using AngularJS and a phone web service to make calls through WebSockets.
The web service has several callbacks such as Phone.onIncomingCall
When I use this function to set a $scope variable the view is not updated automatically except if I use $scope.$apply right after.
Phone.onIncomingCall = function(){
$scope.myVar = "newValue";
$scope.$apply(); // only works if I call this line
};
What is the reason for this behaviour (is it expected) and is there a way around using $scope.apply() in each function?
Angular is "unaware" of the update to the scope variable you've made, since you're updating it from outside of the Angular context. From the docs for $apply:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
By running $apply on the scope, $digest is called from $rootScope, which will trigger any $watchers registered on $scope.myVar. In this case, if you're using the variable in your view via interpolation, this is where the $watcher was registered from.
It is the expected behavior, angular works like that internally.
I recommend the following:
Phone.onIncomingCall = function () {
$scope.$apply(function () {
$scope.myVar = 'newValue';
});
}
I am currently using the Durandal framework to build a simple site (my first with Durandal) and have a question on how to go about adding a simple javascript function. I want to call the function after the DOM loads, but am not sure how to attach it to the current viewmodel. The problem I am having is the function is being called before the DOM loads and the div ID hasn't been created yet, which in this case is "sb-search".
I then tried adding the function to the viewmodel:
define(["plugins/router"], function (router) {
var vm = {
viewAttached: viewAttached
};
return {
router: router
};
function attached() {
new UISearch(document.getElementById('sb-search'));
}
return vm;
});
but, to no avail.
Any help would be greatly appreciated. Even a simple tutorial on how to "document.write('Hello World')" would be helpful. Thanks!
This is not strictly related, but I'd like to add something to what was said in the comments: you most likely shouldn't scan the global document in the attached handler. When it's called, composed views may not be, well, composed/attached themselves yet, and in general it's a good idea not to make assumptions about the global state. Also, you can gain performance by not scanning the whole DOM.
When calling attached, Durandal passes the root DOM element of the view bound to the view model as the first argument to the function. Use it to restrict search. If it's in a child/composed view, use the compositionComplete handler, called after all composition in complete (the event "bubbles up"). If it's in a parent view, use the second argument passed to these functions. If it really sounds too complicated, consider that your design might be flawed itself, look for MVVM guidance.
For completeness:
The comments mention that
You must export the right function (attached != viewAttached),
If you indeed intended to define an attached handler called by Durandal, know that viewAttached is deprecated in favor of attached.
And I'd also add that you return an anonymous object containing a router property before you return your vm (view model for sure), although that might be a left-over from some tests you did and copy-pasted here by mistake.
It is possible for a directive to update a service and then use the updated version?
In my service (cfg), I have a variable and an update function...
var test = "unfired";
function updateTest(){
console.log("LOG:","updateTest is firing");
test = "fired";
}
In the linking function of my directive I have
scope.$watch(watcher, function(newVal, oldVal) {
console.log("Before:",cfg.test);
cfg.updateTest();
console.log("After:",cfg.test);
}); //scope.$watch
Even though the updateTest function is firing, the console logs the same value before and after.
Now if cfg were a controller instead of a service I would do something like
function updateTest(){
console.log("LOG:","updateTest is firing");
test = "fired";
cfg.$apply() //or cfg.$digest()
}
But obviously that won't work. I have also tried injecting cfg to the controller and and $apply() to the link function...
console.log("Before:",cfg.test);
scope.$apply(function(){
cfg.updateTest()
});
console.log("After:",cfg.test);
which did trigger updateTest(), but it did not update the cfg service as the directive understands it.
Perhaps another way to say it is that I would like to "reinject" the service into the directive.
If you are wondering why I'd like to do this, it's because I have a bunch of d3.js animations as directives that share the same scales, and I'd like certain events to trigger changes in the scales' domains from one directive to the others.
Rather than using a service to communicate between directives. Try using "broadcast". You can throw an event into the air and anybody listening will run whatever function you want. It works like this.
Directive 1:
$rootScope.$broadcast('event:updateTest');
Directive 2:
$rootScope.$on("event:updateTest", function (event, next, current) { ... }
Then you can deal with local instances of your 'test' variable, rather than a service 'global' variable.
I'm trying to build a directive to output some HTML formatted code for a paging control (Twitter Bootstrap styled), this directive needs to take the current page and total pages from the scope of my controller and when a paging link is clicked trigger a function on my controller to change the page (builds a url and calls $location to change page).
I've watched many of the excellent YouTube angularjs videos (http://www.youtube.com/watch?v=nKJDHnXaKTY) but none seem to cover this particular complex scenario.
Any help would be great!
Here is jsfiddle that makes it clearer:
http://jsfiddle.net/philjones88/dVFDT/
What I can't get working is passing the parameter, I get:
changing page to: undefined
In your directive add the changePage call there (I know it's not where you want it). Have it call the parents scope changePage with the same parameter.
$scope.changePage = function(index){
console.log("running changePage");
$scope.$parent.changePage(index); //might want to check if the parent scope has this too
}
As another tip, in directives you shouldn't use the $ in front of the variables being sent in. In this case that would be $scope, $element, $attrs. The $ you see in front of scope in controllers (not linking functions) is there to let you know that it is being injected. It is not being injected in the linking controller. For instance, here:
app.directive("pager", function ($injector1, $injector2) {
This is where injected parameters would go, and you want to be able to distinguish the two of them. I realize this got a little off track and I hope the suggestion I have for the changePage is what you're looking for.
Edit: Updated fiddle http://jsfiddle.net/dVFDT/48/
Modified answer for future searchers: The function you were passing in via the click method like so:
..... click="changePage()".....
Needed to be changed to:
..... click="changePage".....
This means you're passing the function through and not the function call. This meant that in your directive when you wired up the changePage callback you were calling the function with the index like this:
changePage()(1)
and that's why you were getting undefined.
I dont understand completely, but at the end of your directive you want to execute a function of your controller?
Try:
<div class="pagination">
<pager current-page="currentPage" total-pages="totalPages" query="query" callback="changePage()"></pager>
</div>
I realize this question is a bit old, but there's actually another way to solve this that doesn't require recompiling or calling the parent scope. It does, however, require calling the method from within the directive in a slightly different way.
You can see the fiddle here: Fiddle
The line that's of most interest is in the template declaration. The call to onClick requires you pass it an object rather than just the value.
template:
"<div ng:repeat='i in [] | range:totalPages'> " +
"<a ng:click='onClick({page: i + 1})'>Page {{i + 1}}</a>" +
"</div>",
This also makes use of a filter from this answer from Gloopy in order to iterate n number of times in an ng:repeat. This allows the binding to all happen in the template.