window.onunload event wont fire inside AngularJS controller - javascript

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()

Related

Rate limiting calls to a function without an event listener (throttle)

I have a long infinite scroll page full of videos that I am refreshing a sticky ad on. On each scroll to a new video the URL is updated. Each URL update fires a callback that calls the refresh function. I would like to rate limit how fast the function can be fired so that refresh calls don't happen too fast if the user scrolls too quickly.
I have been able to get throttle working in a test environment while using an event listener for a button click instead of the URL change callback, but have been unable to find a way to make it work without an event listener.
Here's the base code, as you can see I need to rate-limit how fast refreshFirstSlot is called.
// Function which refreshes the first slot
var refreshFirstSlot = function () {
googletag.cmd.push(function () {
googletag.pubads().refresh([adSlot1]);
});
};
// UrlUpdate is called each time the URL updates
UrlUpdate = function (url, type) {
refreshFirstSlot();
};
// throttle code
const throttle = (callback, delay) => {
let throttleTimeout = null;
let storedEvent = null;
const throttledEventHandler = (event) => {
storedEvent = event;
const shouldHandleEvent = !throttleTimeout;
if (shouldHandleEvent) {
callback(storedEvent);
storedEvent = null;
throttleTimeout = setTimeout(() => {
throttleTimeout = null;
if (storedEvent) {
throttledEventHandler(storedEvent);
}
}, delay);
}
};
return throttledEventHandler;
};
// adding the refresh call
var returnedFunction = throttle(function () {
refreshFirstSlot();
}, 5000);
// final call
UrlUpdate = function (url, type) {
returnedFunction();
};
Where am I going wrong here?
You could do something like this :
load your page => call your ad
scroll your page => refresh your ad unless previous call has not been rendered / ended
and so on...
To do so, you can use Google Publisher Tag's events (see here). Here is a simple example :
var refreshReady = false;
//slotRequested
googletag.pubads().addEventListener('slotRequested', function(event) {
var slotId = event.slot.getSlotElementId();
if(slotId === adSlot1.getSlotElementId()) {
//every time adSlot1 is requested, we disable the refresh variable
refreshReady = false;
}
});
//slotRenderEnded
googletag.pubads().addEventListener('slotRenderEnded', function(event) {
var slotId = event.slot.getSlotElementId();
if(slotId === adSlot1.getSlotElementId()) {
//every time adSlot1 has been rendered, we enable the refresh variable
refreshReady = true;
}
});
var refreshFirstSlot = function () {
if(refreshReady) {
googletag.cmd.push(function () {
googletag.pubads().refresh([adSlot1]);
});
}
else {
console.log('not yet !');
}
};
UrlUpdate = function (url, type) {
refreshFirstSlot();
};
You could add a timeout to make sure the ad is not refreshed as soon as rendered (improve viewability)

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

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.

$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.

AngularJS $scope.$on multiple events

I have a scenario when I need to listen to 2 different events transmitted via $scope.$emit and I want to act only when both have happened.
So for example if the events triggering is the following:
$scope.$emit('first');
// do nothing
$scope.$emit('second');
// execute something
$scope.$emit('first');
// Do nothing
$scope.$emit('first');
// Do nothing
$scope.$emit('second');
// execute something
Is there anything that does it out of the box? Ideally like
$scope.$on('first', 'second', function() {});
I've considered doing the following:
var triggeredEvents = [];
$scope.$on('first', function() {
notifyEventTriggered('first');
});
$scope.$on('second', function() {
notifyEventTriggered('second');
});
function notifyEventTriggered(event) {
if (triggeredEvents.indexOf(event) == -1) {
triggeredEvents.push(event);
}
if (triggeredEvents.length > 1) {
execute();
triggeredEvents.length = 0;
}
}
So is there something that does it simpler? Or some suggestions on how to improve it? Other than creating a service for this.
Thanks
There's no built in way to do it. I would personally attach a new method to $rootScope like this:
https://jsfiddle.net/qv6m2drz/5/
var app = angular.module('jsbin', []);
app.run(['$rootScope', function($rootScope) {
$rootScope.onAllEvents = (function() {
var watchedEvents = {};
return onAllEvents;
/*
* Attach listeners to all events
*/
function onAllEvents(events, fn){
if(!events || !events.length) return ;
events.forEach(function(evt){
watchedEvents[evt] = false;
this.$on(evt, function(){
watchedEvents[evt] = true;
tryExecute(fn);
});
}.bind(this));
}
/*
* Check if all `watchedEvents` have fired. If yes, run fn() and reset the events
*/
function tryExecute(fn){
var shouldExecute = true;
for(evt in watchedEvents){
if(watchedEvents.hasOwnProperty(evt) && !watchedEvents[evt]){
shouldExecute = false;
}
}
if(shouldExecute){
fn();
for(evt in watchedEvents){
if(watchedEvents.hasOwnProperty(evt)){
watchedEvents[evt] = false;
}
}
}
}
})();
}]);

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.

Categories

Resources