What is the "scope" that gets passed to a $scope.$watch function? - javascript

$scope.$watch(
function(scope) {return scope.aNumber;},
function(newValue, oldValue) {alert("Value changed");}
);
});
What is the "scope" that $scope.$watch takes in its first function? (everything after this is extra info that is tangentially related). I know "scope" without the $ generally represent just a variable, as in the directive link function (scope, element, attributes, ngController), etc. However I have no idea where it "comes from" here. Clearly it's connected to the controller's $scope, but how?
Also, the official doc states "The watchExpression is called on every call to $digest() and should return the value that will be watched. (Since $digest() reruns when it detects changes the watchExpression can execute multiple times per $digest() and should be idempotent.)" So what's the advantage to doing the return function rather than just saying $scope.valueToWatch (which doesn't work for me, but I have seen people do it).
Plunk with working watch just for the hell of it, don't really need it for q:
http://plnkr.co/edit/y86Wr93xLIao3wTwVsT8?p=preview
For those reading with same q later:
Good article on $watch: http://tutorials.jenkov.com/angularjs/watch-digest-apply.html

Simply the same scope from which the $watch is being executed. The only advantage to using this one instead of $scope is that it avoids you a useless closure, but it does the same.
I'm not sure to understand your second question, but note that these are equivalent:
1. $scope.$watch(function(scope) { return scope.prop1.prop2; }, cb);
2. $scope.$watch(function() { return $scope.prop1.prop2; }, cb);
3. $scope.$watch('prop1.prop2', cb);

Related

Watching a deleted model?

Consider this psuedo-code:
$scope.model = [{ A: 'a', B: 'b' }, { A: 'c', B: 'd' }];
$scope.$watchCollection('model', (model) => {
for (var i = 0; i < model.length; i += 1) {
$scope.$watch('model[' + i + '].A', () => ...);
}
});
What happens the watch expression if I delete $scope.model[1]? Would it be a "memory leak", a zombie, or some other leakiness?
Edit This method offers a terrible solution to an already solved problem. I ended up using angularjs equality check instead of reference check. Refer to the documentation for $watch.
Both $watch and $watchcollection continue to watch during digest cycles. If the item of interest becomes defined again, the $rootScope executes the registered listener function. You can de-register the watcher by calling the de-register function that was returned when the $watch was registered. (You did save it, didn't you?)
var deRegisterFn = $scope.$watchCollection('model', function (newValue) {
console.log(newValue);
});
delete $scope.model;
deRegisterFn();
Otherwise the watcher remains until the scope is destroyed.
For more information on $watch, see the AngularJS $rootScope.scope API Reference -- $watch.
BTW, your example, adding watchers inside a listening function is very strange. AngularJS is not jQuery and even in the jQuery community there are people who discourage binding and unbinding of listeners. Some even calling it an Anti-pattern.
Update: objectEquality AKA Deep-Watch
$watch(watchExpression, listener, [objectEquality]);
When objectEquality == true, inequality of the watchExpression is determined
according to the angular.equals function. To save the value of the object for
later comparison, the angular.copy function is used. This therefore means that
watching complex objects will have adverse memory and performance implications.
For performance reasons the $watch function uses a "shallow watch". The author of the question needed a "deep watch" of his object. Being unaware of the "objectEquality" option, also known as (AKA) the "deep watch" option, he solved his problem by adding and removing watches on properties of his model. The "deep watch" option solved his problem in a cleaner, more elegant manner. Thus obviating the need to add and remove watches.
For more information on $watch, see the AngularJS $rootScope.scope API Reference -- $watch.

Is AngularJs digest cycle triggered after any function call?

I have a directive that defines a function innerBarStyle() at the link stage and binds it to the scope:
restrict : 'EA',
scope: {
values: '='
},
link: function(scope, elements, attributes){
scope.innerBarStyle = function(value){
console.count("innerBarStyleCounter");
return {
width: 10px;
};
}),
templateUrl: 'template.html'
};
The function does nothing but counting the number of times it gets executed and returning an object.
Now, in a template directive's template I'm calling this function by means of an expression. Something like <div ... ng-style=innerBarStyle(someValueInCurrentScope)><div>
What I get in practice is an infinite loop that causes the aforementioned function to be called repeatedly.
After some research, I've found that this usually occurs when the called function implicitly or explicitly triggers the digest cycle (e.g. if it makes use of the $http service). But in this case the function is really doing nothing. Is it possible that the digest cycle is triggered somewhere else or am I missing something?
BTW, I know that there would be better ways to achieve the same result, I'm just curious about how things works here.
Without seeing the actual code (I understand you can't post the exact code since it's for your work) I can only guess. But I think what's happening is that you are adjusting the style of the element via the return of the $scope. innerBarStyle which triggers the ng-style directive which calls a digest cycle, which triggers the scope function again. Ergo the continuous execution of this logic.
In order to fix this you should probably use the angular.element APIs on the elem of the directive to adjust the CSS.

Can I access $timeout without using the DI container?

In AngularJS, can I access $timeout without resorting to using the DI container?
Edit:
For those asking for "why". I am using an older version of AngularJS and want to create a utility function that will perform a digest asynchronously.
The intention being that I can place logic inside a promise then for execution after a digest has occurred and the UI has taken into account the model change.
I do not want client code to have to use the injector to use said function.
I wanted something like this:
my-file.js
//...
model.watchedProperty = 'new value';
// Now I want to wait for a digest to occur so that I can ensure the UI is updated before proceeding...
digestAsync(localScope)
.then(function() {
// continue...
});
// ...
digest-async.js
function digestAsync(scope) {
return $timeout(function() { // I don't want to have to use the injector...
scope.$digest();
});
}
You can manually get the injector and then get the $timeout service.
var $injector = angular.injector(['ng']);
var $timeout = $injector.get('$timeout');
If you don't want to inject $timeout you can add $injector as a DI, and in your code you can put this:
$timeout = $injector.get('$timeout');
No, you can not. A lot of angular is itself written in angular, including $timeout. So you can access it in any way you can access any other self-written service - by Dependency Injection
You only need to use $timeout if you want the callback function to be executed inside an Angular digest cycle, and if you pass 0 as the interval then it will be executed in the next digest.
The setTimeout function from JS will execute the callback using the next "thread" cycle. That means that the current thread has to terminate first before the callback can be execute. That doesn't mean that the next thread cycle will also be an Angular digest.
This doesn't matter in your example because you are forcing a $digest, except that you should be using $apply and not $digest.
I think what you are trying to do is create a promise that resolves inside an Angular digest. That's not really the proper use for promises because a digest is not a resource to be resolved.
I think you can skip everything related to the $timeout and just use $apply as it was designed.
localscope.$apply(function(){
// do digest work here
});
That is the same as the following.
$timeout(function(){
// do digest work heere
},0);
Both can be executed outside of Angular, and both will execute the callback during the next digest cycle. $apply will call $digest if needed and it does state this in the documentation.
For times when you don't know if a $digest is in progress.
/**
* Executes the callback during a digest.
*
* #param {angular.IScope|angular.IRootScopeService} $scope
* #param {function()} func
*/
function safeApply($scope, func) {
if ($scope.$$phase) {
func();
} else {
$scope.$apply(function () {
func();
});
}
};
Since you plan to use it outside the app, there is zero chance that you will stumble upon infamous '$digest already in progress' error. Why? Because $digest isn't asynchronous process, more like the opposite of it. All that $digest() function does is calculating current scope state in loop - no promises, no timeouts.
That's exactly what
Don't do if (!$scope.$$phase) $scope.$apply(), it means your
$scope.$apply() isn't high enough in the call stack.
well-known statement refers to. The only time when 'already in progress' will happen is when $digest is triggered during $digest or within $apply, e.g. when outer JS function is used as Angular callback. This indicates poor application design and should be treated accordingly.
Thereby $digest function can be exposed to window
app.run(function ($rootScope) {
window.$digest = angular.bind($rootScope, $rootScope.$digest);
});
And used in synchronous manner. No promises. No timeouts.
model.watchedProperty = 'new value';
$digest();
// 'watchedProperty' watcher has been digested ATM
And I assume that you already know why mixing Angular and outer code like that is considered a bad practice and should be avoided.

Calling $setViewValue in a directive doesn't fire $watch in the controller

I am using a directive with a ngModelController like this
var directive = {
link: link,
restrict: 'E',
require: '?ngModel'
}
My link function does something like this (I omitted the long directive stuff)
function link(scope, element, attrs, ctrl) {
scope.updateModel = function(){
var newModel = {};
newModel.aValue = 'foo';
ctrl.$setViewValue(newModel);
}
}
I use the directive in my controller
View: this is correctly updated
<my-directive ng-model="aModel">
{{aModel.aValue}} // This is changed correctly when I update the value in the directive
</my-directive>
Controller: here is where I have problems
$scope.aModel = {};
$scope.aModel.aValue = 'bar';
$scope.$watch('aModel', function(newValue, oldValue){
console.log('aModel changed'); // This is never fired
});
$scope.$watch('aModel.aValue', function(newValue, oldValue){
console.log('aModel changed'); // This is neither fired
});
The documentation of $setViewValue says
"Note that calling this function does not trigger a $digest."
so I tried to fire it manually with $render, $digest or $apply, but every time run into trouble since the console says that the $digest is already in progress.
So what's the problem with this?
UPDATE
I just realized something really interesting.
Adding a $timeout to check the value in the controller, I realized that it is not actually changed and that's probably why the $watch function is not called.
But since the view is updated, I don't really get the point.
This is the situation:
Controller
$timeout(function(){
console.log('Check aModel value in the controller');
console.log($scope.aModel.aValue); // This is always 'bar' even if the view display 'foo'
},10000);
UPDATE 2
I think I found the problem: ngModel cannot be an object but it must be a value.
I leave the question open to see if anyone comes with a different solution, but I guess that's the point.
UPDATE 3
I was wrong, you can update the ngModel as object, but there is no way to fire the $watch method.
I create a plunker to show the problem.
I put a large timeout to avoid any $digest in progress problem.
Any hint would be really appreciated.
Add third parameter true to $watch function.
Use $timeout with no time parameter in order to call $apply and trigger the digest loop. This will cause it to execute a fraction of a second later and not run into the digest loop in a digest loop error. Clearly what your doing is not 'angular aware' and so you need to trigger the digest loop using $apply but your in a digest loop so you need to use the 'hack' mentioned above.

Is it safe to remove dependencies in the controller function when the directive does not seem to use them?

The following controller does not seem to use $element, $attrs, $transclude. The controller code below runs fine if these params are commented out.
myApp.directive("menu", function () {
return {
restrict: "E",
scope: {},
transclude: true,
replace: true,
template: "<div class='menu' data-ng-transclude></div>",
controller: function ($scope ,$element, $attrs, $transclude) {
$scope.submenus = [];
console.log('[$element]->', $element);
console.log('[$attrs]->', $attrs);
console.log('[$transclude]->', $transclude);
this.addSubmenu = function (submenu) {
console.log('[addToggleMenu]->');
$scope.submenus.push(submenu);
}
this.closeAllOtherPanes = function (displayedPane) {
angular.forEach($scope.submenus, function (submenu) {
if (submenu != displayedPane) {
console.log('[displayedPane]->', displayedPane);
submenu.removeDisplayClass();
}
})
}
}
}
});
Here is my working fiddle.
Since posting, I have learned that in JavaScript, a function can be called with any number of arguments no matter how many of them are listed. In some languages, a programmer may write two functions with same name but different parameter list, and the interpreter/compiler would choose the right one. That is called function polymorphism. Having used function polymorphism most of my career I expected to be told "Hey , you're not using this param". Also I did not understand that well that the controller parameters are dependencies while the link function parameters are order based. I still struggle in understanding whether $scope, $element or commonly used parameters in the directives internal controller are required and which are optional. Apparently the $ is only required in the controller and not in link because of the DI injection of angular services..whew lot to digest.
A special thanks to Esteban for explaining special pseudo-array inside each function
called arguments. This explains the link function which is half the equation. So I have rewritten the question in hopes that it may get answered. This excellent explanation,
straightened out most of my confusion.
Why are you wanting to remove the $attrs argument? The directive's arguments are in a specific order and removing any one of them will cause subsequent arguments to not be what you think they are.
Say you changed your link function to something like this:
function ($scope, $iElement, menuController) {
menuController.addSubmenu($scope); // this will throw an exception
// because menuController is actually
// $attrs
The reason for this is due to the fact that the function will be called with the same arguments passed in regardless of whether they were defined in your directive's link function.
function argTest(one, two){
console.log('argTest.arguments', arguments);
// even though I don't have three defined, it is still passed in:
console.log('three is passed in', arguments[2]);
}
argTest(1,2,3);
JSFiddle for above: http://jsfiddle.net/TwoToneBytes/NM8DK/

Categories

Resources