executeJs with onbeforeunload or similar events

According to https://vaadin.com/docs/v14/flow/browser/tutorial-execute-javascript.html and https://vaadin.com/docs/v14/flow/element-api/client-server-rpc.html, I should be able to do the following:

	public WmsEntryPoint() {
        ...
		final UI currentUI = UI.getCurrent();
		currentUI.addDetachListener(e->onDetach(e));

        getElement().executeJs(
        		"window.onbeforeunload = function (e) { var e = e || window.event; " +
        		"this.$server.closeTab($0); return; };",
        		currentUI);
	}
	
	@ClientCallable
	private void closeTab( UI currentUI ) {
		if( currentUI.getSession() instanceof VaadinSession )
			LOGGER.info( "Tab being closed for session/UI " + currentUI.getSession() + "/" + currentUI );
		else
			LOGGER.info( "Tab being closed for UI " + currentUI );
		currentUI.close();
	}	

This does not work, though. I get this problem in Chrome console:

VM10048:3 Uncaught TypeError: Cannot read property 'closeTab' of undefined
    at window.onbeforeunload (eval at tt (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:980), <anonymous>:3:101)
    at Object.eval (eval at tt (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:980), <anonymous>:3:21)
    at tt (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:980)
    at st (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:937)
    at qt (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:562)
    at Yq (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:479)
    at ur.vr [as W]
 (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:988)
    at oA (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:870)
    at $q (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:983)
    at or.pr [as D]
 (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:988)
    at Ej (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:396)
    at Sq (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:984)
    at Tq (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:968)
    at et.gt [as qb]
 (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:988)
    at TA.UA [as K]
 (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:988)
    at XMLHttpRequest.<anonymous> (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:572)
    at sb (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:421)
    at vb (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:856)
    at XMLHttpRequest.<anonymous> (VM10039 client-59E69D05DF6B41AFC722309793280A0E.cache.js:585)

So what am I doing wrong?

Alejandro Duarte has [a nice blog post]
(https://www.alejandrodu.com/2019/01/how-to-call-java-method-from-javascript.html) about this.

Notice how he doesn’t use this before $server and instead uses the element of the view. This is crucial. For this to work you need to pass the element of the view into the js-execution.
Also, your passing around of the UI doesn’t really make sense to me, as you could just as well save the ui instance in a member field.

private final UI currentUI;
public WmsEntryPoint() {
	...
	this.currentUI = UI.getCurrent();

	currentUI.getPage().executeJs(
		"window.addEventListener('beforeunload', function(e){"+		
			"$0.$server.closeTab();"+
		"}", this.getElement()
	);
}

@ClientCallable
private void closeTab() {
	// you have access to this.currentUI here
}	

You could also put the definition of the js function in a separate file and only invoke that method in the view constructor. I don’t like writing js in java with string concatenation, and the javascript becomes reusable for other views so I always choose this way. Here is how that could look.

// {project-root}/frontend/js/addBeforeUnloadListener.js
window.addBeforeUnloadListener = function(element) {
	window.addEventListener('beforeunload', function(e){	
		element.$server.closeTab();
		// if you want, use these 2 lines to show alert message to confirm or deny leaving the page
		// e.preventDefault();
		// e.returnValue = 'Are you sure you want to leave this page?';
	}
}

// WmsEntryPoint View
@JsModule("./js/addBeforeUnloadListener.js")
public class WmsEntryPoint extends Component {
	private UI currentUI;
	public WmsEntryPoint() {
        ...
		this.currentUI = UI.getCurrent();
		currentUI.getPage().executeJs("addBeforeUnloadListener($0)", this.getElement());
	}
	
	@ClientCallable
	private void closeTab() {
		// you have access to this.currentUI here
	}	
}

Thanks, this really helped. I actually ended up using his JsModule because I liked it better for long term support.

For anyone else who is interested, and purely as an FYI, the following worked:

currentUI.getPage().executeJs(
        		"window.onbeforeunload = function (e) { var e = e || window.event; " +
        		"$0.$server.closeTab($1); return; };",
        		this.getElement(), currentUI);

but this did not:

currentUI.getPage().executeJs(
        		"window.onbeforeunload = function (e) { var e = e || window.event; " +
        		"$0.$server.closeTab($1); return; };",
				getElement(), currentUI);

This second one gave a JSON error about reference an element that was not there. I suspect this.getElement() is more precise then getElement(), and this second one ( with no this., that does NOT work ) probably tries to get the element of something else.