mixins because Tapestry gives it
special treatment.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like
We use a "strict" DTD to make IE follow the alignment rules. -->
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd">
<head>
<link rel="stylesheet" type="text/css" href="${context:css/examples/js.css}"/>
</head>
<body>
<h1>Creating Mixins: ClickOnce (1)</h1>
<noscript class="js-required">
${message:javascript_required}
</noscript>
This page demonstrates another custom Mixin. It's a Mixin that tackles a classic problem on the web: how to
prevent <strong>duplicate submissions</strong> caused by additional clicks after a page has been submitted and before
the response has come back. The ClickOnce mixin can be mixed in to the Submit, EventLink, and ActionLink components.<br/><br/>
<strong>Without the ClickOnce Mixin.</strong> Here's an example of the duplicate submissions problem.<br/>
See how you can easily order more than 1 item by clicking impatiently on any or all of these elements...
<div class="eg">
<form t:type="form" t:id="plainForm">
<input t:type="submit" value="Order 1 Apple"/>
<a t:type="eventlink" t:event="orderOneOrange" href="#">Order 1 Orange</a>
<a t:type="actionlink" t:id="orderOneBanana" href="#">Order 1 Banana</a>
</form>
</div>
<strong>With the ClickOnce Mixin.</strong> The Mixin uses JavaScript to ignore clicks after the first one.<br/>
See how the mixin prevents ordering more than 1 item...<br/><br/>
<strong>WARNING: </strong>This solution has problems. It might not work on the submit button in some versions of Internet Explorer,
and it may have problems when used in a form that has validated fields and client-side validation enabled.
See <a href="http://tapestry.1045711.n5.nabble.com/Prevent-double-submission-w-linkSubmit-tt3291904.html">
http://tapestry.1045711.n5.nabble.com/Prevent-double-submission-w-linkSubmit-tt3291904.html</a> .
<div class="eg">
<form t:type="form" t:id="mixinForm" t:clientValidation="blur">
<input t:type="submit" value="Order 1 Apple" t:mixins="clickonce"/>
<a t:type="eventlink" t:event="orderOneOrange" t:mixins="clickonce" href="#">Order 1 Orange</a>
<a t:type="actionlink" t:id="orderOneBananaWithMixin" t:mixins="clickonce" href="#">Order 1 Banana</a>
</form>
</div>
Mixin location is important. Mixins must be put in a package called <code>mixins</code> because Tapestry gives it
special treatment.<br/><br/>
References:
<a href="http://tapestry.apache.org/component-mixins.html">Component Mixins</a>,
<a href="http://wiki.apache.org/tapestry/Tapestry5AndJavaScriptExplained">Tapestry 5 and JavaScript Explained</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/corelib/components/Form.html">Form</a>,
<a href="http://tapestry.apache.org/5.3.7/apidocs/org/apache/tapestry5/annotations/Import.html">@Import</a>,
<a href="http://tapestry.apache.org/session-storage.html">Session Storage</a>.<br/><br/>
<a t:type="pagelink" t:page="Index" href="#">Home</a><br/><br/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/javascript/CreatingMixins1.tml"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/javascript/CreatingMixins1.java"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/js.css"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/ClickOnce.java"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/mixins/ClickOnce.js"/>
<t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/state/examples/javascript/MyOrder.java"/>
</body>
</html>
package jumpstart.web.pages.examples.javascript;
import jumpstart.web.state.examples.javascript.MyOrder;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.SessionState;
public class CreatingMixins1 {
// Work fields
@SessionState
@Property
private MyOrder myOrder;
// The code
void setupRender() {
myOrder.setApplesQuantity(0);
myOrder.setOrangesQuantity(0);
myOrder.setBananasQuantity(0);
}
Object onSuccessFromPlainForm() {
orderOneApple();
return CreatingMixins2.class;
}
Object onOrderOneOrange() {
orderOneOrange();
return CreatingMixins2.class;
}
Object onActionFromOrderOneBanana() {
orderOneBanana();
return CreatingMixins2.class;
}
Object onSuccessFromMixinForm() {
orderOneApple();
return CreatingMixins2.class;
}
Object onActionFromOrderOneBananaWithMixin() {
orderOneBanana();
return CreatingMixins2.class;
}
void orderOneApple() {
sleep(1500); // Sleep 1.5 seconds to simulate busy system
myOrder.setApplesQuantity(myOrder.getApplesQuantity() + 1);
}
void orderOneOrange() {
sleep(1500); // Sleep 1.5 seconds to simulate busy system
myOrder.setOrangesQuantity(myOrder.getOrangesQuantity() + 1);
}
void orderOneBanana() {
sleep(1500); // Sleep 1.5 seconds to simulate busy system
myOrder.setBananasQuantity(myOrder.getBananasQuantity() + 1);
}
private void sleep(long duration) {
try {
Thread.sleep(duration);
}
catch (InterruptedException e) {
}
}
}
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; color: #333;
line-height: 17px; }
h1 { font-size: 26px; line-height: 20px; } /* For IE 7 */
form { margin: 0; }
.eg { margin: 20px 0; padding: 20px; color: #888;
border: 1px solid #ddd; border-radius: 4px; -webkit-border-radius: 4px; -mox-border-radius: 4px; }
a { text-decoration: none; color: #3D69B6; }
a:hover { text-decoration: underline; }
/* For javascript examples. */
.js-required { color: red; display: block; margin-bottom: 14px; }
.js-recommended { color: red; display: block; margin-bottom: 14px; }
.grid { border-collapse: collapse; border-spacing: 0; border: 1px solid #dddddd; font-size: 13px; }
.grid tr.odd { background-color: #f8f8f8; }
.grid tr:hover { background-color: #eeeeee; }
.grid th { padding: 3px 5px; text-align: left; width: 130px; border: 1px solid #dddddd;
font-weight: normal; background-color: #eeeeee;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbfbfb', endColorstr='#e4e4e4'); /* for IE */
background: -webkit-gradient(linear, left top, left bottom, from(#fbfbfb), to(#e4e4e4)); /* for webkit browsers */
background: -moz-linear-gradient(top, #fbfbfb, #e4e4e4); /* for firefox 3.6+ */ }
.grid td { padding: 3px 5px; text-align:left; }
/**
* A simple mixin that uses JavaScript to observe an element, detecting whether it has been clicked. The click will be
* ignored if any element using this mixin has already been clicked.
*/
package jumpstart.web.mixins;
import org.apache.tapestry5.ClientElement;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
// The @Import tells Tapestry to put a link to the file in the head of the page so that the browser will pull it in.
@Import(library = "ClickOnce.js")
public class ClickOnce {
// Generally useful bits and pieces
@Inject
private JavaScriptSupport javaScriptSupport;
@InjectContainer
private ClientElement clientElement;
// The code
public void afterRender() {
// Tell the Tapestry.Initializer to do the initializing of a ClickOnce, which it will do when the DOM has been
// fully loaded.
JSONObject spec = new JSONObject();
spec.put("elementId", clientElement.getClientId());
javaScriptSupport.addInitializerCall("clickOnce", spec);
}
}
// A class that ignores clicks after the first one.
var alreadyClickedOnce = false;
ClickOnce = Class.create( {
initialize: function(elementId) {
Event.observe($(elementId), 'click', this.doClickOnce.bindAsEventListener(this));
},
doClickOnce: function(e) {
if (alreadyClickedOnce) {
e.stop();
}
alreadyClickedOnce = true;
}
} )
// Extend the Tapestry.Initializer with a static method that instantiates a ClickOnce.
Tapestry.Initializer.clickOnce = function(spec) {
new ClickOnce(spec.elementId);
}
package jumpstart.web.state.examples.javascript;
public class MyOrder {
private int applesQuantity;
private int orangesQuantity;
private int bananasQuantity;
public int getApplesQuantity() {
return applesQuantity;
}
public void setApplesQuantity(int applesQuantity) {
this.applesQuantity = applesQuantity;
}
public int getOrangesQuantity() {
return orangesQuantity;
}
public void setOrangesQuantity(int orangesQuantity) {
this.orangesQuantity = orangesQuantity;
}
public int getBananasQuantity() {
return bananasQuantity;
}
public void setBananasQuantity(int bananasQuantity) {
this.bananasQuantity = bananasQuantity;
}
}