Watching a deleted model? - javascript

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.

Related

Angular scope watch doesn't work in my app

I've got the following code where I'm trying to show/hide a text box based on if a key was pressed in the global window scope. However, every time a key is pressed, it does not seem to fire the watch service. Why is this?
Plnkr here http://plnkr.co/edit/qL9ShNKegqJfnyMvichk
app.controller('MainCtrl', function($scope, $window) {
$scope.name = '';
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name)
})
$scope.$watch('name', function() {
console.log('hey, name has changed!');
});
});
It is because you are handling the keypress event outside of the digest cycle. I would strongly encourage you to let angular do its thing with databinding or using ngKeypress
Otherwise, in your handler, call $scope.$digest().
angular.element($window).on('keypress', function(e) {
//this changes the name variable
$scope.name = String.fromCharCode(e.which);
console.log($scope.name);
$scope.$digest();
})
On a high level view, watching a value on a scope needs two parts:
First: the watcher - like you created one. Every watcher has two parts, the watch function (or like here the value) and the listener function. The watch function returns the watched object, the listener function is called when the object has changed.
Second: the $digest cycle. The $digest loops over all watchers on a scope, calls the watch function, compares the returned newValue with the oldValue and calls the corresponding listener function if these two do not match. This is called dirty-checking.
But someone has to kick the $digest. Angular does it inside its directives for you, so you don't care. Also all build-in services start the digest. But if you change the object outside of angular's control you have to call $digest yourself, or the preferred way, use $apply.
$scope.$apply(function(newName) {
$scope.name = newName;
});
$apply first evaluates the function and then starts the $digest.
In your special case, I would suggest to use ngKeypress to do it the angular way.

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

$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);

Why are scopes not destroyed even after $destroy is triggered?

I have made a directive that, when clicked, creates a dialog that is appended to the body using jQuery. The problem is that when the dialog is closed, the scopes are never properly cleaned up. As shown in the picture below 167 ChildScopes are preserved. Which matches the amount of items in the dialog which includes the ng-repeat directive.
I attempted to create an extremely simple version of the scenario on Plnkr. To my surprise the scopes ARE in fact being removed on each close in the Plnkr. So something, somewhere in production is causing the scopes to stay alive even after $destroy has been called.
link: ($scope, $element, $attr) ->
$element.on 'click', () ->
$scope.$apply () ->
child = $scope.$new()
template = """<span ng-controller="ListCtrl">...List dialog things...</span>"""
compiledTemplate = $compile(template)(child)
container = containers.createDialogContainer($element)
container.append(compiledTemplate)
#cleanup
$scope.closeWidget = () ->
container.trigger("container_close")
return
container.on "container_close", ()->
child.$apply () ->
child.$destroy()
return
So here is my question:
What can cause a scope to stay alive even after $destroy has been called, triggered and garbage collection performed?
For obvious reasons I cannot show you our production code. However the directive in the Plnkr matches the one im debugging sufficiently.
In general, a scope (or any other JS object) can not be cleaned up by the GC if it is still accessible by another JS object.
In practice, in an Angular project that also uses JQuery, this is most likely caused by:
an Angular service, controller, scope or some other object still having a reference to your scope object
a reference to your object still exists through a DOM element, probably through an event listener. The DOM element might not be GC-able itself because it is still in the cache of JQuery
Your example, for instance, creates a memory leak.
From your code:
$scope.removeDialog = () ->
console.log "closing"
child.$destroy()
$('.listshell').remove()
return
You do not set child to null so $scope.removeDialog still has access to the scope object referenced by the child variable. Therefore, this object can not be GC'ed.
NB: It seems to me that it would be more appropriate to put removeDialog on the child scope. Now your example only works because the child scope is not isolated.
Closure functions can cause a functions Activation object to stay alive even after the scope has been "destroyed". For instance you might have inner functions still referencing variable objects in the functions whose scope you are trying to destroy.
nullifying the reference variables would be the best option as opposed to delete
The only thing I can think of is that if you somewhere have a global function or function outside of the controller or directive that references a method inside of the scope for the directive it will infact keep that scope alive for the duration of the application.

$scope.$watch isn't firing after load in angular.js

I'm using angular.js and am trying to use $watch to fire a function when my variable changes. It fires when the data is initially loaded, but not after. I'm not sure exactly what is going on here?
Code is pasted below:
function gradeChart($scope, $http) {
$http.get('studentData.json').success(function(data) {
$scope.students = data;
});
$scope.$watch('students',function(change){
console.log('this fires on load but not after');
});
}
It is not clear what code runs "after" and updates $scope.students.
However, here are the two most common problems related to updating $scope arrays:
If you reassign $scope.students to a new array, the $watch may still be looking at the previous array (reference). Try using angular.copy() in your "after" code:
angular.copy(data, $scope.students);
If you are changing one of the elements of the array, you'll need to use either $watchCollection (if it is available in the version of Angular you are using) or check for object equality instead of reference (note 3rd parameter):
$scope.$watch('students',function(change){...}, true);

How can I tell AngularJS to "refresh"

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.

Categories

Resources