In angularJs is possible to watch a global variable?
I set a window.test variable from legacy code, then I need to watch that variable to know if it exists.
I've tried something like
$window.$watch("test" , function(n,o){
//some code here...
}
Somewhat. You can if you include the Angular $window service (which is safer, as explained in the docs, than accessing window directly):
app.controller('myCtrl', function ($scope,$window) {...}
And then use a watch function as the first parameter to your $watch like so:
$scope.$watch(
function () {
return $window.test
}, function(n,o){
console.log("changed ",n);
}
);
demo fiddle
But note that the $watch won't execute until something triggers Angular to do a $digest. One possible way to do that is to wrap your legacy code in a $scope.$apply or trigger a $digest once the legacy code has exectuted. Here's some good documentation on this.
Basically whenever a change happens outside of angular (for instance this is a common issue when jQuery causes the change) something has to tell Angular to go see if something changed. It's one way Angular maintains reasonable performance.
Related
As digest cycle do the dirty checking of the variable that is if there are 100 scope variables and if I change one variable then it will run watch of all the variables.
Suppose I have 100 scope model variables that are independent of each other. If I make changes in one variable then I don't want to check all other 99 variables. Is there any way to do this ? If yes, how ?
Surprisingly, this is usually not a problem, Browsers don’t have problems even with thousands of bindings, unless the expressions are complex. The common answer for how many watchers are ok to have is 2000.
Solutions :
It is fairly easy onwards from AngularJS 1.3, since one-time bindings are in core now.
One time Binding of the variables.
We can use One time binding(::) directive to prevent the watcher to watch the unwanted variables. Here, variable will be watch only once & after that it will not update that variable.
Stop the digest cycle manually.
HTML :
<ul ng-controller="myCtrl">
<li ng-repeat="item in Lists">{{lots of bindings}}</li>
</ul>
Controller Code :
app.controller('myCtrl', function ($scope, $element) {
$element.on('scroll', function () {
$scope.Lists = getVisibleElements();
$scope.$digest();
});
});
During the $digest, you are only interested in changes to Lists object, not changes to individual items. Yet, Angular will still interrogate every single watcher for changes.
directive for stop and pause the digest:
app.directive('stopDigest', function () {
return {
link: function (scope) {
var watchers;
scope.$on('stop', function () {
watchers = scope.$$watchers;
scope.$$watchers = [];
});
scope.$on('resume', function () {
if (watchers)
scope.$$watchers = watchers;
});
}
};
});
Now, Controller code should be changed :
<ul ng-controller="listCtrl">
<li stop-digest ng-repeat="item in visibleList">{{lots of bindings}}</li>
</ul>
app.controller('myCtrl', function ($scope, $element) {
$element.on('scroll', function () {
$scope.visibleList = getVisibleElements();
$scope.$broadcast('stop');
$scope.$digest();
$scope.$broadcast('resume');
});
});
Reference Doc : https://coderwall.com/p/d_aisq/speeding-up-angularjs-s-digest-loop
Thanks.
This is a good question and highlights one of the biggest deficiencies with Angular 1.x. There is little control over how the digest cycle is managed. It is meant to be a black box and for larger applications, this can cause significant performance issues. There is no angular way of doing what you suggest, but There is something that would help you achieve the same goals (ie- better performance of the digest cycle when only one thing changes).
I recommend using the bind-notifier plugin. I have no relationship with the project, but I am using it for my own project and have had great success with it.
The idea behind is that you can specify certain bindings to only be $digested when a specific event has been raised.
There are multiple ways of using the plugin, but here is the one that I find must effective:
In a template file, specify a binding using the special bind-notifier syntax:
<div>{{:user-data-change:user.name}}</div>
<div>{{:job-data-change:job.name}}</div>
These two bindings will not be dirty-checked on most digest cycles unless they are notified.
In your controller, when user data changes, notify the bindings like this:
this.refreshUserData().then(() => {
$scope.$broadcast('$$rebind::user-data-change');
});
(and similar for job-data-changed)
With this, the bindings for user.name will only be checked on the broadcast.
A few things to keep in mind:
This essentially subverts one of the key benefits of angular (also it's core weakness for large applications). Two way binding usually means that you don't need to actively manage changes to your model, but with this, you do. So, I would only recommend using this for the parts of your application that have lots of bindings and cause slowdowns.
$emit and $broadcast themselves can affect performance, so try to only call them on small parts of the $scope tree (scopes with few or no children).
Take a good look at the documentation since there are several ways to use the plugin. Choose the usage pattern that works best for your application.
This is quite a specific use-case to do exclusive/conditional checks in the digest cycle and I don't think it is possible without forking/hacking the angular core.
I would consider refactoring how/what you are $watching. Perhaps using ngModelController's $viewChangeListeners would be more suitable than $watch?
I have a directive that abstracts a menu. The menu item selection needs to be notified to its parent controller so it can take required action. There are multiple ways I could get this achieved.
Pass a scope variable from controller to directive and observe the change on this variable. Within the directive change this variable to indicate the selection option.
Pass a callback method from controller to directive. Invoke the callback from directive upon change.
Observe the changes in controller using $scope.$on and notify from the directive using scope.$emit
I could not clearly arrive at which one option is better. I am leaning towards option 3 as it seems to be cleaner but I am not sure if this has a unwanted coupling. I would like to hear an opinion from others, which solution would favour clear dependency and good for testability.
UPDATE:
After reading the suggestions and thought, I picked up Option 2 for below reasons:
It is very obvious by looking into the HTML about the dependency
<menu save="onSave()" filterByDate="filterByDate(date)"></menu>
Unit testing is very explicit and tell about the API (interface) the directive
I personally don't like using observe\emit\on unless i have too. It isn't always obvious looking at someone elses code where it is being set. This can lead to spaghetti code. If you have a call back, i find the direct link is more obvious to the eye and easier to find. This is in conjunction with well named properties etc. At the end of the day it's a matter of personal\team taste.
I recommend using a modified version of 1. But instead of passing a variable, inject a service wherever you need.
The service can look something like this:
yourApp.factory('SelectedMenu',
function () {
//set a default or just initialize it
var menuItem = {
id: 1,
code:"x"
};
return {
getId: function () { return menuItem.id; },
getCode: function() { return menuItem.code;},
setId: function(newId){menuItem.id = newId},
setCode: function(newCode){menuItem.code=newCode;},
};
}
);
In the directive(s) which controlls the behavior create a watch:
$scope.$watch(
function() {
//should be idempotent! can execute multiple times per $digest cycle when a change is detected
// (good compromise for not polluting $rootScope)
return SelectedMenu.getId();
},
function( newValue) {
//do your thing...
}
);
When you need to use it/update the value you can set the code or any other property you want and finally set the id to trigger the update.
SelectedMenu.setId(someones.id);
It is the cleanest solution i came across. If you want, you can abstract the usage even more with another service if the data object is more complex.
I am trying to pass a function from a controller to a directive so an event fired from the directive could cause a refresh in a different controller.
controllers.controller('UserCtrl', function ($scope) {
$scope.name="Name";
$scope.test = function (t) {
console.log("Inside "+t+" "+$scope.name)
return $scope.test2();
};
$scope.test2 = function(){
return 2;
}
}
<my-test-directive respond="test">
This seems to work fine but when I change it to try and match the Google Angular conventions I get an undefined error on return this.test2();. Here is a plunker with the failing version.
Using the "Google Angular style" how would I handle this?
Please see here: http://plnkr.co/edit/Mugs2C7UOjlXx1L3LsMP?p=preview
You need to change few things :
To call function from directive you should use '&' instead '='
In your html change respond="userController.test" to respond="userController.test(msg)"
Finally you should pass object to function instad of string change scope.respond("So I should be getting inside"); to scope.respond({msg:"So I should be getting inside"})
I hope that will helps.
When you pass a function in javascript the context of the function gets lost so the this keyboard won't work anymore. If you want to pass your test function you need to wrap it in a function that is bound to userCtrl. You can do this with bind() by adding the following line to your controller's constructor and passing boundTest to your directive instead of test
this.boundTest = this.test.bind(this);
Note that bind() was introduced in ES5 so it won't work in IE8
Here is a working plunker http://plnkr.co/edit/BTuwTFf2XsmrR3As4x1x?p=preview
Here is what I came up with
Thanks to rob for the idea, I don't know that I like this though for two reasons...
1.) I cannot seem to use prototype, limiting my inheritance options (as I understand it)
2.) This looks rather hacky
$scope.userController.test2();
Depending on the vote count and comments will depend what I mark as the answer so please provide feedback
I have some code that creates and registers some animations inside a controller. Unfortunately, whenever I register an animation inside a controller, it does not seem to initialise (The function never gets called.)
I have created a plunker to illustrate the issue. In this, I call"createAnim1" outside the controller and the animation functions correctly. I call "createAnim2" inside a controller, and the animation does not function.
createAnim1();
app.controller('main', function($scope) {
createAnim2(); //This animation does not work
});
The createAnim function are in the form:
app.animation('.anim1', function() {
return {
...
}
});
Am I missing something obvious? The $animate and $animateProvider documentation don't seem to contain anything relevant to this.
I assume this has to do with the fact that a compilation has already happened, and some step required to initialise the animations is being missed? Is there some way to get around this?
PS: I have only been using angular since last week, so I'm not well versed on the internals.
Edit:
After the comments from Alex, I think it is worth clarifying what I am trying to do:
I have some code which generates css class animation selectors and appends them to the document head. If the browser does not support transitions, the code generates a jquery animation equivalent. I have tried to create a factory to expose this code via DI, and it is when this code is run (From inside a controller) the animation is never able to trigger (The initialisation code is never run).
I'm having an issue getting a watch to work within a directive. I've put together a simple example here. http://plnkr.co/edit/A7zbrsh8gJhdpM30ZH2P
I have a service and two directives. One directive changes a property in the service, and another directive has a watch on that property. I expected the watch to fire when the property is changed but it doesn't.
I've seen a few other questions like this on the site, but the accepted solutions on them have not worked here. I've considered using $broadcast or trying to implement an observer, but it seems like this should work and I don't want to over complicate things if possible.
Mark Rajcok' answer is incomplete. Even with angular.copy(), $watch listener will be called once and never again.
You need to $watch a function:
$scope.$watch(
// This is the important part
function() {
return demoService.currentObject;
},
function(newValue, oldValue) {
console.log('demoService.currentObject has been changed');
// Do whatever you want with demoService.currenctObject
},
true
);
Here the plunker that works: http://plnkr.co/edit/0mav32?p=preview
Open your browser console to see that both the directive and the demoService2 are notified about demoService.currentObject changes.
And btw angular.copy() is not even needed in this example.
Instead of
this.currentObject = newObject;
use
angular.copy(newObject, this.currentObject);
With the original code, the viewer directive is watching the original object, {}. When currentObject is set to newObject, the $watch is still looking for a change to the original object, not newObject.
angular.copy() modifies the original object, so the $watch sees that change.