Friday, November 16, 2007

Warning on page exit for Wicket 1.2.x

I needed to warn the user if s/he leaves a form that hasn't been submitted. I was surprised that this requirement wasn't very commonly solved on the Net, and even less so for Wicket. There are more complicated solutions out there, but none of them was super simple. This is the simplest solution I could come up with.

After researching it I found out that to make this warning work you need to trap the Javascript event called onbeforeunload. This event is especially for this purpose and is supported by the major browsers. If you return a string from this event (typically "Are you sure you want to leave this page?"), the browser will display this string in a confirm dialog. If the user clicks OK, the page is unloaded and the form is submitted (or the link is followed), but if the user clicks Cancel, control returns to the page and the form is not submitted.

My solution consists of one form and one short javascript file. First, an example of how to use it:

WarnOnExitPage.java

public class WarnOnExitPage extends WebPage
{
private static final long serialVersionUID = 1L;

private String name;
private String email;

public WarnOnExitPage(final PageParameters parameters)
{
final WarnOnExitForm form = new WarnOnExitForm("myform");

final TextField nameField = new TextField("namefield",
new PropertyModel(this, "name"));
// Note the use of a specialized addFormField method. This
// appends the necessary javascript.
form.addFormField(nameField);

final TextField emailField = new TextField("emailfield",
new PropertyModel(this, "email"));
form.addFormField(emailField);

// Note the use of a specialized addButton method. This
// appends the necessary javascript.
form.addButton(new Button("okbutton")
{
private static final long serialVersionUID = 1L;

@Override
protected void onSubmit()
{
setResponsePage(HomePage.class);
}
});
add(form);
}

public String getName()
{
return name;
}

public void setName(String name)
{
this.name = name;
}

public String getEmail()
{
return email;
}

public void setEmail(String email)
{
this.email = email;
}
}

WarnOnExitPage.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.sourceforge.net/" xml:lang="en"
lang="en">
<head>
<title>Wicket Quickstart Archetype Homepage</title>
</head>

<body>

<strong>Wicket Quickstart Archetype Homepage</strong>

<form wicket:id="myform">
<fieldset>
<table>
<tr>
<td>Name:</td>

<td>
<input wicket:id="namefield" type="text" />
</td>
</tr>

<tr>
<td>Email:</td>

<td>
<input wicket:id="emailfield" type="text" />
</td>
</tr>

<tr>
<td> </td>

<td>
<input wicket:id="okbutton" type="submit" />
</td>
</tr>
</table>
</fieldset>
</form>

<wicket:link>
<a href="HomePage.html">HomePage</a>
</wicket:link>

</body>
</html>

Now the solution:

WarnOnExitForm.java

public class WarnOnExitForm extends Form
{
private static final long serialVersionUID = 1L;

private boolean firstTimeThru = true;

public WarnOnExitForm(String id)
{
super(id);
// Don't confirm when the user hits the enter key
add(new SimpleAttributeModifier("onsubmit", "dontConfirm();"));
// Turn off autocomplete because IE will not register an entry
// as a change if it came from the autocomplete cache
add(new SimpleAttributeModifier("autocomplete", "off"));
add(HeaderContributor.forJavaScript(WarnOnExitForm.class, "WarnOnExit.js"));
}

public void addFormField(FormComponent c)
{
add(c);
c.add(new SimpleAttributeModifier("onchange", "setDirty();"));
// onblur is not always reliable when it comes to the back
// button, but it works for other situations
c.add(new SimpleAttributeModifier("onblur", "setDirty();"));
}

public void addButton(Button b)
{
add(b);
// Don't confirm when the user hits a form button
b.add(new SimpleAttributeModifier("onclick", "dontConfirm();"));
}

/** We must add attributes to the page's body tag in onAttach()
* because the body tag is not available at construction time
*/
@Override
protected void onAttach()
{
super.onAttach();

if (firstTimeThru)
{
final WebMarkupContainer c = getWebPage().getBodyContainer()
.getBodyContainer();
c.add(new SimpleAttributeModifier("onload",
"FORMCONFIRM=true;FORMISDIRTY=false;"));
c.add(new SimpleAttributeModifier("onbeforeunload",
"return warnOnPageExit('" + getString("formdirtymessage")
+ "');"));
firstTimeThru = false;
}
}
}

WarnOnExitForm.properties

formdirtymessage=You have attempted to leave this page. \
If you have made any changes to the fields without clicking \
the OK button, your changes will be lost. Are you sure you \
want to exit this page?

WarnOnExit.js

function warnOnPageExit(formdirtymessage)
{
if (FORMCONFIRM && FORMISDIRTY){return formdirtymessage};
}
function setDirty()
{
FORMISDIRTY=true;
}
function dontConfirm()
{
FORMCONFIRM=false;
}
As you can see, the solution is a simple one. However, it does gloss over some things:
  • Wicket modifies the body tag when you add an Ajax behavior to your page. This could overwrite your body tag modifications
  • onAttach() is not available in Wicket 1.3
  • getBodyContainer() is not available in Wicket 1.3

No comments: