468

I have some pages with forms in my application.

How can I secure the form in such a way that if someone navigates away or closes the browser tab, they should be prompted to to confirm they really want to leave the form with unsaved data?

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
Khan
  • 5,052
  • 5
  • 22
  • 23

19 Answers19

702

Short, wrong answer:

You can do this by handling the beforeunload event and returning a non-null string:

window.addEventListener("beforeunload", function (e) {
    var confirmationMessage = 'It looks like you have been editing something. '
                            + 'If you leave before saving, your changes will be lost.';

    (e || window.event).returnValue = confirmationMessage; //Gecko + IE
    return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});

The problem with this approach is that submitting a form is also firing the unload event. This is fixed easily by adding the a flag that you're submitting a form:

var formSubmitting = false;
var setFormSubmitting = function() { formSubmitting = true; };

window.onload = function() {
    window.addEventListener("beforeunload", function (e) {
        if (formSubmitting) {
            return undefined;
        }

        var confirmationMessage = 'It looks like you have been editing something. '
                                + 'If you leave before saving, your changes will be lost.';
        
        (e || window.event).returnValue = confirmationMessage; //Gecko + IE
        return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
    });
};

Then calling the setter when submitting:

<form method="post" onsubmit="setFormSubmitting()">     
    <input type="submit" />
</form>

But read on...

Long, correct answer:

You also don't want to show this message when the user hasn't changed anything on your forms. One solution is to use the beforeunload event in combination with a "dirty" flag, which only triggers the prompt if it's really relevant.

var isDirty = function() { return false; }

window.onload = function() {
    window.addEventListener("beforeunload", function (e) {
        if (formSubmitting || !isDirty()) {
            return undefined;
        }
        
        var confirmationMessage = 'It looks like you have been editing something. '
                                + 'If you leave before saving, your changes will be lost.';

        (e || window.event).returnValue = confirmationMessage; //Gecko + IE
        return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
    });
};
    

Now to implement the isDirty method, there are various approaches.

You can use jQuery and form serialization, but this approach has some flaws. First you have to alter the code to work on any form ($("form").each() will do), but the greatest problem is that jQuery's serialize() will only work on named, non-disabled elements, so changing any disabled or unnamed element will not trigger the dirty flag. There are workarounds for that, like making controls readonly instead of enabling, serializing and then disabling the controls again.

So events seem the way to go. You can try listening for keypresses. This event has a few issues:

  • Won't trigger on checkboxes, radio buttons, or other elements that are being altered through mouse input.
  • Will trigger for irrelevant keypresses like the Ctrl key.
  • Won't trigger on values set through JavaScript code.
  • Won't trigger on cutting or pasting text through context menus.
  • Won't work for virtual inputs like datepickers or checkbox/radiobutton beautifiers which save their value in a hidden input through JavaScript.

The change event also doesn't trigger on values set from JavaScript code, so also won't work for virtual inputs.

Binding the input event to all inputs (and textareas and selects) on your page won't work on older browsers and, like all event handling solutions mentioned above, doesn't support undo. When a user changes a textbox and then undoes that, or checks and unchecks a checkbox, the form is still considered dirty.

And when you want to implement more behavior, like ignoring certain elements, you'll have even more work to do.

Don't reinvent the wheel:

So before you think about implementing those solutions and all required workarounds, realize you're reinventing the wheel and you're prone to running into problems others have already solved for you.

If your application already uses jQuery, you may as well use tested, maintained code instead of rolling your own, and use a third-party library for all of this.

jquery.dirty (suggested by @troseman in the comments) provides functions for properly detecting whether a form has been changed or not, and preventing the user from leaving the page while displaying a prompt. It also has other useful functions like resetting the form, and setting the current state of the form as the "clean" state. Example usage:

$("#myForm").dirty({preventLeaving: true});

An older, currently abandoned project, is jQuery's Are You Sure? plugin, which also works great; see their demo page. Example usage:

<script src="jquery.are-you-sure.js"></script>

<script>
  $(function() {
    $('#myForm').areYouSure(
      {
        message: 'It looks like you have been editing something. '
               + 'If you leave before saving, your changes will be lost.'
      }
    );
  });
  
</script>

Custom messages not supported everywhere

Do note that since 2011 already, Firefox 4 didn't support custom messages in this dialog. As of april 2016, Chrome 51 is being rolled out in which custom messages are also being removed.

Some alternatives exist elsewhere on this site, but I think a dialog like this is clear enough:

Do you want to leave this site?

Changes you made may not be saved.

Leave Stay

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
  • 14
    Note that using custom strings doesn't work anymore in Chrome; https://www.chromestatus.com/feature/5349061406228480 – amann May 04 '17 at 12:58
  • 8
    Yes, that's described in the last section of my answer. – CodeCaster May 04 '17 at 13:00
  • @CodeCaster, I got are-you-sure running in my angular app and in the event that I attempt to close the window, or navigate to an entirely different domain, I get an alert as expected. If I navigate to a different page from within my application it doesn't recognize the state change, and will not issue the alert. It just goes to whatever page I've set that button to take me to. Do you have any idea what the issue might be? – Chris May 04 '17 at 18:04
  • 2
    @Chris that'll be because Angular is manipulating the DOM to change the page, while not navigating away from the current document. – CodeCaster May 04 '17 at 18:07
  • 28
    Be warned jQuery areYouSure has been abandoned since 2014 (57 open issues on [Github](https://github.com/codedance/jquery.AreYouSure/issues)) , it's full of bugs from my experience, it may seem like it works at first but after some quick tests I realised it doesn't always work. Would Not recommend this at all – Mark Jan 10 '18 at 21:38
  • @Mark Is there no alternative to jQuery Are You Sure that is currently maintained? If not, the solution is to write our own Javascript then? – JacopoStanchi Jul 05 '18 at 08:40
  • 6
    @JacopoStanchi I unfortunately didn't find any. And I mean I looked quite a lot - but this was back in Jan 10 so someone may have written something but I doubt it. Your best bet is to implement it yourself - it's not what you want to hear tho so sorry. ( But at least you won't get any surprises like I did using jQuery areYouSure.) Plus who knows maybe you could even make it open source and your implementation would be shared with others ;) – Mark Jul 06 '18 at 14:38
  • 1
    this one looks maintained, https://github.com/simon-reynolds/jquery.dirty – troseman Jun 08 '21 at 15:59
  • logged in to SO just to upvote this answer, thanks! – grapes May 12 '22 at 14:39
80

Check out the JavaScript onbeforeunload event. It's non-standard JavaScript introduced by Microsoft, however it works in most browsers and their onbeforeunload documentation has more information and examples.

Paul Annesley
  • 3,387
  • 22
  • 23
  • 4
    I think it is become standards now. >> The event was originally introduced by Microsoft in Internet Explorer 4 and standardized in the HTML5 specification. – vee Jul 09 '18 at 16:06
42

Universal solution requiring no configuration that automatically detects all input modification, including contenteditable elements:

"use strict";
(() => {
const modified_inputs = new Set;
const defaultValue = "defaultValue";
// store default values
addEventListener("beforeinput", (evt) => {
    const target = evt.target;
    if (!(defaultValue in target || defaultValue in target.dataset)) {
        target.dataset[defaultValue] = ("" + (target.value || target.textContent)).trim();
    }
});
// detect input modifications
addEventListener("input", (evt) => {
    const target = evt.target;
    let original;
    if (defaultValue in target) {
        original = target[defaultValue];
    } else {
        original = target.dataset[defaultValue];
    }
    if (original !== ("" + (target.value || target.textContent)).trim()) {
        if (!modified_inputs.has(target)) {
            modified_inputs.add(target);
        }
    } else if (modified_inputs.has(target)) {
        modified_inputs.delete(target);
    }
});
// clear modified inputs upon form submission
addEventListener("submit", (evt) => {
    modified_inputs.clear();
    // to prevent the warning from happening, it is advisable
    // that you clear your form controls back to their default
    // state with evt.target.reset() or form.reset() after submission
});
// warn before closing if any inputs are modified
addEventListener("beforeunload", (evt) => {
    if (modified_inputs.size) {
        const unsaved_changes_warning = "Changes you made may not be saved.";
        evt.returnValue = unsaved_changes_warning;
        return unsaved_changes_warning;
    }
});
})();
Eli Grey
  • 35,104
  • 14
  • 75
  • 93
  • 3
    Cool beans. Just copy paste this code into your test page, make a change and hit refresh. It will display a native browser alert warning you that you have unsaved changes. – TheLegendaryCopyCoder Oct 09 '18 at 08:27
  • 2
    This worked for me on Chrome 71.0.3578.98 (Official Build) (64-bit) – JacobRossDev Jan 30 '19 at 20:52
  • 2
    This works really well, but if you're using this with a form for submission, you will need to clear the modified_inputs set first before otherwise you will get a confirm changes alert. I added an event listener to my form to call on submit, and just ran the set function clear, that way the modified_inputs is clear when beforeunload event listener runs, and allows post to continue. – stepquick Jan 07 '20 at 20:09
  • 1
    Cool actually copy and paste. thanks for save my time. Work for me in Firfox 75.0 – Vinay Kaithwas Apr 17 '20 at 11:30
  • `beforeinput` is not supported in Firefox or IE11 – sol Jul 06 '20 at 04:58
  • 1
    @sol: You can track this issue and voice your support at https://bugzilla.mozilla.org/show_bug.cgi?id=970802 – Eli Grey Jul 29 '20 at 22:46
  • Doesn't work for checkboxes as their value is always 'on'. – Matt Nov 27 '20 at 02:25
  • 1
    Doesn't work on radio inputs/checkboxes but that's not a deal-breaker nor do I think it's worth bothering checking for further hacks. Input values are what we need to check for. I doubt a user REALLY wants to stay on the page if he ticked a checkbox :) – usernotnull Dec 18 '20 at 13:10
  • @EliGrey this is great! Is there a way to customise the "Leave Site?" header? I would change it to "Leave Page?" – Jurgen Cuschieri Feb 17 '21 at 20:00
40

via jquery

$('#form').data('serialize',$('#form').serialize()); // On load save form current state

$(window).bind('beforeunload', function(e){
    if($('#form').serialize()!=$('#form').data('serialize'))return true;
    else e=null; // i.e; if form state change show warning box, else don't show it.
});

You can Google JQuery Form Serialize function, this will collect all form inputs and save it in array. I guess this explain is enough :)

Wasim A.
  • 9,660
  • 22
  • 90
  • 120
  • 7
    do not use #form as form id because window will create and object named form :) – mdikici Jul 25 '14 at 15:26
  • 2
    Check here if it is not working for you: http://stackoverflow.com/questions/7080269/javascript-before-leaving-the-page/21061009#comment41813429_21061009 – Leniel Maccaferri Oct 28 '14 at 01:38
21

Built on top of Wasim A.'s excellent idea to use serialization. The problem there was that the warning was also shown when the form was being submitted. This has been fixed here.

var isSubmitting = false

$(document).ready(function () {
    $('form').submit(function(){
        isSubmitting = true
    })

    $('form').data('initial-state', $('form').serialize());

    $(window).on('beforeunload', function() {
        if (!isSubmitting && $('form').serialize() != $('form').data('initial-state')){
            return 'You have unsaved changes which will not be saved.'
        }
    });
})

It has been tested in Chrome and IE 11.

Eerik Sven Puudist
  • 2,098
  • 2
  • 23
  • 42
  • 2
    Wasim A and Eerik Sven Puudist - Thanks I used id for form and id for my save button as all my buttons were type submit
    and few more all type submit Hence replaced .Submit( by specific click $('#save').click(function(){ isSubmitting = true }) Use '#marksform' instead of 'form'
    – Jayanta May 25 '19 at 14:02
  • I also used the id instead of the form and it works too. – Jeric Cruz Jul 28 '20 at 05:45
11

The following one-liner has worked for me.

window.onbeforeunload = s => modified ? "" : null;

Just set modified to true or false depending on the state of your application.

spencer.sm
  • 19,173
  • 10
  • 77
  • 88
  • 2
    Some suggestions: Return a custom message rather than an empty string as some browsers will display it (e.g. Edge), and return `undefined` rather than `null` as IE will print 'null' if `modified` is false. – Kevin Lee Jul 17 '17 at 05:50
  • 1
    how about mobile devices? my understanding is that IOS devices do not honor onbeforeunload. – jaybro Apr 10 '18 at 18:59
10

Based on the previous answers, and cobbled together from various places in stack overflow, here is the solution I came up with which handles the case when you actually want to submit your changes:

window.thisPage = window.thisPage || {};
window.thisPage.isDirty = false;

window.thisPage.closeEditorWarning = function (event) {
    if (window.thisPage.isDirty)
        return 'It looks like you have been editing something' +
               ' - if you leave before saving, then your changes will be lost.'
    else
        return undefined;
};

$("form").on('keyup', 'textarea', // You can use input[type=text] here as well.
             function () { 
                 window.thisPage.isDirty = true; 
             });

$("form").submit(function () {
    QC.thisPage.isDirty = false;
});
window.onbeforeunload = window.thisPage.closeEditorWarning;

It's worth noting that IE11 seems to require that the closeEditorWarning function returns undefined for it not to show an alert.

Jonathan
  • 25,873
  • 13
  • 66
  • 85
  • This solution does not verify that any values are changed from their original state, therefor invalidating in the case that the user inputs text and the deletes that inputted text before attempting to navigate away. – Brett Weber Apr 24 '14 at 22:41
  • Good point, @BrettWeber. You could modify closeEditorWarning in that case, and have "if (window.thisPage.isDirty && uiHasChanged())", where uiHasChanged is a function that knows about the original state from the server, and checks all differences, but this solution was for a case where I didn't particularly want to do all that extra work, and this only has false positives when actual keypresses have been made in the textareas, which I felt was a reasonable compromise. :) – Jonathan Apr 25 '14 at 14:27
  • Shouldn't `QC.thisPage.isDirty = false;` be `window.thisPage.isDirty = false;`? That way, it will check if you're submitting the form and not show a warning. Seemed to work for my tests. Also, most forms have an `input` rather than a `textarea`. I used the following which works rather well: `$("form").on('keyup', 'textarea,input,select', function () { ` – Mike Jun 27 '17 at 10:12
6

You can use serialize() to create a URL encoded text string by serializing form values and check whether the form has changed beforeunload

$(document).ready(function(){
    var form = $('#some-form'),
        original = form.serialize()

    form.submit(function(){
        window.onbeforeunload = null
    })

    window.onbeforeunload = function(){
        if (form.serialize() != original)
            return 'Are you sure you want to leave?'
    }
})

Refer this link https://coderwall.com/p/gny70a/alert-when-leaving-page-with-unsaved-form Written by Vladimir Sidorenko

Epsi Vennila
  • 190
  • 3
  • 9
  • 1
    It Worked, but as answered by code caster, there are some flaws with this method, refer this explanation https://stackoverflow.com/a/7317311/10555981 – Pradeep Nov 08 '19 at 06:43
  • There are already a couple of answers using `serialize()`, and indeed, it has its drawbacks. – CodeCaster Nov 08 '19 at 07:04
  • This should be the correct answer, very simple to use and works as expected. – Jodyshop Nov 28 '19 at 23:57
4

Following code works great. You need to reach your form elements' input changes via id attribute:

var somethingChanged=false;
            $('#managerForm input').change(function() { 
                somethingChanged = true; 
           }); 
            $(window).bind('beforeunload', function(e){
                if(somethingChanged)
                    return "You made some changes and it's not saved?";
                else 
                    e=null; // i.e; if form state change show warning box, else don't show it.
            });
        });
metzelder
  • 655
  • 2
  • 15
  • 35
4

Tested Eli Grey's universal solution, only worked after I simplified the code to

  'use strict';
  (() => {
    const modified_inputs = new Set();
    const defaultValue = 'defaultValue';
    // store default values
    addEventListener('beforeinput', evt => {
      const target = evt.target;
      if (!(defaultValue in target.dataset)) {
        target.dataset[defaultValue] = ('' + (target.value || target.textContent)).trim();
      }
    });

    // detect input modifications
    addEventListener('input', evt => {
      const target = evt.target;
      let original = target.dataset[defaultValue];

      let current = ('' + (target.value || target.textContent)).trim();

      if (original !== current) {
        if (!modified_inputs.has(target)) {
          modified_inputs.add(target);
        }
      } else if (modified_inputs.has(target)) {
        modified_inputs.delete(target);
      }
    });

    addEventListener(
      'saved',
      function(e) {
        modified_inputs.clear()
      },
      false
    );

    addEventListener('beforeunload', evt => {
      if (modified_inputs.size) {
        const unsaved_changes_warning = 'Changes you made may not be saved.';
        evt.returnValue = unsaved_changes_warning;
        return unsaved_changes_warning;
      }
    });

  })();

The modifications to his is deleted the usage of target[defaultValue] and only use target.dataset[defaultValue] to store the real default value.

And I added a 'saved' event listener where the 'saved' event will be triggered by yourself on your saving action succeeded.

But this 'universal' solution only works in browsers, not works in app's webview, for example, wechat browsers.

To make it work in wechat browsers(partially) also, another improvements again:

  'use strict';
  (() => {
    const modified_inputs = new Set();
    const defaultValue = 'defaultValue';
    // store default values
    addEventListener('beforeinput', evt => {
      const target = evt.target;
      if (!(defaultValue in target.dataset)) {
        target.dataset[defaultValue] = ('' + (target.value || target.textContent)).trim();
      }
    });

    // detect input modifications
    addEventListener('input', evt => {
      const target = evt.target;
      let original = target.dataset[defaultValue];

      let current = ('' + (target.value || target.textContent)).trim();

      if (original !== current) {
        if (!modified_inputs.has(target)) {
          modified_inputs.add(target);
        }
      } else if (modified_inputs.has(target)) {
        modified_inputs.delete(target);
      }

      if(modified_inputs.size){
        const event = new Event('needSave')
        window.dispatchEvent(event);
      }
    });

    addEventListener(
      'saved',
      function(e) {
        modified_inputs.clear()
      },
      false
    );

    addEventListener('beforeunload', evt => {
      if (modified_inputs.size) {
        const unsaved_changes_warning = 'Changes you made may not be saved.';
        evt.returnValue = unsaved_changes_warning;
        return unsaved_changes_warning;
      }
    });

    const ua = navigator.userAgent.toLowerCase();

    if(/MicroMessenger/i.test(ua)) {
      let pushed = false

      addEventListener('needSave', evt => {
        if(!pushed) {
          pushHistory();

          window.addEventListener("popstate", function(e) {
            if(modified_inputs.size) {
              var cfi = confirm('确定要离开当前页面嘛?' + JSON.stringify(e));
              if (cfi) {
                modified_inputs.clear()
                history.go(-1)
              }else{
                e.preventDefault();
                e.stopPropagation();
              }
            }
          }, false);
        }

        pushed = true
      });
    }

    function pushHistory() {
      var state = {
        title: document.title,
        url: "#flag"
      };
      window.history.pushState(state, document.title, "#flag");
    }
  })();
Jeff Tian
  • 5,210
  • 3
  • 51
  • 71
  • For anyone coming in here in early 2021, I can relate that the first (non-weChat) version works for me in a single-form page in FireFox 84.0.2, unlike others on this page including (surprisingly) mtjmohr's. All in all, I tested this code in FireFox 84.0.2, Chrome 88.0.4324.96, and Safari 13.1.2 (13609.3.5.1.5) on a macOS 10.13.6 system. – mhucka Jan 25 '21 at 18:37
3
var unsaved = false;
$(":input").change(function () {         
    unsaved = true;
});

function unloadPage() {         
    if (unsaved) {             
        alert("You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?");
    }
} 
window.onbeforeunload = unloadPage;
Christos Lytras
  • 36,310
  • 4
  • 80
  • 113
Ankit
  • 4,755
  • 2
  • 22
  • 14
2

Short answer:

let pageModified = true

window.addEventListener("beforeunload", 
  () => pageModified ? 'Close page without saving data?' : null
)
Daniel Garmoshka
  • 5,849
  • 39
  • 40
2

The solution by Eerik Sven Puudist ...

var isSubmitting = false;

$(document).ready(function () {
    $('form').submit(function(){
        isSubmitting = true
    })

    $('form').data('initial-state', $('form').serialize());

    $(window).on('beforeunload', function() {
        if (!isSubmitting && $('form').serialize() != $('form').data('initial-state')){
            return 'You have unsaved changes which will not be saved.'
        }
    });
})

... spontaneously did the job for me in a complex object-oriented setting without any changes necessary.

The only change I applied was to refer to the concrete form (only one form per file) called "formForm" ('form' -> '#formForm'):

<form ... id="formForm" name="formForm" ...>

Especially well done is the fact that the submit button is being "left alone".

Additionally, it works for me also with the lastest version of Firefox (as of February 7th, 2019).

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
mtjmohr
  • 99
  • 9
1

Adding to te idea of @codecaster you could add this to every page with a form (in my case i use it in global way so only on forms would have this warn) change his function to

if ( formSubmitting || document.getElementsByTagName('form').length == 0) 

Also put on forms submit including login and in cancel buttons links so when person press cancel or submit the form won't trigger the warn also in every page witouth a form...

<a class="btn btn-danger btn-md" href="back/url" onclick="setFormSubmitting()">Cancel</a>
1

You could check for a detailed explanation here: http://techinvestigations.redexp.in/comparison-of-form-values-on-load-and-before-close/

The main code:

function formCompare(defaultValues, valuesOnClose) {
 
    // Create arrays of property names
    var aPropsFormLoad = Object.keys(defaultValues);
    var aPropsFormClose = Object.keys(valuesOnClose);
 
    // If number of properties is different,
    // objects are not equivalent
    if (aPropsFormLoad.length != aPropsFormClose.length) {
        return false;
    }
 
    for (var i = 0; i < aPropsFormLoad.length; i++) {
        var propName = aPropsFormLoad[i];
 
        // If values of same property are not equal,
        // objects are not equivalent
        if (defaultValues[aPropsFormLoad]+"" !== valuesOnClose[aPropsFormLoad]+"") {
            return false;
        }
    }
 
    // If we made it this far, objects
    // are considered equivalent
    return true;
 
}

//add polyfill for older browsers, as explained on the link above

//use the block below on load
    for(i=0; i < document.forms[0].elements.length; i++){
    console.log("The field name is: " + document.forms[0].elements[i].name +
        " and it’s value is: " + document.forms[0].elements[i].value );
    aPropsFormLoad[i] = document.forms[0].elements[i].value;
    }

//create a similar array on window unload event.

//and call the utility function
    if (!formCompare(aPropsOnLoad, aPropsOnClose))
    {
    //perform action: 
    //ask user for confirmation or
    //display message about changes made
    }
jkmartindale
  • 523
  • 2
  • 9
  • 22
Kanika Sud
  • 11
  • 2
0

I did it differently, sharing here so that someone can get help, tested only with Chrome.

I wanted to warn user before closing the tab only if there are some changes.

<input type="text" name="field" value="" class="onchange" />

var ischanged = false;

$('.onchange').change(function () {
    ischanged = true;
});

window.onbeforeunload = function (e) {
    if (ischanged) {
        return "Make sure to save all changes.";
    }        
};

Works good, but got an-other issue, when i submit the form i get the unwanted warning, i saw lots of workaround on it, this is because onbeforeunload fires before onsubmit thats why we can't handle it in onsubmit event like onbeforeunload = null, but onclick event of submit button fires before these both events, so i updated the code

var isChanged = false;
var isSubmit = false;

window.onbeforeunload = function (e) {
    if (isChanged && (!isSubmit)) {
        return "Make sure to save all changes.";
    }        
};

$('#submitbutton').click(function () {
    isSubmit = true;
});

$('.onchange').change(function () {
    isChanged = true;
});
sairfan
  • 970
  • 2
  • 12
  • 20
0

I made following code. It can compare changes in all fields (except those marked with .ignoreDirty class) or optionally for currently visible fields only. It can be reinitialized for new fields added by Javascript. From that reason I save not the form status but the status of each control.

/* Dirty warning for forms */
dirty = (skipHiddenOrNullToInit) => {
    /*  will return True if there are changes in form(s)
        for first initialization you can use both: .dirty(null) or .dirty() (ignore its result)
            .dirty(null) will (re)initialize all controls - in addititon use it after Save if you stay on same page
            .dirty() will initialize new controls - in addititon use it if you add new fields with JavaScript
        then
            .dirty() (or: .dirty(false)) says if data are changed without regard to hidden fields
            .dirty(true) says if data are changed with regard to hidden fields (ie. fields with .d-none or .hidden class)
        controls with .ignoreDirty class will be skipped always
        previous about .d-none, .hidden, .ignoreDirty applies to the control itself and all its ancestors
    */
    let isDirty = false;
    let skipSelectors = '.ignoreDirty';
    if (skipHiddenOrNullToInit) {
        skipSelectors += ', .d-none, .hidden'
    } else if (skipHiddenOrNullToInit === undefined) {
        skipHiddenOrNullToInit = false;
    }
    $('input, select').each(
    function(_idx, el) {
        if ($(el).prop('type') !== 'hidden') {
            let dirtyInit = $(el).data('dirty-init');
            if (skipHiddenOrNullToInit === null || dirtyInit === undefined) {
                try {
                    isChromeAutofillEl = $(el).is(":-webkit-autofill");
                } catch (error) {
                    isChromeAutofillEl = false;
                }
                if (isChromeAutofillEl && $(el).data('dirty-init') === undefined) {
                    setTimeout(function() {  // otherwise problem with Chrome autofilled controls
                        $(el).data('dirty-init', $(el).val());
                    }, 200)
                } else {
                    $(el).data('dirty-init', $(el).val());
                }
            } else if ($(el).closest(skipSelectors).length === 0 && dirtyInit !== $(el).val()) {
                isDirty = true;
                return false; // breaks jQuery .each
            }
        }
    }
    );
    return isDirty;
}

I have additional troubles with Chrome autofill values because it is difficult to initizialize and have them loaded already. So I do not initialize on page load but in any focusin event. (But: Maybe there is still problem with control values changed by JavaScript.) I use following code which I call at page load:

let init_dirty = (ifStayFunc) => {
    /*  ifStayFunc: optional callback when user decides to stay on page
    use .clearDirty class to avoid warning on some button, however:
        if the button fires JavaScript do't use .clearDirty class and instead
            use directly dirty(null) in code - to be sure it will run before window.location */
    $('input, select').on('focusin', function(evt) {
        if (!$('body').data('dirty_initialized')) {
            dirty();
            $('body').data('dirty_initialized', true);
        }
    });
    window.addEventListener('beforeunload', (evt) => {
        if (dirty(true)) {
            if (ifStayFunc) {
                ifStayFunc();
            }
            evt.preventDefault();
            evt.returnValue = '';  // at least Google Chrome requires this
        }
    });
    $('.clearDirty').on('click', function(evt) {
        dirty(null);
    });
};

So, I add the .clearDirty class to the buttons which provide Save and that way I prevent the warning in this case. Callback ifStayFunc allows me to do something if user will Stay on Page while he is warned. Typically I can show additional Save Button (if I have still visible only some default/primary button, which makes Safe+SomethingMore - and I want allow Save withou this "SomethingMore").

mirek
  • 1,140
  • 11
  • 10
0

For React and react-router-dom v6:

import { useCallback } from 'react'
import { useBeforeUnload, unstable_usePrompt as usePrompt } from 'react-router-dom'

interface UnsavedChangesWarningProps {
  isDirty?: boolean
  message?: string
}

export const useUnsavedChangesWarning = ({ isDirty, message }: UnsavedChangesWarningProps) => {
  useBeforeUnload(
    useCallback(
      (event) => {
        if (isDirty) {
          event.preventDefault()
          event.returnValue = message
          return message
        }
      },
      [isDirty],
    ),
  )

  usePrompt({ when: !!isDirty, message })
}

Just before that, you need to switch to the new routing syntax https://reactrouter.com/en/main/routers/router-provider

Kiritushka
  • 770
  • 1
  • 6
  • 11
0

A 2023 answer:

Based on this article from the MDN docs, property returnValue is now considered deprecated.

If you just want to prevent a user from leaving the page, you can use .preventDefault() method on beforeunload event for window like below:

window.addEventListener("beforeunload", (e) => {
    e.preventDefault();
});

This will show browser's default dialog on top of the page before reloading or closing the tab.

method 2

Using the dialog HTML element. (MDN docs)

YSLdev
  • 142
  • 1
  • 11