Loading...
Important Notice - Forums is archived

To simplify things and help our users to be more productive, we have archived the current forum and focus our efforts on helping developers on Stack Overflow. You can post new questions on Stack Overflow or join our Discord channel.

Product icon
TUTORIAL

Vaadin lets you build secure, UX-first PWAs entirely in Java.
Free ebook & tutorial.

Unsaved Changes - Detect page exit or reload

Stuart Robinson
4 years ago Feb 18, 2019 2:55pm
Stuart Robinson
4 years ago Feb 19, 2019 11:57am

Ok, to answer my own question...

(I'm using Vaadin 12 with Spring Boot)

First I create a JavaScript file which adds a beforeUnload event listener to the window and stores if there are any changes.

var hasChanges = false;
function addChangesListener() {    
    window.addEventListener('beforeunload', function (e) {
      if (hasChanges) {
          // Cancel the event
          e.preventDefault();
          // Chrome requires returnValue to be set
          e.returnValue = 'There are changes afoot';
      }
    });
}
function setChanges()   { hasChanges = true;  }
function resetChanges() { hasChanges = false; }

I then add the @JavaScript import to my UI.

@JavaScript("frontend://js/script.js")

Next I created a ServiceInitListener so I can call the function to add the event listener when the body.onload event if fired.

@Component
public class ApplicationServiceInitListener implements VaadinServiceInitListener {
    @Override
    public void serviceInit(ServiceInitEvent event) {
        event.addBootstrapListener(response -> 
                response.getDocument().body().attr("onload", "addChangesListener();"));
    }
}

I then add a valueChangeListener to my binder and get it to call the setChanges function in script.js.

binder.addValueChangeListener(listener -> UI.getCurrent().getPage().executeJavaScript("setChanges();"));

In my ’save’ button, I reset the changes flag in the script.js file..

Button save = new Button("Save",click -> UI.getCurrent().getPage().executeJavaScript("resetChanges();")); 

I also changed the ValueChangeMode to EAGER on all the fields in the binder…

binder.getFields().filter(field -> field instanceof HasValueChangeMode)
                .forEach(field -> ((HasValueChangeMode) field).setValueChangeMode(ValueChangeMode.EAGER));

That's it. :-)

Any changes to any of the fields in the binder results in a 'navigating away' message from the browser when navigating away or closing the tab or browser.

Good times.

S.

Last updated on Feb, 19th 2019
Kaspar Scherrer
4 years ago Feb 19, 2019 12:46pm

In Vaadin Flow, your View could simply implement BeforeLeaveObserver.

In the linked example, they use a not-implemented method this.hasChanges(), but you can use binder.hasChanges() (API) there - as long as you use binder.readBean(bean); and not binder.setBean(bean);
This works like a charm for me.

Last updated on Feb, 19th 2019
Stuart Robinson
4 years ago Feb 19, 2019 12:50pm
Kaspar Scherrer
4 years ago Feb 20, 2019 8:15am

Stuart Robinson: Does that work if you close your browser?? The BeforeLeaveObserver would need a round trip to the server which wouldn't get fired if you quit your tab/browser.

Yes you are right, the BeforeLeaveObserver is not as strong as I believed. It doesn't work when the browser or the tab is closed, or even when I enter a new url (even though the new url is just another view in the same app). It seems to only work when navigating away by clicking a link.

Your usage of the beforeUnload event is quite clever. Have you thought about making an add-on for this? I think many Vaadin devs like me would be interested in this.

Stuart Robinson
4 years ago Feb 21, 2019 3:07pm
Kaspar Scherrer
4 years ago Mar 28, 2019 10:45am
Stuart Robinson
4 years ago Mar 28, 2019 10:56am
Leif Åstrand
3 years ago Jun 19, 2019 5:42am

I strongly recommend against using the browser's beforeunload event for anything related to cleanup. The reason is that in some situations the event is fired even though the user is actually not navigating away from the page.

The most common case is if the user clicks a link that starts a download. In that case the browser will fire the even immediately when the user clicks the link. Slightly later when the browser receives the response headers, it will discover that it's a file to download and not a new HTML page to display. The end result is then that beforeunload has been fired but the original page is still active.

If you want to use the event for cleanup, then the best approach today is probably a combination of the unload event and then using the new-ish Beacon API for notifying the server that the user has actually navigated away. Integrating will require slightly more JavaScript and custom integration on the server, but it has the benefit that it will actually work.

Stuart Robinson
3 years ago Jun 21, 2019 9:17am
Kaspar Scherrer
3 years ago Aug 20, 2019 1:14pm

Update for Vaadin 14
During migration from 13 to 14 this custom solution has caused problems for Stuart and me, see this thread. It turned out that the problem only arose because the scope of the script has changed and it's run with strict mode. Adding window. before certain functions was key here.

For people using the code shared in this thread here, I wanted to give an update how it looks in my V14 Project

{project root}/frontend/js/detectChanges.js:

var hasChanges = false;
window.setChanges = function() { hasChanges = true; }
window.resetChanges = function() { hasChanges = false; }

window.addEventListener('beforeunload', function (e) {
    if (hasChanges) {
        // Cancel the event
        e.preventDefault();
        // Chrome requires returnValue to be set
        e.returnValue = 'Unsaved Changes';
    }
});

Any View that needs to catch unsaved changes before leaving:

@JsModule("./js/detectChanges.js")
@Route(value = "MyView", layout = MainView.class)
public class ViewWithUnsavedChangesHandling extends VerticalLayout {...}

Note that with this implementation, defining your own VaadinServiceInitListener implementation is not needed (the body does not need an onload attribute). Also note that Leif's objection to the usage of "beforeunload" is not addressed yet with this solution.

Last updated on Aug, 20th 2019
Stuart Robinson
3 years ago Aug 20, 2019 3:23pm
Kaspar Scherrer
3 years ago Aug 23, 2019 9:01am
Miki Olsz
2 years ago May 04, 2020 1:11pm

I have allowed myself to take Kaspar's and Stuart's code from this thread and make a Vaadin 14.X add-on called UnloadObserver. It allows controlling whether or not to capture beforeunload events and also can have server-side listeners to be notified when those events happen.

It is a part of SuperFields, https://vaadin.com/directory/component/superfields - I have also made it clear in the docs and everywhere that the code is based on this thread.

If you find any issues with this component, feel free to submit an issue to https://github.com/vaadin-miki/super-fields/issues

Leif Åstrand
2 years ago Aug 13, 2020 8:52am