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.
Related
I'm trying to request a variable declared globally inside of the touchmove function but I'm getting a reference error. Does anybody know what's wrong?
function drawer(pulltab,drawer){
$('#pulltab').on('touchstart',function(e){
notworking=e.originalEvent.touches[0].pageX;
})
$(drawer).on('touchmove',function(loc){
var fingerloc=loc.originalEvent.touches[0].pageX;
var dist=fingerloc-notworking;
console.log(dist);
if (dist<0){
$(this).css('margin-left',dist);
}
})
$(drawer).on('touchend',function(){
$(this).css('transition','margin-left .1s');
$(this).css('margin-left',0);
})
}
drawer('#pulltab','#navigation-drawer');
I'm trying to request a variable declared globally inside of the touchmove function
There are no global variable declarations in your quoted code.
Assuming you haven't declared it, then you are creating (but not declaring) a global variable in the touchstart handler on #pulltab:
notworking=e.originalEvent.touches[0].pageX;
That uses The Horror of Implicit Globals* to create a global. But the global won't exist until that code runs.
Clearly, your touchmove handler on drawer is firing before your touchstart handler on #pulltab. Since there is no existing global called notworking, you can't read its value, and you get a ReferenceError. If the touchstart on #pulltab had executed first, you wouldn't.
Don't rely on the horror of implicit globals. Declare your variables. If you want it to be global, put
var notworking;
...outside all functions. (Although global variables are a Bad Thing™ best avoided; if you only use notworking within the drawer function and you don't need it shared between calls to drawer, just declare it within drawer.) You might also want to check, when using it, whether it has a useful value.
* (that's a post on my anemic little blog)
What I want is very simple, I want the Expand All button to be auto clicked when I open this pluralsight course page. Its HTML is:
<a id="expandAll"
ng-click="expandAllModules()"
ng-hide="allModulesExpanded()">
Expand All
</a>
So it seems easy and we just need to call the function expandAllModules(). However I don't know why it give me undefined when I check its type:
typeof expandAllModules
=> "undefined"
Generally typeof a function should give me "function" like this:
function a(){}
=> undefined
typeof a
=> "function"
Since the function expandAllModules() is not available, I can't call it. Anyone can give me a hand on this issue?
Edit
Perhaps I need to elaborate on my question. I'm not the author of that page. I just want to make a simple greasemonkey or tempermonkey script and expand the modules automatically when I enter the page.
The Problem
The reason calling just expandAllModules() doesn't work is because this function belongs to one of Angular's scopes and isn't a method assigned to window. This function is defined in Plural Sight's table-of-contents-controller-v9.js like so:
"use strict";
pluralsightModule
.controller("TableOfContentsController", ['$scope', ..., function ($scope, ...) {
...
$scope.expandAllModules = function() {
_.each($scope.courseModules, function (module) { module.visible= true; });
};
...
}])
The Solution
In order for us to call this function ourselves, we have to go through this scope.
scope is an object that refers to the application model. It is an execution context for expressions. Scopes are arranged in hierarchical structure which mimic the DOM structure of the application. Scopes can watch expressions and propagate events. – AngularJS: Developer Guide
The scope is part of the element which triggers the function. We can access this particular scope by passing the element's id attribute into angular.element(), then calling scope() on that object:
angular.element('#expandAll').scope()
This will give us the following data, where we can see the expandAllModules() function:
Unfortunately AngularJS doesn't let us simply execute scope().expandAllModules(); instead we have to go through it's $apply and $eval methods:
var scope = angular.element('#expandAll').scope();
scope.$apply(function() {
scope.$eval(scope.expandAllModules())
});
We can now also collapse the modules as well by calling:
scope.$apply(function() {
scope.$eval(scope.collapseAllModules())
});
I apologize if I am off-base here. Are you trying to "link" into that page and 'force' the page to "expand all", or do you have access to the page, and want to trigger the click with some code on the page, by you inserting the code? Just doing something like this seems to work from commandline.
jQuery(function(){
jQuery('#expandAll').trigger('click');
});
Since I do not know your need, my thought is that this is a bit simplistic and not what you are looking for. From the responses of others, it appears you want to create your own directive to initiate the click?
I might have some typos -- but the idea is there.
angular.element(document.body).ready(function() {
var el = angular.element( document.getElementById('expandAll') );
var scope = el.scope();
scope.expandAllModules();
scope.$digest(); <--- might not be needed, but when i check your site, it needs to have this
});
updates
if it was just 'onclick' instead of 'ng-click', you do not need to get the scope; and just call the function directly.
updates
I have tried this on your site, you need to have scope.$digest(). When I tried it, i was using the developer console.
see the developer console below
I was playing with it on your site.
So i am really confused right now, not sure if I'm being stupid or not but.. when my page loads I want to bind some localStorage to a variable called JsonData.
$(document).ready(function() {
var JsonData = localStorage.getItem(0);
.....
Here is a screen shot of my console to prove localStorage.getItem(0) has a item on page load.
Any ideas?
Because the variable JsonData is defined in a dom ready callback, making it local to that method - you are trying to access the variable from the console where the variable does not exists.
If you really want to access the variable across multiple independent scopes then declare it as a global variable, but I would recommend against it(simple doesn't like to pollute the global scope with my variables)
var JsonData = localStorage.getItem(0);
$(document).ready(function () {
});
since the data has nothing to do with the dom structure you can move out of the dom ready handler
Unless the code is paused by a breakpoint in the same scope JsonData resides, console.log operates on the global scope. JsonData lives inside the ready callback and is not accessible from the global scope, thus the error.
I suggest placing a breakpoint somewhere inside the ready handler. Then you can use console.log. In Chrome, you can pop-out the console from anywhere in dev tools by pressing ESC.
I have a function in a parent file that depends on several global variables. I am having the child iframe update one of the parent's global variables with
parent.myvar = "myvalue";
Works great (note: everything is on the same domain). Now I need to run the function in the parent to complete the process. Problem:
parent.myfunction();
I have just run the function I need minus any of the global variables in the parent file (it is being executed within the child iframe). Of course, I could bring over all the global variables from the parent file by just redeclaring all my global variables (I could even have the parent function do all the redeclairing). However, I was hoping for a more elegant solution. Is there any way to get a child to initiate a function to be run within the parent environment?
I'm not sure if I understand the question, but couldn't you pass in a variable reference of parent to the child?
Like
var parentRef = this;
so the child could run
parentRef.myFunction();
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.