Unsaved Changes - Detect page exit or reload

Hi all,

Is there anything in Vaadin Flow like this → https://vaadin.com/directory/component/beforeunload ??

From the addon page…
“Offers simple one line API to define verification dialog shown to user when user is trying to exit or reload page. Can be used to warn if user has any unsaved changes.”

I’m not sure how to hook the beforeUnload event from the client side.

Thanks,

Stuart

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. :slight_smile:

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.

In Vaadin Flow, your View could simply implement [BeforeLeaveObserver]
(https://vaadin.com/docs/v12/flow/routing/tutorial-routing-lifecycle.html).

In the linked example, they use a not-implemented method this.hasChanges(), but you can use binder.hasChanges() ([API]
(https://vaadin.com/api/platform/12.0.6/com/vaadin/flow/data/binder/Binder.html#hasChanges--)) there - as long as you use binder.readBean(bean); and not binder.setBean(bean);
This works like a charm for me.

Hi Kaspar,

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.

Stuart.

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.

Yes, I’d like to, though I’ve not ventured down that rabbit hole yet :slight_smile:

Hey Stuart

I am trying to implement it your way, and I noticed that it doesn’t work when navigating to another view (which has the same parentLayout) by clicking on a RouterLink, because the window is not unloaded but only some inner parts of it.

Therefore I combined our two approaches (BeforeLeaveObserver and window-beforeunload) as they seem to compliment each other nicely. One only triggers for RouterLink Navigation, while the other triggers for everything else.

The only thing that is not optimal with this combined approach is that there are two different confirm dialogs. window-beforeunload will show a browser-native popup that I cannot control (styling, I18N, actions) (or can I?) and with the BeforeLeaveObserver I can define my own ConfirmDialog.

Yes, that is a good point. In my implementation I store the view and reattach it when you go back to it, so I don’t check for changes when navigating away to another page. If I was to, you are right, I’d use the BeforeLeaveObserver.

Sadly is doesn’t look like you can style the window-beforeUnload dialog as it’s browser controlled. When we are using window-beforeUnload, we are only asking the browser the ask the user to confirm navigating away. Its then down to browser to implement that. (It stops the webpage from spawning loads of sub windows like they used to do in the 90s. :slight_smile: :slight_smile: )

S.

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.

Hi Leif,

I guess beforeUnload is fine if you’re not providing any download links. For me, the main concern is to make sure the user doesn’t loose and changes they may make to a form before saving.

With the Beacon API in your suggestion, can you stop users navigating away or closing the browser if they have unsaved changes?

Stuart.

Update for Vaadin 14
During migration from 13 to 14 this custom solution has caused problems for Stuart and me, see [this thread]
(https://vaadin.com/forum/thread/17800495/vaadin-14-javascript). 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.

Hi Kaspar,

Thanks for you work on this, its been invaluable!

Stuart

Regarding Leif’s objection to using the beforeunload event: I have tested whether the event is triggered when clicking a download link, and have also tested it with a [FileDownloadWrapper]
(https://vaadin.com/directory/component/file-download-wrapper/overview).

When [using an Anchor]
(https://vaadin.com/forum/thread/17061112/17061262) as download link, then Leif is correct - the beforeunload event is fired.
When using a FileDownloadWrapper as download button, then there is no such event being fired.

TL;DR
As long as you use FileDownloadWrapper for all your downloads, then you should be fine.

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

I did an example using the Beacon API from the unload event for an upcoming collection of real-world Vaadin use case examples. While the overall collection is still in progress, you can have a sneak peek at https://cookbook.vaadin.com/notice-closed.

The trick is to generate a unique URL for the UI, add an unload listener that triggers a beacon request to that URL and finally register a request handler with the session to handle that request to fire an event to Java listeners. Finally, there’s also some cleanup to remove the request handler.

This implementation doesn’t support IE11, but that could be fixed by making the unload listener send a synchronous XHR if navigator.sendBeacon doesn’t exist. There might also be some complications with @PreserveOnRefresh depending on exactly what you do in the listener.