Users can edit their profile information. If they attempt to navigate away from the page while changes are present, the desired functionality should be that they are presented with a confirmation box. When I use Durandal's canDeactivate, it is only triggered when I try to navigate to another Durandal page. When I use window.onbeforeunload it is only triggered when I either hard refresh or type in a new URL etc.
Is there any universal solution (unified look and feel) that can catch both of these classes of events in order to prevent users from immediately navigating away from a page?
My two approaches are displayed below:
Durandal canDeactivate
canDeactivate: function () {
if ($("#saveButtonsBottom").css('visibility') === 'visible') {
var title = 'Warning';
var msg = 'Do you want to leave this page and lose all of your edits to this form?';
return app.showMessage(msg, title, ['Yes', 'No'])
.then(function (selectedOption) {
return selectedOption === 'Yes';
});
}
return false;
}
window.onbeforeunload
window.onbeforeunload = function() {
if ($("#saveButtonsBottom").css('visibility') === 'visible') {
var title = 'Warning';
var msg = 'Do you want to leave this page and lose all of your edits to this form?';
return app.showMessage(msg, title, ['Yes', 'No'])
.then(function (selectedOption) {
return selectedOption === 'Yes';
});
}
return true;
};
I have found in practice that you need both approaches to be sure of the desired behavior. window.onbeforeunload is considered by many to be a bad practice for web applications.
We finally abandoned this approach in our web application in favor of a Work in Progress pattern, where changes are saved (out to a back-end) every 3 seconds. That way, users can freely move from page to page without ever fearing the loss of their work. It does require adjusting one's data model, and the ability to turn off validation for works in progress. A Project document collection--or Projects table, depending on your approach to data--would have a corresponding ProjectDraft document collection or table.
But that's a topic of another discussion. In the meantime, if you have to take the approach you've given, why not encapsulate the logic in another require-able module? In other words:
var onNavigateOrShutdown = function () {
var title = 'Warning';
var msg = 'Do you want to leave this page and lose all of your edits to this form?';
return app.showMessage(msg, title, ['Yes', 'No'])
.then(function (selectedOption) {
return selectedOption === 'Yes';
});
}
and then
canDeactivate: function () {
if ($("#saveButtonsBottom").css('visibility') === 'visible') {
onNavigateOrShutdown();
return false;
}
and
window.onbeforeunload = function() {
if ($("#saveButtonsBottom").css('visibility') === 'visible') {
onNavigateOrShutdown();
}
return true;
};
Now, let's move this functionality into a new singleton module called, say, navigation.manager. Then, it's simply a matter of requiring the module wherever you need this logic. Of course, you can elaborate on navigation.manager and have it contain an evented hub that's capable of responding to messages and/or publishing them.
Related
Here's the problem. I'm making a callback to the server that receives an MVC partial page. It's been working great, it calls the success function and all that. However, I'm calling a function after which iterates through specific elements:
$(".tool-fields.in div.collapse, .common-fields div.collapse").each(...)
Inside this, I'm checking for a specific attribute (custom one using data-) which is also working great; however; the iterator never finishes. No error messages are given, the program doesn't hold up. It just quits.
Here's the function with the iterator
function HideShow() {
$(".tool-fields.in div.collapse, .common-fields div.collapse").each(function () {
if (IsDataYesNoHide(this)) {
$(this).collapse("show");
}
else
$(this).collapse("hide");
});
alert("test");
}
Here's the function called in that, "IsDataYesNoHide":
function IsDataYesNoHide(element) {
var $element = $(element);
var datayesnohide = $element.attr("data-yes-no-hide");
if (datayesnohide !== undefined) {
var array = datayesnohide.split(";");
var returnAnswer = true;
for (var i in array) {
var answer = array[i].split("=")[1];
returnAnswer = returnAnswer && (answer.toLowerCase() === "true");
}
return returnAnswer;
}
else {
return false;
}
}
This is the way the attribute appears
data-yes-no-hide="pKanban_Val=true;pTwoBoxSystem_Val=true;"
EDIT: Per request, here is the jquery $.post
$.post(path + conPath + '/GrabDetails', $.param({ data: dataArr }, true), function (data) {
ToggleLoader(false); //Page load finished so the spinner should stop
if (data !== "") { //if we got anything back of if there wasn't a ghost record
$container.find(".container").first().append(data); //add the content
var $changes = $("#Changes"); //grab the changes
var $details = $("#details"); //grab the current
SplitPage($container, $details, $changes); //Just CSS changes
MoveApproveReject($changes); //Moves buttons to the left of the screen
MarkAsDifferent($changes, $details) //Adds the data- attribute and colors differences
}
else {
$(".Details .modal-content").removeClass("extra-wide"); //Normal page
$(".Details input[type=radio]").each(function () {
CheckOptionalFields(this);
});
}
HideShow(); //Hide or show fields by business logic
});
For a while, I thought the jquery collapse was breaking, but putting the simple alert('test') showed me what was happening. It just was never finishing.
Are there specific lengths of time a callback function can be called from a jquery postback? I'm loading everything in modal views which would indicate "oh maybe jquery is included twice", but I've already had that problem for other things and have made sure that it only ever includes once. As in the include is only once in the entire app and the layout is only applied to the main page.
I'm open to any possibilities.
Thanks!
~Brandon
Found the problem. I had a variable that was sometimes being set as undefined cause it to silently crash. I have no idea why there was no error message.
Rewriting the question -
I am trying to make a page on which if user leave the page (either to other link/website or closing window/tab) I want to show the onbeforeunload handeler saying we have a great offer for you? and if user choose to leave the page it should do the normal propogation but if he choose to stay on the page I need him to redirect it to offer page redirection is important, no compromise. For testing lets redirect to google.com
I made a program as follows -
var stayonthis = true;
var a;
function load() {
window.onbeforeunload = function(e) {
if(stayonthis){
a = setTimeout('window.location.href="http://google.com";',100);
stayonthis = false;
return "Do you really want to leave now?";
}
else {
clearTimeout(a);
}
};
window.onunload = function(e) {
clearTimeout(a);
};
}
window.onload = load;
but the problem is that if he click on the link to yahoo.com and choose to leave the page he is not going to yahoo but to google instead :(
Help Me !! Thanks in Advance
here is the fiddle code
here how you can test because onbeforeunload does not work on iframe well
This solution works in all cases, using back browser button, setting new url in address bar or use links.
What i have found is that triggering onbeforeunload handler doesn't show the dialog attached to onbeforeunload handler.
In this case (when triggering is needed), use a confirm box to show the user message. This workaround is tested in chrome/firefox and IE (7 to 10)
http://jsfiddle.net/W3vUB/4/show
http://jsfiddle.net/W3vUB/4/
EDIT: set DEMO on codepen, apparently jsFiddle doesn't like this snippet(?!)
BTW, using bing.com due to google not allowing no more content being displayed inside iframe.
http://codepen.io/anon/pen/dYKKbZ
var a, b = false,
c = "http://bing.com";
function triggerEvent(el, type) {
if ((el[type] || false) && typeof el[type] == 'function') {
el[type](el);
}
}
$(function () {
$('a:not([href^=#])').on('click', function (e) {
e.preventDefault();
if (confirm("Do you really want to leave now?")) c = this.href;
triggerEvent(window, 'onbeforeunload');
});
});
window.onbeforeunload = function (e) {
if (b) return;
a = setTimeout(function () {
b = true;
window.location.href = c;
c = "http://bing.com";
console.log(c);
}, 500);
return "Do you really want to leave now?";
}
window.onunload = function () {
clearTimeout(a);
}
It's better to Check it local.
Check out the comments and try this: LIVE DEMO
var linkClick=false;
document.onclick = function(e)
{
linkClick = true;
var elemntTagName = e.target.tagName;
if(elemntTagName=='A')
{
e.target.getAttribute("href");
if(!confirm('Are your sure you want to leave?'))
{
window.location.href = "http://google.com";
console.log("http://google.com");
}
else
{
window.location.href = e.target.getAttribute("href");
console.log(e.target.getAttribute("href"));
}
return false;
}
}
function OnBeforeUnLoad ()
{
return "Are you sure?";
linkClick=false;
window.location.href = "http://google.com";
console.log("http://google.com");
}
And change your html code to this:
<body onbeforeunload="if(linkClick == false) {return OnBeforeUnLoad()}">
try it
</body>
After playing a while with this problem I did the following. It seems to work but it's not very reliable. The biggest issue is that the timed out function needs to bridge a large enough timespan for the browser to make a connection to the url in the link's href attribute.
jsfiddle to demonstrate. I used bing.com instead of google.com because of X-Frame-Options: SAMEORIGIN
var F = function(){}; // empty function
var offerUrl = 'http://bing.com';
var url;
var handler = function(e) {
timeout = setTimeout(function () {
console.log('location.assign');
location.assign(offerUrl);
/*
* This value makes or breaks it.
* You need enough time so the browser can make the connection to
* the clicked links href else it will still redirect to the offer url.
*/
}, 1400);
// important!
window.onbeforeunload = F;
console.info('handler');
return 'Do you wan\'t to leave now?';
};
window.onbeforeunload = handler;
Try the following, (adds a global function that checks the state all the time though).
var redirected=false;
$(window).bind('beforeunload', function(e){
if(redirected)
return;
var orgLoc=window.location.href;
$(window).bind('focus.unloadev',function(e){
if(redirected==true)
return;
$(window).unbind('focus.unloadev');
window.setTimeout(function(){
if(window.location.href!=orgLoc)
return;
console.log('redirect...');
window.location.replace('http://google.com');
},6000);
redirected=true;
});
console.log('before2');
return "okdoky2";
});
$(window).unload(function(e){console.log('unloading...');redirected=true;});
<script>
function endSession() {
// Browser or Broswer tab is closed
// Write code here
alert('Browser or Broswer tab closed');
}
</script>
<body onpagehide="endSession();">
I think you're confused about the progress of events, on before unload the page is still interacting, the return method is like a shortcut for return "confirm()", the return of the confirm however cannot be handled at all, so you can not really investigate the response of the user and decide upon it which way to go, the response is going to be immediately carried out as "yes" leave page, or "no" don't leave page...
Notice that you have already changed the source of the url to Google before you prompt user, this action, cannot be undone... unless maybe, you can setimeout to something like 5 seconds (but then if the user isn't quick enough it won't pick up his answer)
Edit: I've just made it a 5000 time lapse and it always goes to Yahoo! Never picks up the google change at all.
now; I'm making a directive to add/remove an article as a favourite (a user has a list of favourite articles), it displays a full/empty heart icon depending if the article is already a favourite or not, and when the user clicks on it, it favorite/unfavourite the article (and changes it's behavior to undo the fav action).
function favouriteWidgetDirective($rootScope, articles) {
return {
scope: {
'article': '='
},
restrict: 'A',
template: '<button class="favourite" >' +
'<i class="icon-heart-{{ favClass }}" ng-click="favFunction(article.id)"></i>' +
'</button>',
link: function postLink($scope, element, attrs) {
//Check if an article is already a favorite, uses the favourites array of the logged user
var isFavourite = function(id) {
var favourite = false;
$rootScope.currentUser.favourites.some(function (fav, idx) {
if (fav.article === id) {
favourite = true;
return true;
}
});
return favourite;
};
var setFavourite = function(id) {
event.stopPropagation();
//API call
articles.favourite(id);
//Update user favourites array
$rootScope.currentUser.favourites.push({article: id});
//Some magic here to update the icon and the click binding??
//....
};
var unFavourite = function(id) {
//Very similar to setFavourite.., but the opposite
};
var articleIsFavourite = isFavourite($scope.article.id);
//The style for the button (an empty or full heart)
$scope.favClass = articleIsFavourite ? 'full' : 'empty';
//The function that will be used on the click
$scope.favFunction = articleIsFavourite ? unFavourite : setFavourite;
}
};
}
Now the problem (that for the trained eye must be obvious at this point), the API gets called and the fav added, but the UI is not updating itself with the new class or the new function.
I've read that Angular is great but not magic, and I need to add an $apply or a $watch, but since I don't master these techniques I havn't been able to get the desired behavior.
The current aproach is very open to changes (since as a begginer with directives this could be really wrong, I know), so if it has to be throwed away and rebuilt just let me know (kindly :), adding suggestions).
Thank you.
I have made a solution for my website which includes using ajax to present the general information on the website. In doing this, I am changing the URL every time a user loads some specific content with the window.history.pushState method. However, when I press backspace or press back, the content of the old url is not loaded (however the URL is loaded).
I have tried several solutions presented on SO without any luck.
Here is an example of one of the ajax functions:
$(document).ready(function(){
$(document).on("click",".priceDeckLink",function(){
$("#hideGraphStuff").hide();
$("#giantWrapper").show();
$("#loadDeck").fadeIn("fast");
var name = $(this).text();
$.post("pages/getPriceDeckData.php",{data : name},function(data){
var $response=$(data);
var name = $response.filter('#titleDeck').text();
var data = data.split("%%%%%%%");
$("#deckInfo").html(data[0]);
$("#textContainer").html(data[1]);
$("#realTitleDeck").html(name);
$("#loadDeck").hide();
$("#hideGraphStuff").fadeIn("fast");
loadGraph();
window.history.pushState("Price Deck", "Price Deck", "?p=priceDeck&dN="+ name);
});
});
Hope you guys can help :)
pushState alone will not make your page function with back/forward. What you'd need to do is listen to onpopstate and load the contents yourself similar to what would happen on click.
var load = function (name, skipPushState) {
$("#hideGraphStuff").hide();
// pre-load, etc ...
$.post("pages/getPriceDeckData.php",{data : name}, function(data){
// on-load, etc ...
// we don't want to push the state on popstate (e.g. 'Back'), so `skipPushState`
// can be passed to prevent it
if (!skipPushState) {
// build a state for this name
var state = {name: name, page: 'Price Deck'};
window.history.pushState(state, "Price Deck", "?p=priceDeck&dN="+ name);
}
});
}
$(document).on("click", ".priceDeckLink", function() {
var name = $(this).text();
load(name);
});
$(window).on("popstate", function () {
// if the state is the page you expect, pull the name and load it.
if (history.state && "Price Deck" === history.state.page) {
load(history.state.name, true);
}
});
Note that history.state is a somewhat less supported part of the history API. If you wanted to support all pushState browsers you'd have to have another way to pull the current state on popstate, probably by parsing the URL.
It would be trivial and probably a good idea here to cache the results of the priceCheck for the name as well and pull them from the cache on back/forward instead of making more php requests.
This works for me. Very simple.
$(window).bind("popstate", function() {
window.location = location.href
});
Have same issue and the solution not working for neither
const [loadBackBtn, setLoadBackBtn] = useState(false);
useEffect(() => {
if (loadBackBtn) {
setLoadBackBtn(false);
return;
} else {
const stateQuery = router.query;
const { asPath } = router;
window.history.pushState(stateQuery, "", asPath);
},[router.query?.page]
I'm in a real bottleneck with backbone.
I'm new to it, so sorry if my questios are stupid, as i probably didn't get the point of the system's structure.
Basically, I'm creating ad application which lets you do some things for different "steps". Therefore, I've implemented some kind of pagination system. Each time a page sasisfies certain conditions, the next page link is shown, and the current page is cached.
Each page uses the same "page" object model/view, and the navigation is appended there each time. it's only registered one time anyway, and I undelegate/re-delegate events as the old page fades out and the new one fades in.
If I always use cached versions for previous pages, everything is okay. BUT, if I re-render a page that was already rendered, when I click "go next page", it skips ahead of how many times i re-rendered the page itself.
it's like the "go next page" button has been registered, say, 3 times, and was never removed from the events listener.
It's a very long application in terms of code, and i hope you can understand the basica idea, and give me some hints, without needing to have the full code here.
Thanks in advance, i hope somebody can help me out since i'm in a real bottleneck!
p.s. for some reason, I've noticed that the next/previous buttons respective html is not cached within the page. Weird.
---UPDATE----
I tried the stopListening suggestion, but it didn't work. Here is jmy troublesome button:
// Register the next button
App.Views.NavNext = Backbone.View.extend({
el: $('#nav-next'),
initialize: function() {
vent.on('showNext', function() {
this.$el.fadeIn();
}, this)
},
events: {
'click': 'checkConditions'
},
checkConditions: function() {
//TODO this is running 2 times too!
console.log('checking conditions');
if (current_step == 1)
this.go_next_step();
vent.trigger('checkConditions', current_step); // will trigger cart conditions too
},
go_next_step: function() {
if(typeof(mainView.stepView) != 'undefined')
{
mainView.stepView.unregisterNavigation();
}
mainView.$el.fadeOut('normal', function(){
$(this).html('');
current_step++;
mainView.renderStep(current_step);
}); //fadeout and empty, then refill
}
});
Basically, checkConditions runs 2 times as if the previousle rendered click is still registered. Here is where it's being registered, and then unregistered after the current step fades off (just a part of that view!):
render: function() {
var step = this;
//print the title for this step
this.$el.attr('id', 'step_' + current_step);
this.$el.html('<h3>'+this.model.get('description')+'</h3>');
// switch display based on the step number, will load all necessary data
// this.navPrev = new App.Views.NavPrev();
// this.navNext = new App.Views.NavNext();
this.$el.addClass('grid_7 omega');
// add cart (only if not already there)
if (!mainView.cart)
{
mainView.cart = new App.Models.Cart;
mainView.cartView = new App.Views.Cart({model: mainView.cart})
mainView.$el.before(mainView.cartView.render().$el)
}
switch (this.model.get('n'))
{
case 5: // Product list, fetch and display based on the provious options
// _.each(mainView.step_values, function(option){
// console.log(option)
// }, this);
var products = new App.Collections.Products;
products.fetch({data:mainView.step_values, type:'POST'}).complete(function() {
if (products.length == 0)
{
step.$el.append('<p>'+errorMsgs['noprod']+'</p>')
}
else {
step.contentView = new App.Views.Products({collection: products});
step.$el.append(step.contentView.render().$el);
}
step.appendNavigation();
});
break;
}
//console.log(this.el)
return this;
},
appendNavigation: function(back) {
if(current_step != 2)
this.$el.append(navPrev.$el.show());
else this.$el.append(navPrev.$el.hide());
this.$el.append(navNext.$el.hide());
if(back) navNext.$el.show();
navPrev.delegateEvents(); // re-assign all events
navNext.delegateEvents();
},
unregisterNavigation: function() {
navNext.stopListening(); // re-assign all events
}
And finally, here is the main view's renderStep, called after pressing "next" it will load a cached version if present, but for the trouble page, I'm not creating it
renderStep : function(i, previous) { // i will be the current step number
if(i == 1)
return this;
if(this.cached_step[i] && previous) // TODO do not render if going back
{ // we have this step already cached
this.stepView = this.cached_step[i];
console.log('ciao'+current_step)
this.stepView.appendNavigation(true);
if ( current_step == 3)
{
_.each(this.stepView.contentView.productViews, function(pview){
pview.delegateEvents(); //rebind all product clicks
})
}
this.$el.html(this.stepView.$el).fadeIn();
} else {
var step = new App.Models.Step({description: steps[i-1], n: i});
this.stepView = new App.Views.Step({model: step})
this.$el.html(this.stepView.render().$el).fadeIn(); // refill the content with a new step
mainView.cached_step[current_step] = mainView.stepView; // was in go_next_step, TODO check appendnavigation, then re-render go next step
}
return this;
}
Try using listenTo and stopListening when you are showing or removing a certain view from the screen.
Take a look at docs: http://backbonejs.org/#Events-listenTo
All events are binded on initialization of the view and when you are removing the view from the screen then unbind all events.
Read this for detailed analysis: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/