I don't think this addresses the problem though, right? If client-side form validation fails you still are in a state where you can't click the submit button again after fixing the error.
Tapestry doesn't have an event for "validation failed", and I don't know of an approved way to workaround that. But! What you can do is add an observer for Tapestry.FORM_VALIDATE_FIELDS_EVENT and set a timeout (1/4 of a second?) to re-enable the field if you don't get a Tapestry.FORM_PREPARE_FOR_SUBMIT_EVENT before the timeout. Here is a prototype based on Geoff's example from jumpstart. This was created for submit buttons, you'd have to adjust for links (find the form...) ClickOnce = Class.create( { initialize: function(elementId) { var el = $(elementId); el.clickOnce = this; if ( el['form'] ) { el.form.observe(Tapestry.FORM_VALIDATE_FIELDS_EVENT, function() { console.log("Hey, we're observing!"); el.clickOnce.clickOnceTimeout = window.setTimeout(function(){ console.log("Let them click again") el.clickOnce.alreadyClickedOnce = false; }, 250) }); el.form.observe(Tapestry.FORM_PREPARE_FOR_SUBMIT_EVENT, function() { window.clearTimeout(el.clickOnce.clickOnceTimeout); }); } this.alreadyClickedOnce = false; Event.observe(el, 'click', this.doClickOnce.bindAsEventListener(this)); }, doClickOnce: function(e) { var element = Event.element(event); console.log("Clicked"); if (element.clickOnce.alreadyClickedOnce) { console.log("and cancelled"); e.stop(); } element.clickOnce.alreadyClickedOnce = true; } } ); // Extend the Tapestry.Initializer with a static method that instantiates a ClickOnce. Tapestry.Initializer.clickOnce = function(spec) { new ClickOnce(spec.elementId); };