AngularJS $destroy on $rootscope never gets called to cancel timeouts - javascript

I have the following code -
function initialize() {
var defer = $q.defer();
var deferTimer = $q.defer();
var cancelTimeout = $timeout(function() {
if (defer !== null) {
ctrlr.setProcessingParameters('XXX');
defer = ctrlr.openProgressBar();
deferTimer.resolve();
}
}, 1000);
deferTimer.promise.then(function() {
var cancelTimeout2 = $timeout(function() {
if (defer !== null) {
defer.resolve();
ctrlr.setProcessingParameters('Please Wait...');
defer = ctrlr.openProgressBar();
}
}, 4000);
});
//Process Backend service n resolbve defer....
}
// cancel the $timeout service
$rootScope.$on('$destroy', function() {
logger.log("cancelTimeout..timer..");
if (cancelTimeout) {
$timeout.cancel(cancelTimeoutProcess);
cancelTimeout = null;
}
});
// cancel the $timeout service
$rootScope.$on('$destroy', function() {
logger.log("cancelTimeout2..timer..")
if (cancelTimeout2) {
$timeout.cancel(cancelTimeout2);
cancelTimeout2 = null;
}
});
I do not see the loggers print or debugger gets into $destroy. Not sure what's happening here.

$rootScope gets destroyed when you close or leave the page. Everything will be gone then, so there's nothing to clean up at that time.
What you are looking for is $destroy on $scope instead,
$scope.$on('$destroy', function() {
logger.log("cancelTimeout..timer..");
if (cancelTimeout) {
$timeout.cancel(cancelTimeoutProcess);
cancelTimeout = null;
}
});
While in the controller, $scope.$on('$destroy'.. will be called when controller gets destroyed (and not the whole application) with which current $scope is associated.

Related

window.onunload event wont fire inside AngularJS controller

I have the following command inside an AngularJS controller
window.onunload = function () {
connection.invoke("RemoveUser", playerName);
}
It's weired because I have a pure JS where this statement works well, so outsite an angularJS controller when I close the tab or the window, it fires and do its job, but when I put this inside a controller, it doesn't fire. Any ideas?
Full script bellow
angular.module("mathGameApp", []).controller("mathGameCtrl", function ($scope) {
// Current player name
$scope.playerName;
$scope.welcomeIsVisible = true;
$scope.gameAreaIsVisible = false;
$scope.countdownIsVisible = false;
// Create connection
const connection = new signalR.HubConnectionBuilder()
.withUrl("/MathGame")
.configureLogging(signalR.LogLevel.Information)
.build();
// Get math challenge
connection.on("GetChallenge", data => {
// Bind challenge
$scope.expression = data.expression + " = " + data.possibleResult;
$scope.$apply();
});
// Receive and bind score
connection.on("ReceiveScore", data => {
$scope.score = data;
$scope.$apply();
});
// Rise alert
connection.on("RiseAlert", data => {
alert(data);
})
// Get status that the player was added to game room
connection.on("AddedToGameRoom", data => {
$scope.welcomeIsVisible = false;
$scope.gameAreaIsVisible = true;
$scope.$apply();
})
connection.on("ChallengeFinished", data => {
$scope.counter = 5;
$scope.countdownIsVisible = true;
$scope.$apply();
let interval = setInterval(function () {
if ($scope.counter == 0) {
$scope.countdownIsVisible = false;
$scope.buttonIsDisabled = false;
$scope.$apply();
clearInterval(interval);
connection.invoke("RefreshChallenge");
}
$scope.counter--;
$scope.$apply();
}, 1000);
})
// rise answer Correct/Wrong
connection.on("RiseAnswer", data => {
$scope.buttonIsDisabled = true;
$scope.expression = data;
$scope.$apply();
console.log($scope.buttonsDisabled);
console.log($scope.expression);
})
// Request the user to be added to game room
$scope.enterGame = function (playerName) {
connection.invoke("EnterGame", playerName);
}
$scope.answerQuestion = function (playerName, answer) {
connection.invoke("AnswerQuestion", {
"playerName": playerName, "isCorrect": answer
});
}
// Open connection
connection.start().then(() => {
}).catch((err) => {
alert(err.toString())
});
window.onunload = function () {
connection.invoke("RemoveUser", playerName);
}
})
Controllers should use the $onDestroy Life-Cycle Hook to release external resources.
app.controller("mathGameCtrl", function ($scope) {
̶w̶i̶n̶d̶o̶w̶.̶o̶n̶u̶n̶l̶o̶a̶d̶ ̶=̶ ̶f̶u̶n̶c̶t̶i̶o̶n̶ ̶(̶)̶ ̶{̶
this.$onDestroy = function () {
connection.invoke("RemoveUser", playerName);
}
})
For more information, see AngularJS $compile Service API Reference - Life-Cyle hooks.
Update
You can and should handle the 'unload' event through window.addEventListener(). It allows adding more than a single handler for an event. This is particularly useful for AJAX libraries, JavaScript modules, or any other kind of code that needs to work well with other libraries/extensions.
For more information, see
MDN Web API Reference - WindowEventHandlers.onunload
MDN Web API Reference - EventTarget.addEventListener()

$scope.$watch only triggered once

I've set the following watcher in my controller:
var embeds = {twitter: false, facebook: false};
$scope.$watch(embeds, function(newVal, oldVal) {
if(embeds.twitter && embeds.facebook) $scope.loading = appLoader.off();
});
This should fire when embeds changes. I have the following functions that check if all my embedded Tweets and Facebook posts have loaded for the page. When all Tweets or Facebook posts are loaded, it updates embeds within a $timeout block in order to trigger a digest cycle.
checkFBInit();
twttr.ready(function(twttr) {
twttr.events.bind('loaded', function(event) {
$timeout(function() {
embeds.twitter = true;
});
});
});
function checkFBInit() {
// Ensure FB.init has been called before attempting to subscribe to event
var fbTrys = 0;
function init() {
fbTrys++;
if (fbTrys >= 60) {
return;
} else if (typeof(FB) !== 'undefined') {
fbTrys = 60;
FB.Event.subscribe('xfbml.render', function() {
$timeout(function() {
embeds.facebook = true;
});
});
return;
} else {
init();
};
};
init();
};
The problem I'm having is that my watcher only fires once when I set it. I've tried binding embeds to $scope and/or watching embeds.twitter and embeds.facebook but the watcher only ever fires once.
Use:
$scope.embeds = {twitter: false, facebook: false};
$scope.$watch('embeds', function(newVal, oldVal) {
if ($scope.embeds.twitter && $scope.embeds.facebook) {
$scope.loading = appLoader.off();
}
}, true);
See https://docs.angularjs.org/api/ng/type/$rootScope.Scope. First argument must be string or function which return the name of param.

Why is my object not updated in the view in Angular?

I have SignalR working in my application:
app.run(['SignalRService', function (SignalRService) {}]);
SignalRService:
app.service("SignalRService", ['$rootScope', function ($rootScope) {
var masterdataChangerHub = $.connection.progressHub;
if (masterdataChangerHub != undefined) {
masterdataChangerHub.client.updateProgress = function (progress) {
$rootScope.$broadcast('progressChanged', progress);
}
masterdataChangerHub.client.completed = function (result) {
$rootScope.$broadcast('taskCompleted', result);
}
}
$.connection.hub.start();
}]);
As you can see I throw an event when a SignalR method gets invoked. This all works fine. However, on 1 directive, my data won't get updated. Here's the code:
app.directive('certificateDetails', ['CertificateService', 'TradeDaysService', 'DateFactory', function (CertificateService, TradeDaysService, DateFactory) {
return {
restrict: 'E',
templateUrl: '/Certificate/Details',
scope: {
certificateId: '=',
visible: '=',
certificate: '=',
certificateSaved: '&'
},
link: function (scope, element, attributes) {
scope.certificateFormVisible = false;
scope.showCancelDialog = false;
scope.splitCertificateFormVisible = false;
scope.partialPayoutFormVisible = false;
scope.$on("taskCompleted", function (evt, response) {
console.log(response);
CertificateService.getCertificateById(scope.certificate.Id).then(function (response) {
scope.certificate = response;
});
});
scope.$watch('visible', function (newVal) {
if (newVal === true) {
scope.showButtonBar = attributes.showButtonBar || true;
if (scope.certificateId) {
getCertificateById();
}
}
});
function getCertificateById() {
CertificateService.getCertificateById(scope.certificateId).then(function (response) {
scope.certificate = response;
});
};
}
}
}]);
The weird thing is, when I have my console open (I use Chrome) on the network tab, I can see that the directive makes a request to the right URL with the right parameters. Also, when the console is open, my data is updated in the view. However, and this is the strange part, when I close the console, nothing happens! It doesn't update the view..
I have also tried to put the code inside the taskCompleted event in a $timeout but that doesn't work either.
Could someone explain why this happens and how to solve this problem?
EDIT I
This is how the getCertificateById looks like in my CertificateService
this.getCertificateById = function (id) {
var promise = $http.post('/Certificate/GetById?id=' + id).then(function (response) {
return response.data;
});
return promise;
};
Handling SignalR events will execute out of the Angular context. You will need to $apply in order to force digest for these to work. I'd try to call $apply on $rootScope after the $broadcast:
var masterdataChangerHub = $.connection.progressHub;
if (masterdataChangerHub != undefined) {
masterdataChangerHub.client.updateProgress = function (progress) {
$rootScope.$broadcast('progressChanged', progress);
$rootScope.$apply();
}
masterdataChangerHub.client.completed = function (result) {
$rootScope.$broadcast('taskCompleted', result);
$rootScope.$apply();
}
}
If this works then the issue definitely a binding issue between SignalR and Angular. Depending on what browser plugins you have installed, having the console open could trigger a digest for you.
On the sample listeners for this project (that binds SignalR and Angular), you can see that a $rootScope.$apply() is needed after handling on the client side:
//client side methods
listeners:{
'lockEmployee': function (id) {
var employee = find(id);
employee.Locked = true;
$rootScope.$apply();
},
'unlockEmployee': function (id) {
var employee = find(id);
employee.Locked = false;
$rootScope.$apply();
}
}
So, I'd assume that you would need to do the same.

AngularJS $emit does not fire the event after added code to unregister

I just found out how to communicate between controllers using $broadcast and $emit, tried it in my POC and it worked, sort of, the original problem described in this other post is still not solved but now I have another question, the event is being registered multiple times so I am trying to unregister it the way I've seen it in multiple posts here on SO but now the event won't fire. The code is as follows:
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
$scope.$on('$destroy', function () {
unbind();
});
});
tabsApp.controller('SportsController', function SportsController($scope, $location, $rootScope) {
$scope.goToOverview = function (showModal) {
$location.path("overview/basic");
$rootScope.$emit('displayModal', { displayModal: showModal })
};
});
If I remove the
var unbind = ...
the event fires and I can see the alert. As soon as I add the code to unregister the event, the code is never fired. How can the two things work together?
Could you just pull out unbind into its own function, and use it in both like this?
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
};
$rootScope.$on('displayModal', unbind);
$scope.$on('$destroy', unbind);
});
I could be wrong but my guess would be that the BasicOverviewController isn't being persisted and it's scope is being destroyed before the SportsController gets a chance to utilize it. Without a working example, I can't deduce much more. If you want to maintain this on $rootScope then a possible pattern would be:
if (!$rootScope.displayModalDereg) {
$rootScope.displayModalDereg = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
This also allows you to check and see if there is an event registered so you can dereg it if needed.
if ($rootScope.displayModalDereg) {// this event has been registered
$rootScope.displayModalDereg();
$rootScope.dispalyModalDereg = undefined;
}
I would heavily suggested creating a displayModal directive that persists all of this instead of maintaining it on $rootScope. Obviously you would still $emit, or better yet, $broadcast from $rootScope, just not persist the dereg function there.
Here is an example of a modal directive I once wrote:
/**
*
* Modal Directive
*/
'use strict';
(function initModalDrtv(window) {
var angular = window.angular,
app = window.app;
angular.module(app.directives).directive('modalDrtv', [
'$rootScope',
function modalDrtv($rootScope) {
return {
restrict: 'A',
scope: {},
templateUrl: '/templates/modal.html',
replace: true,
compile: function modalCompileFn(tElement, tAttrs) {
return function modalLinkFn(scope, elem, attrs) {
scope.show = false;
scope.options = {
'title': '',
'message': '',
'markup': undefined,
'buttons': {
showCancel: false,
showSecondary: false,
secondaryAction: '',
primaryAction: 'Ok'
},
'responseName': ''
};
scope.respond = function(response) {
var r = '';
if (response === 1) {
r = scope.options.buttons.primaryAction;
} else if (response === 2) {
r = scope.options.buttons.secondaryAction;
} else {
r = response;
}
$rootScope.$broadcast(scope.options.responseName, r);
scope.show = false;
};
scope.$on('initIrpModal', function(event, data) {
if (angular.isUndefined(data)) throw new Error("Data missing from irp modal event");
scope.options.title = data.title;
scope.options.message = data.message;
scope.options.buttons.showCancel = data.buttons.showCancel;
scope.options.buttons.showSecondary = data.buttons.showSecondary;
scope.options.buttons.secondaryAction = data.buttons.secondaryAction;
scope.options.buttons.primaryAction = data.buttons.primaryAction;
scope.options.responseName = data.responseName;
scope.show = true;
});
}
}
}
}
]);
})(window);
This directive utilizes one modal and let's anything anywhere in the app utilize it. The registered event lives on its isolate scope and therefore is destroyed when the modal's scope is destroyed. It also is configured with a response name so that if a user response is needed it can broadcast an event, letting the portion of the app that initialized the modal hear the response.

How to cancel asynchronous process in javascript?

I have a one-window javascript application. I have a dashboard that displays certain images by loading via multiple get requests in the background.
Problem arises when not all get requests are finished on time and the context of the site changes because then I want to clear the dashboard. Yet if the get request havent't finished yet, they will populate the dashboard with the wrong images.
I am trying to think of a way to abort those get request. Can someone please direct me in the right direction?
var Dashboard = {
showAllAssets: function(){
var self = this;
this.resetDashboard();
$.get(this.urlForAllAssets, function(json){
self.loadAssets(json);
});
},
showAssetsForCategory: function(categoryId) {
...
},
getHtmlForAsset: function(id) {
var self = this;
$.get(this.urlForDashboardThumb + "/" + id.toString(), function(assetHtml){
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
// this gets inserted even when context changed, how can I prevent that?
var thumb = Object.create(Thumbnail);
thumb.init($asset);
}, 'html')
},
insertAssetThumbIntoDom: function($asset) {
$asset.appendTo(this.$el);
},
resetDashboard: function() {
this.$el.html("");
},
loadAssets: function(idList) {
var self = this;
var time = 200;
// These get requests will pile up in the background
$.each(idList, function(){
var asset = this;
setTimeout(function(){
self.getHtmlForAsset(asset.id);
}, time);
time += 200;
});
},
bind: function() {
$document.on('loadAssets', function(event, idList) {
self.loadAssets(idList);
});
$document.on('switched_to_category', function(event, categoryId) {
self.showAssetsForCategory(categoryId);
});
$document.on('show_all_assets', function(){
self.showAllAssets();
})
},
init: function($el) {
this.$el = $el;
this.resetDashboard();
this.bind();
}
}
Though you cant stop an already sent request, you can still solve your problem.
My solution is to generate a simple ID, a random set of numbers for example, and store somewhere in your dashboard, and send it along with the request and send it back with the image.
If a new context is generated, it will have a new ID.
If the image comes back with a different ID than the one in the current context, then discard it.
As pointed out by the comments, a possible solution is to store the current context and compare it within the success method on the get request.
I have changed my code insofar that now I'll store the current within the manager and also I pass the event around to the $.get-method.
This has the downside that the get requests are still processed though and the loading of the new context takes longer as those get requests are processed later if there are too many to process. I also dislike passing the event around.
var Dashboard = {
currentLoadEvent: null,
loadAssets: function(idList, event) {
var self = this;
$.each(idList, function(){
var asset = this;
self.getHtmlForAsset(asset.id, event);
});
},
getHtmlForAsset: function(id, event) {
var self = this;
$.get(this.urlForDashboardThumb + "/" + id.toString(), function(assetHtml){
if (event === self.currentLoadEvent) {
console.log('same event continuing');
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
var thumb = Object.create(Thumbnail);
thumb.init($asset);
} else {
console.log('context changed');
}
}, 'html')
},
bind: function() {
var self = this;
$document.on('loadAssets', function(event, idList) {
self.currentLoadEvent = event;
self.loadAssets(idList, event);
});
$document.on('switched_to_category', function(event, categoryId) {
self.currentLoadEvent = event;
self.showAssetsForCategory(categoryId, event);
});
$document.on('show_all_assets', function(event){
self.currentLoadEvent = event;
self.showAllAssets(event);
})
}
}
I created a different solution by storing the request in an array and aborting them when the context changed:
loadAssets: function(idList, event) {
var self = this;
var requests = [];
$.each(idList, function(){
var asset = this;
if (self.currentLoadEvent === event){
var request = $.get(self.urlForDashboardThumb + "/" + asset.id.toString(), function(assetHtml){
if (event === self.currentLoadEvent) {
var $asset = $(assetHtml);
self.insertAssetThumbIntoDom($asset);
var thumb = Object.create(Thumbnail);
thumb.init($asset);
console.log('completed get request');
} else {
console.log('context changed');
$.each(requests, function(){
this.abort();
console.log('aborted request');
})
}
}, 'html');
requests.push(request);
} else {
return false;
}
});
},

Categories

Resources