Clearing an interval when an element housing an angular directive is removed - angularjs

I've built a simple directive that adds a javascript-based loading animation. It is operating with a window.setInterval() loop. This works great, but when loading is complete, I use ngSwitch to swap in my content, which removes the element housing the loading directive attribute from the page.
Ideally, I'd like to watch for this change and clear my interval so the animation calculations are not still running in the background. I have tried watching a custom function that evaluates the presence of the element on the page. I know the function works at detecting this, but it seems timing is an issue -- namely, as far as I can tell, the $watch itself is cleared when the directive attribute's element leaves the page. My $watch'ed expression therefore never detects a change and never calls its callback that clears the animation interval function.
Is there a recommended pattern for dealing with this type of situation?
Relevant snippet from my template:
<div ng-switch on="dataStatus">
<div ng-switch-when="loading">
<div loading-spinner></div>
</div>
<div ng-switch-when="haveData">
<!-- data dependent on content we were loading -->
</div>
</div>
Simplified version of my directive:
myModule.directive('loadingSpinner', function () {
var updateMySweetAnimation = function (element) { /* ... */ };
return {
link: function (scope, iElement, iAttrs) {
var spinner = window.setInterval(function () {
updateMySweetAnimation(iElement);
}, 100);
scope.$watch(function () {
return $(document).find(iElement).length;
}, function (present) {
if (!present) {
clearInterval(spinner);
}
});
}
};
});

When the element is cleared from the page by ng-switch, two things should happen:
The scope created for ng-switch-when, the element with your directive on, is destroyed. This kills your $watch and generates a $destroy event across the scope that you can watch with scope.$on('$destroy', ...).
The element is removed from the DOM. This generates a separate destroy event that you can watch with iElement.on('$destroy', ...).
They should happen in this order, looking at the latest stable release (1.0.8 - https://github.com/angular/angular.js/blob/v1.0.8/src/ng/directive/ngSwitch.js), so your scope and thus your watch should always be dead when the element is removed from the DOM.
You could avoid this problem by watching from the outer scope, where ng-switch is defined. Or you could watch dataStatus, the same condition as in your ng-switch, rather than looking for the results of the ng-switch seeing your condition change.
Both of these would probably work, but actually all you need to do, and in fact the normal pattern for this, is to just watch for one of the $destroy events and clean everything up there. As the interval feels more relevant to the view than the model, I would use the DOM event and replace your $watch with
iElement.on('$destroy', function(){
clearInterval(spinner);
});

Related

Angularjs & dialog jQueryui - change {{msg}} only after the dialog closed

I'm creating a jQueryui dialog in an angularjs directive.
The template of the directive is very simple:
<div class="myPopup">
<div>{{msg}}</div>
</div>
In the link method in the directive, I'm creating the dialog and register on openPopup event:
link: function (scope, element) {
var popupEl = $(element).find('.myPopup');
popupEl.dialog({ ... });
scope.$on('openPopup', function (event, args) {
scope.msg = "MY_MSG";
popupEl.dialog( "open" );
});
}
Pay attention that I'm populate scope.msg variable.
When the popup is open, I cannot see my msg.
I looked into the DOM and found that when the popup was opened, the {{msg}} was empty:
<div class="myPopup">
<div></div>
</div>
but when I closed the popup the DOM changed and the msg changed to scope.msg:
<div class="myPopup">
<div>MY_MSG</div>
</div>
Can someone please tell me what is wrong here and why it reacts like this?
Thanks!
As you have noted when you add scope.$apply() after your change to the scope.msg the value is updated correctly, but you get an exception of $apply already in progress.
If you read this article it explains why you need to use the $scope.$apply() method. Essentially AngularJS watches for changes to data on your scope, then it runs through a process of notifying the parts of your code that care about these changes. However sometimes parts of your code run while this process is in the middle of notifying, and if that happens and your code makes changes to the scope, it doesn't get seen until a little later - which is why you saw the message only update when the dialog closed.
So you need to trigger the $scope.$apply manually. Sometimes this can simply be to call that command after you make your change, but other times it will conflict and throw the 'in progress' exception.
The solution is to use the applyAsync method, which will queue your code to run in the next digest cycle; this is usually after a delay of about 10 milliseconds.
link: function (scope, element) {
var popupEl = $(element).find('.myPopup');
popupEl.dialog({ ... });
scope.$on('openPopup', function (event, args) {
// Trigger the change in the next digest cycle
scope.$applyAsync(function() {
scope.msg = "MY_MSG";
popupEl.dialog( "open" );
});
});
}

ng-show directive takes too long to update the dom after trigger

The app has a controller, that uses a service to create an instance of video player. The video player triggers events to show progress every few seconds. When the video reaches to a certain point, I want to show a widget on top of the video player.
The view has the widget wrapped in ng-show directive.
It takes more then 60 seconds for the dom element to receive the signal to remove the ng-hide class after the event has been triggered and the values have been populated.
If I try to implement this using the plain dom menthod (like document.getElementById(eleId).innerHTML = newHTML), the update is instant.
What am I doing wrong? Here is the complete sequence in code:
Controller:
MyApp.controller('SectionController', ['$scope', 'PlayerService'], function($scope, PlayerService){
$scope.createPlayer = function() {
PlayerService.createPlayer($scope, wrapperId);
}});
Service:
MyApp.service('PlayerService', [], function(){
this.createPlayer=function(controllerScope, playerWrapper){
PLAYER_SCRIPT.create(playerWrapper) {
wrapper : playerWrapper,
otherParam : value,
onCreate : function(player) {
player.subscribe(PLAY_TIME_CHANGE, function(duration){
showWidget(controllerScope, duration);
})
}
}
}
function showWidget(controllerScope, duration) {
if(duration>CERTAIN_TIME) {
$rootScope.widgetData = {some:data}
$rootScope.showWidget = true;
}
}});
View:
<div ng-show="showWidget"> <div class="wdgt">{{widgetData.stuff}}</div> </div>
Solved it! $scope.$apply() did the trick.
My guess is, due to other complex logic ad bindings inside the app, there was a delay in computing the change by angular the default way.
#floribon Thanks for the subtle hint about "complex angular stuff".
The code inside the service function changed to:
function showWidget(controllerScope, duration) {
if(duration>CERTAIN_TIME) {
$rootScope.widgetData = {some:data}
$rootScope.showWidget = true;
$rootScope.$apply();
}}
Do you have complex angular stuff within your hidden view?
You should try to use ng-if instead of ng-show, the difference being that when the condition is false, ng-if will remove the element from the DOM instead of just hidding it (which is also what you do in vanilla JS).
When the view is simply hidden using ng-show however, all the watchers and bindings within it keep being computed by Angular. Let us know if ng-if solve your problem, otherwise I'll edit my answer.

$watch on clientWidth isn't working

Inside of my directive I have this and it works only in the beginning, and whenever I resize later it never fires.
scope.$watch ->
cw: element[0].clientWidth
,(newValue, oldValue)->
if newValue.cw isnt oldValue.cw
console.log "changed"
,true
Your function looks right to me, but it's important to note that it won't fire on resize unless you manually trigger a digest on the resize event.
Without this, angular doesn't realise there's been an event that should trigger a digest, and so none happen.
Try adding this (remember to inject $window):
angular.element($window).bind('resize', ()->
scope.$apply();
)
Just like Jason Goemaat said, its because Watches are only checked on a $digest cycle
Depending on how the way your element is resized maybe this example may help you.
Here is a menu that expands it self when pressed on the arrow.
<div class="menu" ng-class="{'expanded' : main.expanded}">
<div class="menu-handler" ng-click="main.expanded = !main.expanded">
</div>
</div>
The menu is expanded by a ng-click that makes a change on the scope, in this case its a boolean var that acts as a flag. As the opening of the menu is made throught the change in the scope it calls the $digest cycle iterating through all $watch.
scope.$watch(function() {
return element[0].clientWidth;
}, function(oldValue, newValue) {
console.log(oldValue, newValue);
});
You can view the full example in this JSfiddle
https://jsfiddle.net/owenbcal/zf17s0mL/
Hope i was helpfull.
the way I know about watching this kind of values is making a function that is fired once the value changes so then I watch that function :
in the service:
this.getDataOut = function () {
return self.dataOut;
};
in the controller:
$scope.$watch(dataService.getDataOut, function() {
$scope.dataOut = dataService.dataOut;
});

Adding ng-change to child elements from linking function of directive

I created a directive that should add a ng-change directive dynamically to all child input tags:
myApp.directive('autosave', function ($compile) {
return {
compile: function compile(tElement, tAttrs) {
return function postLink(scope, iElement, iAttrs) {
var shouldRun = scope.$eval(iAttrs.autosave);
if (shouldRun) {
iElement.find(':input[ng-model]').each(function () {
$(this).attr("ng-change", iAttrs.ngSubmit);
});
$compile(iElement.contents())(scope);
console.log("Done");
}
}; //end linking fn
}
};
});
The problem that I have is that the ng-change directive isn't running. I can see it that its added to the DOM element BUT not executing when value changes.
The strange thing is that if I try with ng-click, it does work.
Dont know if this is a bug on ng-change or if I did somehting wrong.
Fiddle is with ng-click (click on the input) http://jsfiddle.net/dimirc/fq52V/
Fiddle is with ng-change (should fire on change) http://jsfiddle.net/dimirc/6E3Sk/
BTW, I can make this work if I move all to compile function, but I need to be able to evaluate the attribute of the directive and I dont have access to directive from compile fn.
Thanks
You make your life harder than it is. you do'nt need to do all the angular compile/eval/etc stuff - at the end angular is javascript : see your modified (and now working) example here :
if (shouldRun) {
iElement.find(':input[ng-model]').on( 'change', function () {
this.form.submit();
});
console.log("Done");
}
http://jsfiddle.net/lgersman/WuW8B/1/
a few notes to your approach :
ng-change maps directly to the javascript change event. so your submit handler will never be called if somebody uses cut/copy/paste on the INPUT elements. the better solution would be to use the "input" event (which catches all modification cases).
native events like change/input etc will be bubbled up to the parent dom elements in the browser. so it would have exactly the same to attach the change listener to the form instead of each input.
if you want to autosave EVERY edit that you will have an unbelievable mass of calls to your submit handler. a better approach would be to slow-down/throttle the submit event delegation (see http://orangevolt.blogspot.de/2013/08/debounced-throttled-model-updates-for.html ).
if you want to autosave EVERY edit you skip your change handler stuff completely and suimply watch the scope for changes (which will happen during angular model updates caused by edits) and everything will be fine :
scope.watch( function() {
eElement[0].submit();
});

Fire an event immediately after $scope.$digest

In my AngularJS app, there's several points at which I want to wait for a $scope to be processed into the DOM, and then run some code on it, like a jQuery fadeIn, for example.
Is there a way to listen for a "digestComplete" message of some sort?
My current method is: immediately after setting whatever $scope variables I want rendered, use setTimeout with a delay of 0 ms, so that it will let the scope finish digesting, and then run the code, which works perfectly. Only problem is, I very occasionally see the DOM render before that setTimeout returns. I'd like a method that is guaranteed to fire after digest, and before render.
In this jQuery fade-in-and-out fiddle (which I found it on the JSFiddles Examples wiki page), the author defines a "fadey" directive and performs the jQuery fadeIn (or fadeOut) in the directive's link function"
<li ng-repeat="item in items" fadey="500">
...
myApp.directive('fadey', function() {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
var duration = parseInt(attrs.fadey);
if (isNaN(duration)) {
duration = 500;
}
elm = jQuery(elm); // this line is not needed if jQuery is loaded before Angular
elm.hide();
elm.fadeIn(duration)
Another possible solution is to use $evalAsync: see this comment by Miško, in which he states:
The asyncEval is after the DOM construction but before the browser renders.
I believe that is the time you want to attach the jquery plugins. otherwise
you will have flicker. if you really want to do after the browser render
you can do $defer(fn, 0);
($defer was renamed $timeout).
However, I think using a directive (since you are manipulating the DOM) is the better approach.
Here's a SO post where the OP tried listening for $viewContentLoaded events on the scope (which is yet another alternative), in order to apply some jQuery functions. The suggestion/answer was again to use a directive.
Alternatively, this example will work the same way as an AngularJS built-in ng-show directive, except it will fade-in or fade-out based on AngularJS condition:
<li ng-repeat="item in items" ng-ds-fade="{condition}">
<!-- E.g.: ng-ds-fade="items.length > 0" -->
...
myApp.directive('ngDsFade', function () {
return function(scope, element, attrs) {
element.css('display', 'none');
scope.$watch(attrs.ngDsFade, function(value) {
if (value) {
element.fadeIn(200);
} else {
element.fadeOut(100);
}
});
};
});
Source:
http://www.codeproject.com/Articles/464939/Angular-JS-Using-Directives-to-Create-Custom-Attri
If all you want is to run some jQuery stuff why not try the Angular-UI jQuery Passthrough?

Resources