mixins
because Tapestry gives it
special treatment.WARNING: This solution has limitations. 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 http://tapestry.1045711.n5.nabble.com/Prevent-double-submission-w-linkSubmit-tt3291904.html .
References: @InjectContainer, ClientElement, Component Mixins, Tapestry JavaScript, JavaScriptSupport, RequireJS, jQuery API, Session Storage.
<!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_4.xsd">
<body class="container">
<h3>Creating Mixins: ClickOnce (1)</h3>
<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/>
<div class="eg">
<form t:type="form" t:id="mixinForm">
<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/>
<p class="alert alert-warning">
WARNING: This solution has limitations. 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> .
</p>
References:
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/annotations/InjectContainer.html">@InjectContainer</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ClientElement.html">ClientElement</a>,
<a href="http://tapestry.apache.org/component-mixins.html">Component Mixins</a>,
<a href="http://tapestry.apache.org/javascript.html">Tapestry JavaScript</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/javascript/JavaScriptSupport.html">JavaScriptSupport</a>,
<a href="http://requirejs.org">RequireJS</a>,
<a href="http://api.jquery.com">jQuery API</a>,
<a href="http://tapestry.apache.org/session-storage.html">Session Storage</a>.<br/><br/>
<t:pagelink page="Index">Home</t:pagelink><br/><br/>
<t:tabgroup>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/javascript/CreatingMixins1.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/javascript/CreatingMixins1.java"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/js.css"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/mixins/ClickOnce.java"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/modules/click-once.js"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/state/examples/javascript/MyOrder.java"/>
</t:tabgroup>
</body>
</html>
package jumpstart.web.pages.examples.javascript;
import jumpstart.web.state.examples.javascript.MyOrder;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.SessionState;
@Import(stylesheet = "css/examples/js.css")
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) {
}
}
}
.eg {
margin: 20px 0;
padding: 14px;
color: #888;
border: 1px solid #ddd;
border-radius: 6px;
-webkit-border-radius: 6px;
-mox-border-radius: 6px;
}
.js-required {
color: red;
display: block;
margin-bottom: 14px;
}
.js-recommended {
color: red;
display: block;
margin-bottom: 14px;
}
/**
* 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.InjectContainer;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
public class ClickOnce {
// Generally useful bits and pieces
@Inject
private JavaScriptSupport javaScriptSupport;
@InjectContainer
private ClientElement attachedTo;
// The code
public void afterRender() {
JSONObject spec = new JSONObject();
spec.put("elementId", attachedTo.getClientId());
javaScriptSupport.require("click-once").invoke("init").with(spec);
}
}
// A module that ignores clicks after the first one.
// For each clickable element that you want to participate, call init.
// Once any of the elements is clicked, this module will cancel subsequent clicks from any of the elements.
define([ "jquery" ], function($) {
var alreadyClickedOnce = false;
var init = function(spec) {
$element = $("#" + spec.elementId);
var doClickOnce = function() {
if (alreadyClickedOnce) {
// Cancel the original click event.
return false;
}
alreadyClickedOnce = true;
return true;
}
$element.on("click", doClickOnce);
};
return {
init : init
};
});
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;
}
}