<!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>Augmenting Translators</h3>
The built-in translators bubble up events <em>toClient</em> and <em>parseClient</em> before doing their translation. <br/>
We can handle those events, which allows us to augment, or even override, the translator.
<div class="eg">
<t:form class="form-horizontal" t:id="inputs">
<div class="form-group">
<t:label for="primitiveWithZeroSuppressed" class="col-sm-4"/>
<div class="col-sm-2">
<t:textfield t:id="primitiveWithZeroSuppressed"/>
</div>
<div class="col-sm-6">
<p class="form-control-static">
${primitiveWithZeroSuppressed}
<span class="text-muted">${primitiveWithZeroSuppressedMessage}</span>
</p>
</div>
</div>
<div class="form-group">
<t:label for="objectDisplayingNullAsZero" class="col-sm-4"/>
<div class="col-sm-2">
<t:textfield t:id="objectDisplayingNullAsZero"/>
</div>
<div class="col-sm-6">
<p class="form-control-static">
${objectDisplayingNullAsZero}
<span class="text-muted">${objectDisplayingNullAsZeroMessage}</span>
</p>
</div>
</div>
<div class="form-group">
<t:label for="objectUsingZeroNullFieldStrategy" class="col-sm-4">
Object Using ZeroNullFieldStrategy
</t:label>
<div class="col-sm-2">
<t:textfield t:id="objectUsingZeroNullFieldStrategy" t:nulls="zero"/>
</div>
<div class="col-sm-6">
<p class="form-control-static">
${objectUsingZeroNullFieldStrategy}
<span class="text-muted">${objectUsingZeroNullFieldStrategyMessage}</span>
</p>
</div>
</div>
<div class="form-group">
<t:label for="objectAllowingShorthandInput" class="col-sm-4"/>
<div class="col-sm-2">
<t:textfield t:id="objectAllowingShorthandInput" t:nulls="zero" t:mixins="ClientTranslatorDisabler"/>
</div>
<div class="col-sm-6">
<p class="form-control-static">
${objectAllowingShorthandInput}
<span class="text-muted">(eg. 2k=2000,5m=5000000) ${objectAllowingShorthandInputMessage}</span>
</p>
</div>
</div>
<div class="form-group">
<div class="col-sm-2 col-sm-offset-4">
<t:submit/>
</div>
</div>
</t:form>
</div>
<ul>
<li>(1) Uses <em>toClient</em> to translate <em>0</em> server-side to empty string client-side.
The client-side translator will not let you leave the field empty because it knows that server-side is a <em>long</em>.</li>
<li>(2) Uses <em>toClient</em> to translate <em>null</em> server-side to <em>"0"</em> client-side.
Notice that if you empty it, <em>parseClient</em> will NOT bubble up.</li>
<li>(3) Uses <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/ZeroNullFieldStrategy.html">ZeroNullFieldStrategy</a>
which translates <em>null</em> server-side to <em>"0"</em> client-side, and empty field client-side to <em>0</em> server-side.</li>
<li>(4) Uses <em>parseClient</em> to translate shorthand input into numerics (eg. <em>"2k"</em> client-side to <em>2000</em> server-side).</li>
<li>(4) Also uses a <em>mixin</em>, to disable the client-side translator which would prevent non-numeric input.
A better mixin might be one that allows a numeric with a <em>k</em> or <em>m</em> suffix.
For more about mixins see the Mixin examples.</li>
</ul>
References:
<a href="http://tapestry.apache.org/forms-and-validation.html#FormsandValidation-OverridingtheTranslatorwithEvents">Overriding the Translator with Events</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/Translator.html">Translator</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/translator/package-summary.html">translator package</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/TextField.html">TextField</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/PasswordField.html">PasswordField</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/components/TextArea.html">TextArea</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/NullFieldStrategy.html">NullFieldStrategy</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/DefaultNullFieldStrategy.html">DefaultNullFieldStrategy</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/internal/ZeroNullFieldStrategy.html">ZeroNullFieldStrategy</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/input/AugmentingTranslators.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/input/AugmentingTranslators.java"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/plain.css"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/mixins/ClientTranslatorDisabler.java"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/modules/client-translator-disabler.js"/>
</t:tabgroup>
</body>
</html>
package jumpstart.web.pages.examples.input;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import org.apache.tapestry5.PersistenceConstants;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.TextField;
import org.apache.tapestry5.internal.translator.NumericTranslatorSupport;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
@Import(stylesheet="css/examples/plain.css")
public class AugmentingTranslators {
private static final String PARSED = "(parseClient handler was invoked)";
// Screen fields
@Property
@Persist(PersistenceConstants.FLASH)
private long primitiveWithZeroSuppressed;
@Property
@Persist(PersistenceConstants.FLASH)
private String primitiveWithZeroSuppressedMessage;
@Property
@Persist(PersistenceConstants.FLASH)
private Long objectDisplayingNullAsZero;
@Property
@Persist(PersistenceConstants.FLASH)
private String objectDisplayingNullAsZeroMessage;
@Property
@Persist(PersistenceConstants.FLASH)
private Long objectUsingZeroNullFieldStrategy;
@Property
@Persist(PersistenceConstants.FLASH)
private String objectUsingZeroNullFieldStrategyMessage;
@Property
@Persist(PersistenceConstants.FLASH)
private Long objectAllowingShorthandInput;
@Property
@Persist(PersistenceConstants.FLASH)
private String objectAllowingShorthandInputMessage;
// Generally useful bits and pieces
@Inject
private Locale locale;
@Inject
private NumericTranslatorSupport numericTranslatorSupport;
@Inject
private Messages messages;
@InjectComponent("objectAllowingShorthandInput")
private TextField objectAllowingShorthandInputField;
// The code
/* 1st field. */
String onToClientFromPrimitiveWithZeroSuppressed() {
if (primitiveWithZeroSuppressed == 0) {
return "";
}
else {
// Return control to the normal translator.
return null;
}
}
Object onParseClientFromPrimitiveWithZeroSuppressed(String input) {
// We included this handler only to set the message. Return control to the normal translator.
primitiveWithZeroSuppressedMessage = PARSED;
return null;
}
/* 2nd field. */
String onToClientFromObjectDisplayingNullAsZero() {
if (objectDisplayingNullAsZero == null) {
return "0";
}
else {
// Return control to the normal translator.
return null;
}
}
Object onParseClientFromObjectDisplayingNullAsZero(String input) {
// We included this handler only to set the message. Return control to the normal translator.
objectDisplayingNullAsZeroMessage = PARSED;
return null;
}
/* 3rd field. */
Object onParseClientFromObjectUsingZeroNullFieldStrategy(String input) {
// We included this handler only to set the message. Return control to the normal translator.
objectUsingZeroNullFieldStrategyMessage = PARSED;
return null;
}
/* 4th field. */
Object onParseClientFromObjectAllowingShorthandInput(String input) throws ValidationException {
objectAllowingShorthandInputMessage = PARSED;
String trimmed = input.trim();
// If the trimmed input has a suffix of "k", "K", "m", or "M", then replace it with 3 or 6 zeroes.
if (trimmed.length() > 1) {
String lastChar = trimmed.substring(trimmed.length() - 1);
if (lastChar.equalsIgnoreCase("k")) {
trimmed = trimmed.substring(0, trimmed.length() - 1) + "000";
}
else if (lastChar.equalsIgnoreCase("m")) {
trimmed = trimmed.substring(0, trimmed.length() - 1) + "000000";
}
}
try {
// Convert to a canonical form, stripping out grouping separators and disallowing decimal separators.
// We can't leave this to NumericTranslatorSupport because it uses Java's NumberFormat.parse(String) which
// is very lenient.
String canonical = toCanonical(trimmed, true);
Long l = numericTranslatorSupport.parseClient(Long.class, canonical);
return l;
}
catch (ParseException e) {
String message = messages.format(numericTranslatorSupport.getMessageKey(Long.class),
objectAllowingShorthandInputField.getLabel());
throw new ValidationException(message);
}
}
/**
* This is the same pre-processing that Tapestry 5.3's client-side translator does (in tapestry.js).
*/
private String toCanonical(String s, boolean anInteger) throws ParseException {
DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
char minusSign = symbols.getMinusSign();
char groupingSeparator = symbols.getGroupingSeparator();
char decimalSeparator = symbols.getDecimalSeparator();
// Convert non-breaking space to space. Necessary for French and other locales.
if ((int) groupingSeparator == 160) {
groupingSeparator = ' ';
}
StringBuilder canonical = new StringBuilder("");
for (char ch : s.toCharArray()) {
if (ch == minusSign) {
canonical.append("-");
}
else if (ch == groupingSeparator) {
continue;
}
else if (ch == decimalSeparator) {
if (anInteger) {
throw new ParseException("Integer contains a decimal separator.", -1);
}
canonical.append(".");
}
else if (ch >= '0' && ch <= '9') {
canonical.append(ch);
}
else {
// System.out.println("ch = " + (int) ch + ", groupingSeparator = " + (int) groupingSeparator + ".");
throw new ParseException(
"Contains character other than digit, minus sign, grouping separator, or decimal separator", -1);
}
}
return canonical.toString();
}
}
.eg {
margin: 20px 0;
padding: 14px;
border: 1px solid #ddd;
border-radius: 6px;
-webkit-border-radius: 6px;
-mox-border-radius: 6px;
}
/**
* A simple mixin that can be applied to a field that has a Translator, eg. a TextField, PasswordField, or TextArea.
* It disables the field's client-side translator assigned by Tapestry.
*/
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.services.javascript.JavaScriptSupport;
public class ClientTranslatorDisabler {
// Generally useful bits and pieces
@InjectContainer
private ClientElement attachedTo;
@Inject
private JavaScriptSupport javaScriptSupport;
// The code
public void afterRender() {
javaScriptSupport.require("client-translator-disabler").with(attachedTo.getClientId());
}
}
// For a field, disables the field's default translator assigned by Tapestry.
define(["jquery"], function($) {
return function(fieldId) {
var $field = $("#" + fieldId);
$field.removeAttr("data-translation");
}
});