I'm using JSF with omnifaces and cdi. Within the xhtml page I'm using some js similar to (more detailed on jsf page below)
window.onbeforeunload = function(e){
// some other stuff
return 'unsaved data';
};
to activate the browser's default confirm dialog if the window is about to be unloaded with unsaved data remaining.
Filling a form and doing a redirect to the same page, pops up the warning. If I decide to 'stay on page', I'll be able to continue my work and submit my form afterwards, like I expected it to be.
The Problem: If I navigate to other pages or try to close the tab, the warning appears as expected too. But in this case, an http POST submitting 'omnifaces.event: unload' is send to the server. As far as I can see, this causes the Bean's onDestroy()
to be called. If I choose to stay then, all values within the form are still present on the page but when submitting the form, NPEs are thrown for the values (I guess because the bean has been destroyed already, not respecting my decision on the confirm dialog).
After hours of research, I couldn't figure out why the second case causes the bean to unload, while the first approach awaits the result of the confirm dialog... Any ideas?
I already noticed that window.onbeforeunload
should be called with plain js, like mentioned in ViewScoped. This works for the first case, but not for the second one.
EDIT:
Steps to reproduce are located on the jsf page. If you follow these, you should be able to understand my problem.
SomeBean
import org.omnifaces.cdi.ViewScoped;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Named;
import java.io.Serializable;
@Named
@ViewScoped
public class SomeBean implements Serializable {
private String fieldA;
private String fieldB;
private String info;
@PostConstruct
public void init(){
this.fieldA = null;
this.fieldB = null;
this.info = "bean = " + this;
}
@PreDestroy
public void preDestroy() {
/* triggered when performing a navigation to other resources or
closing the browser tab (unwanted), but not invoked if navigation is done
within the same resource, e.g by using templates and compositions (wanted) */
this.info = "destroy will be invoked for bean " + this;
}
public void submit(){
// do smth. with the fields
}
public String getFieldA() {
return fieldA;
}
public void setFieldA(String fieldA) {
this.fieldA = fieldA;
}
public String getFieldB() {
return fieldB;
}
public void setFieldB(String fieldB) {
this.fieldB = fieldB;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
JSF page
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:p="http://primefaces.org/ui"
xml:lang="en" lang="en">
<h:head>
<title>reproduce example</title>
<script type="application/javascript">
$(document).ready(function () {
var unsavedData = false;
function setUnsavedData(flag) {
unsavedData = flag;
}
window.onbeforeunload = function (e) {
e = e || window.event;
if (unsavedData) {
if (e) {
e.preventDefault();
e.returnValue = 'Any string';
}
return 'Any string';
} else {
return undefined;
}
};
$(document).on("change", ":input:not(.stateless)", function () {
setUnsavedData(true);
});
$(document).on("click", "button.stateless", function () {
setUnsavedData(false);
});
});
</script>
</h:head>
<h:body>
<h:form id="myform">
<h3>steps to reproduce:</h3><br/>
1) enter some value for fieldA<br/>
2) click the 'navigate via navigationBean' link > confirm the dialog and 'stay on page'<br/>
3) press the submit button > as you can see, the bean instance is still the same and value is passed.<br/>
4) enter some value for fieldB<br/>
5) click somewhere else on the page to lose the input's focus (otherwise the confirm dialog won't show up!)<br/>
7) close the browser tab or use the browser's nav buttons > confirm the dialog and 'stay on page'<br/>
8) press the submit button again > as you can see now, submit has not been called! Pressing submit shows the bean instance has changed!<br/><br/>
<h4> > For this example, values are still usable after the new bean was initialized. Because the original page is way more complex then this example, <br/>
I really need to prevent onmnifaces from initializing a new bean, when a user confirms to stay on the page, even if he tries to close the tab!</h4><br/>
<br/>
fieldA
<p:inputText value="#{someBean.fieldA}"/>
<!-- note: in real code this is represented by a NavigationBean logic!-->
<p:commandLink value="navigate via navigationBean"
action="#"
ajax="false">
</p:commandLink>
<br/>
fieldB
<p:inputText value="#{someBean.fieldB}"/>
<br/>
<p:commandButton
id="submitBtn"
value="submit"
action="#{someBean.submit()}"
process="@form"
update="@form" styleClass="stateless">
</p:commandButton>
<br/>
<br/>
bean info: <p:outputLabel id="output_1" value="#{someBean.info}"/>
<br/>
value info for fieldA: <p:outputLabel id="output_2" value="#{someBean.fieldA}"/>
<br/>
value info for fieldB: <p:outputLabel id="output_3" value="#{someBean.fieldB}"/>
</h:form>
</h:body>
</html>
Note: This is a minimized version, the original has more details, but caused by some restrictions of our company, I can't show the full code stack. I hope this is still enough and imo the problem related parts are shown.
Omnifaces version is 2.7
The @ViewScoped
unload script is initialized in end of HTML <body>
. It will at that point check for existing window.onbeforeunload
functions before decorating it.
Your window.onbeforeunload
function is defined during $(document).ready()
. But this is not yet executed in end of HTML <body>
. It's only executed after the end of <html>
. Hence the @ViewScoped
unload script won't be able to correctly decorate it.
You need to make sure that the window.onbeforeunload
is defined before the @ViewScoped
unload script is initialized. You can do so by putting it outside $(document).ready()
and importing the JavaScript file containing the definition via a <h:outputScript target="head">
or <h:outputScript target="body">
. Putting the script inline in a <head>
does also work, but this is not recommended as it only makes the HTML document bigger for nothing and doesn't offer the browser the opportunity to cache the scripts.