Creating Mixins: ClickOnce (1)

This page demonstrates another custom Mixin. It's a Mixin that tackles a classic problem on the web: how to prevent duplicate submissions 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.

Without the ClickOnce Mixin. Here's an example of the duplicate submissions problem.
See how you can easily order more than 1 item by clicking impatiently on any or all of these elements...
    Order 1 Orange    Order 1 Banana
With the ClickOnce Mixin. The Mixin uses JavaScript to ignore clicks after the first one.
See how the mixin prevents ordering more than 1 item...

WARNING: 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 http://tapestry.1045711.n5.nabble.com/Prevent-double-submission-w-linkSubmit-tt3291904.html .
    Order 1 Orange    Order 1 Banana
Mixin location is important. Mixins must be put in a package called mixins because Tapestry gives it special treatment.

References: Component Mixins, Tapestry 5 and JavaScript Explained, Form, @Import, Session Storage.

Home

CreatingMixins1.tml


<!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 &nbsp; 
     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"/>&nbsp;&nbsp;&nbsp;
            <a t:type="eventlink" t:event="orderOneOrange" href="#">Order 1 Orange</a>&nbsp;&nbsp;&nbsp;
            <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"/>&nbsp;&nbsp;&nbsp;
            <a t:type="eventlink" t:event="orderOneOrange" t:mixins="clickonce" href="#">Order 1 Orange</a>&nbsp;&nbsp;&nbsp;
            <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/apidocs/org/apache/tapestry5/corelib/components/Form.html">Form</a>, 
    <a href="http://tapestry.apache.org/5.3/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>

CreatingMixins1.java


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) {
        }
    }

}

js.css


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; }

ClickOnce.java


/**
 * 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);
    }

}

ClickOnce.js


// 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);
}

MyOrder.java


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;
    }
}