I've got this listener setup on a form of mine that checks for a state transition to occur via angular router. When the listener is tripped it checks if the form is dirty, if it is it throws a window.confirm alert up saying the user may have unsaved changes.
All of that looks like this
this.setListener('form.dirty-check', this.setExitCheck);
setListener = (el, cb) => {
if ($(el).length) {
cb();
} else {
setTimeout(() => {
this.setListener(el, cb);
}, 500);
}
};
setExitCheck = () => {
this.$transitions.onStart({}, () => {
if ($('#compForm').hasClass('ng-dirty')) {
if (window.confirm('You may have unsaved changes! Press ok to continue, or press cancel to go back and save your work.') === false) {
return false;
} else {
return true;
}
}
});
};
This code is working pretty well, except for a singular bit of unexpected behaviour.
For some reason, when I hit, "Ok" to leave the page the transition will fire off just fine, but if I go back to the page and try it again, I now have to hit okay twice, and get two window.confirm alerts. If I go back a third time to try, I get three window.confirm alerts, and have to hit Ok on all three of them. I tried this up to the point of receiving 10 alerts, and have to press ok 10 times.
Once I refresh the page though, it seems to reset, and I start it all over again. Works right away, then takes two Ok's, then three, and so on.
Does anyone know what might be going on causing this incremental behaviour?
ui-router won't clear listeners automatically, so you have to clear it manually.
and $transitions.onStart returns a function which will destroy the listener's hook when it's called. documentation here(the last line).
the syntax is the same as deregister events of $rootScope, refer How can I unregister a broadcast event to rootscope in AngularJS?
$scope.onStartHandler = this.$transitions.onStart(...);
$scope.$on('destroy', function() {
$scope.onStartHandler();
});
Related
I have a very simple code:
window.onbeforeunload = function(e) {
console.log("closing");
e.preventDefault();
try { self.recorder.stop(); }
catch (err) { console.log(err); }
return "custom message here";
}
What I need is to run the recorder.stop() only if the user presses yes, and actually unloads the page.
What I tried:
onpagehide
I found out that onpagehide actually runs console.log("closing") but not recorder.stop() for some reason. Possibly because it runs in the "terminated" state, and no async tasks can run in the terminated state.
onbeforeunload
The popup shows, and the code runs, however the code also runs if the user presses "cancel", and decides not to unload the page.
onunload
The code simply does not run.
I have a console.log() inside my recorder.stop() function, and tried it with the "keep logs" functionality from the inspector.
onvisibilitychange
This one looked promising, but I can't find the right state for it.
document.visibilityState === 'hidden'
Runs anytime I switch to another window, even without closing the previous one.
document.visibilityState === 'terminated'
Most probably won't work, for the same reason as onpagehide()
document.visibilityState === 'frozen'
Doesn't work, probably because the page does not freeze before terminating.
I'm writing tests for a React component. It's a timer that starts counting down when you press a button, then stops when you press the same button. I have a test case that tries to press the pause button, wait a second, then press the pause button again, checking the timer to make sure that a second has elapsed:
Timer.test.js
render(<Timer />)
const pauseButton = screen.getByText('pause')
const timerOutput = screen.getAllByRole('heading')[1]
describe('Timer', () => {
test('Timer starts counting down when unpaused', done => {
function fetchTime(callback) {
fireEvent.click(pauseButton)
setTimeout(
fireEvent.click(pauseButton),
1250
)
return callback(timerOutput)
}
function callback(data) {
try {
expect(data).toHaveTextContent('24:59')
done()
} catch(error) {
done(error)
}
}
fetchTime(callback)
})
})
The problem is, the test doesn't seem to be hitting click on pauseButton the way I want it to. Jest tells me in the terminal when I run my test that timerOutput turns out to be '25:00' rather than '24:59', and it seems as if the component failed the test. But this is a problem with the test, not the component; when I run the app in my browser and press the button myself, it works the way it should. How do I get this test to work properly, and hit the buttons the way I want it to?
It's difficult to have an accurate answer with few information about the component itself.
First I would recommend use async arrow function inside test() whenever you need to handle async calls so you don't rely in callback hell.
Besides that, I would try to use jest.useFakeTimers() so you can advance the setTimeout timer in order to test properly. It seems that your second fireEvent.click never gets fired since the test checks it synchronously.
And I just noticed you requested the timerOutput at first but didn't request it after the click events.
I would suggest something like:
test('Timer starts counting down when unpaused', async () => {
jest.useFakeTimers();
fireEvent.click(pauseButton)
setTimeout(
() => {fireEvent.click(pauseButton)},
1250
)
jest.runPendingTimers(); // This would run the above function
expect(screen.getAllByRole('heading')[1]).toHaveTextContent('24:59')
}
})
Indeed, the expect statement would be better from a user perspective assertion, like:
expect(screen.getByText("24:59")).toBeVisible();
Since you don't matter about the HTML elements that contains that text content
Following the answer here, I have the code below:
.directive('confirmOnExit', function($window) {
return {
scope: {},
link: function(scope) {
var message = "Changes you made may not be saved.";
$window.onbeforeunload = function(){
return message;
};
scope.$on('$locationChangeStart', function(event) {
if(!$window.confirm("Do you want to leave this page? "+message))
event.preventDefault();
});
scope.$on('$destroy',function(){
$window.onbeforeunload = null;
});
}
};
})
On Chrome, everything is fine. However, on Firefox, almost every time I click the button of the confirm dialog, the error occurs:
Error: [$rootScope:inprog] $digest already in progress
The solutions I found online mostly suggest to use $timeout. However, event.preventDefault() inside a $timeout function seems not preventing URL change. What should I do?
I just ran into this same problem. For me, calling confirm causes an error in firefox and IE. To get around it, I prevent default immediately if there's a message to show, and run the confirm in a timeout. Then, if the user clicks "leave page", I clear the onbeforeunload and use the $location service to set the url again. If your app is a single page app, $locationChangeStart will be called on the first page load, so you'll want to add in a flag at the top like: if (!hasLoaded) { hasLoaded = true; return; }
$rootScope.$on('$locationChangeStart', function (e, newUrl, oldUrl) {
// Call the function and show the confirm dialogue if necessary
if ($window.onbeforeunload) {
let message = $window.onbeforeunload();
if (message) {
// Since we're going to show a message, cancel navigation
e.preventDefault();
$timeout(() => {
if (confirm(message)) {
// User chose not to stay. Unregister the function so that it doesn't fire again.
$window.onbeforeunload = undefined;
// Run the url again. We've cleared onbeforeunload, so this won't create a loop
$location.url(newUrl.replace($window.location.protocol + '//' + $window.location.host, ''));
}
});
}
}
})
I have one beforeunload handler, that is called when the user actually navigates away from the page:
$(window).on("beforeunload", function () {
cleanup();
});
Another part of my application might however add another beforeunload handler, that asks the user if he really wants to leave because of an ongoing operation:
$(window).on("beforeunload", function () {
return "Do you really want to leave";
});
How can I assure, that the second handler will always be called first and that the first handler will not be called in case the user decides to stay on the page?
I already tried to use the unload event instead. But this doesn't work since it will not execute my clean-up function and the backend call within that function reliably.
You can use window.confirm in case. As in (pseudo-code)
$(window).on("beforeunload", function () {
var result = window.confirm('Do you really want to quit?');
if (result) {
cleanup();
}
}
This way, if the user wants to quit it will cleanup. Does that work?>
I want to do some stuff when user is leaving a page, I add this code
window.onbeforunload = function (e){
return "You save some unsaved data, Do you want to leave?";
}
This prompt can notify the user and user can stay on the page or leave. But I want more to know whether he leaves or not, and do thing on his decision. I tried this,
window.onbeforunload = function (e){
var event = jQuery.Event(e);
var result = confirm('want to leave?');
if (result == false){
//do sth..
event.preventDefault();
}else{
//do clean up
}
}
But it fails!! It always goes away!
Can any body help me doing this?
The method you use (preventing bubbling of the event) is intentionally not possible, otherwise you could prevent users from leaving your page.
You can achieve something similar to what you want by doing your cleanup onunload, and do the stuff you always want to do onbeforeunload.
But I want more to know whether he leaves or not, and do thing on his decision
If you wanna do something when he leaves, you can do it in unload event. For example, as #Erik Bakker mentioned you can send async events in unload event.
However if you wanna find out if user "stayed", in other words cancelled the leaving process, there is a way as well. It's kinda hackish, but it works.
const doSomethingWhenUserStays = function doSomethingWhenUserStays() {
alert('user stayed!!!');
}
window.addEventListener('beforeunload', function onBeforeUnload(e) {
setTimeout(doSomethingWhenUserStays, 500);
// Dialog text doesn't really work in Chrome.
const dialogText = 'A dialog text when leaving the page';
e.returnValue = dialogText;
return dialogText;
});
Method doSomethingWhenUserStays will be called every time, but if user leaves the page, he won't see what it performed anyway. It can perform asynchronous stuff, synchronous, it doesn't really matter because it's within setTimeout therefore it's out of the normal flow of onBeforeUnload and won't interfere with it.
If you want to perform it ONLY if user really stays on the page it's slightly harder. You'd have to set a global flag that checks whether user reached unload or not and only then call what's inside doSomethingWhenUserStays. Consider the following example.
let hasUserLeft = false;
const doSomethingWhenUserStays = function doSomethingWhenUserStays() {
// Perform the following only if user hasn't left the page
if (!hasUserLeft) {
alert('user stayed!!!');
}
}
window.addEventListener('beforeunload', function onBeforeUnload(e) {
// It won't perform doSomethingWhenUserStays in 500ms right after this is called,
// but instead, it will perform it in 500ms after you click "Stay" or "Leave".
// Therefore, there should be some time for `unload` handler to fire and
// set `hasUserLeft` flag before `doSomethingWhenUserStays` is called.
setTimeout(doSomethingWhenUserStays, 500);
// Dialog text doesn't really work in Chrome.
const dialogText = 'A dialog text when leaving the page';
e.returnValue = dialogText;
return dialogText;
});
window.addEventListener('unload', function onUnload() {
hasUserLeft = true;
});
As far as I have read about this method in different browser forums like MSDN, MozillaDev, etc, this method does not have any callbacks for OK/Cancel. You have this for the confirm dialog but not for this.
This is a security implementation to allow users to have full right about which website they should see. Also, it averts hackers from locking users to their sites.