Autocomplete Mixin

This page demonstrates the Autocomplete Mixin. It has been mixed into an ordinary TextField component.
As you type, Autocomplete issues providecompletions events which we handle by returning a list of partial matches.
Type in a country name:
Looking at the source of Autocomplete.java and autocomplete.js you will find that what it does is quite simple -
it adds a short piece of javascript to the page, which instantiates a script.aculo.us Ajax.Autocompleter:
new Ajax.Autocompleter(id_of_component, id_of_div_to_populate, url, options);
URL: The mixin populates the url with an event link to itself. The event is "autocomplete".
When the mixin handles the event, it bubbles up the provideCompletions event.
Your handler for provideCompletions returns a list, which the mixin returns to the client's Autocompleter.

Options: The mixin lets you control some options through its parameters (frequency, minChars, tokens).
To control other options, create your own mixin - a subclass of Autocomplete - and override configure(JSONObject config).

Server return: Ajax.Autocompleter requires the server to return an unordered list, which the mixin does like this:
<ul><li>AUSTRALIA</li><li>AUSTRIA</li></ul>
You are allowed to put additional information in the "server return" (see Ajax.Autocompleter).
To do so, create your own mixin - a sublcass of Autocomplete - and override generateResponseMarkup(MarkupWriter writer, List matches).
But to make use of the information you may need to add some javascript and an afterUpdateElement option that references the javascript.

For a multi-value autocomplete, try Multivalue Autocomplete for Tapestry. It uses jQuery, so refer to JumpStart's jQuery example.
For tips on styling Autocomplete, see this mailing list thread.

References: Autocomplete Mixin, Autocomplete API, Ajax.Autocompleter, Component Mixins, Defining Tapestry IOC Services.

Home

AutocompleteMixin.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>Autocomplete Mixin</h1>
    
    <noscript class="js-required">
        ${message:javascript_required}
    </noscript>     

    This page demonstrates the Autocomplete Mixin.  It has been mixed into an ordinary TextField component.<br/> 
    As you type, Autocomplete issues <em>providecompletions</em> events which we handle by returning a list of partial matches.

    <div class="eg">
        <form t:type="form">
            Type in a country name:
            <input t:id="countryName" t:type="TextField" t:mixins="autocomplete"/>
        </form>
    </div>
    
    Looking at the source of 
    <a href="http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/corelib/mixins/Autocomplete.java?view=markup">Autocomplete.java</a> and 
    <a href="http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/mixins/autocomplete.js?view=markup">autocomplete.js</a> 
    you will find that what it does is quite simple - <br/>
    it adds a short piece of javascript to the page, which instantiates a script.aculo.us <a href="http://madrobby.github.com/scriptaculous/ajax-autocompleter/">Ajax.Autocompleter</a>:
    
    <div style="padding: 14px;">
        <code>new Ajax.Autocompleter(id_of_component, id_of_div_to_populate, url, options);</code>
    </div>
    
    <strong>URL:</strong> The mixin populates the url with an event link to itself. The event is "autocomplete". <br/>
    When the mixin handles the event, it bubbles up the <em>provideCompletions</em> event.<br/>
    Your handler for <em>provideCompletions</em> returns a list, which the mixin returns to the client's Autocompleter.<br/><br/>

    <strong>Options:</strong> The mixin lets you control some options through its parameters (frequency, minChars, tokens).<br/>
    To control other options, create your own mixin - a subclass of <em>Autocomplete</em> - and override <em>configure(JSONObject config)</em>.<br/><br/>

    <strong>Server return:</strong> Ajax.Autocompleter requires the server to return an unordered list, which the mixin does like this: 

    <div style="padding: 14px;">
        <code>&lt;ul>&lt;li>AUSTRALIA&lt;/li>&lt;li>AUSTRIA&lt;/li>&lt;/ul></code>
    </div>

    You are allowed to put additional information in the "server return" (see <a href="http://madrobby.github.com/scriptaculous/ajax-autocompleter/">Ajax.Autocompleter</a>). <br/>
    To do so, create your own mixin - a sublcass of <em>Autocomplete</em> - and override <em>generateResponseMarkup(MarkupWriter writer, List matches)</em>.<br/>
    But to make use of the information you may need to add some javascript and an <em>afterUpdateElement</em> option that references the javascript.<br/><br/>
    
    For a multi-value autocomplete, try 
    <a href="http://tawus.wordpress.com/2011/12/28/multivalue-autocomplete-for-tapestry/">Multivalue Autocomplete for Tapestry</a>. 
    It uses jQuery, so refer to JumpStart's jQuery example.<br/>
    For tips on styling Autocomplete, see this <a href="http://web.archiveorange.com/archive/v/NzHgI7qCz2F9Gt8grftw">mailing list thread</a>.<br/><br/>

    References: 
    <a href="http://tapestry.apache.org/ajax-and-zones.html#AjaxandZones-autocomplete">Autocomplete Mixin</a>,
    <a href="http://tapestry.apache.org/5.3/apidocs/org/apache/tapestry5/corelib/mixins/Autocomplete.html">Autocomplete API</a>, 
    <a href="http://madrobby.github.com/scriptaculous/ajax-autocompleter/">Ajax.Autocompleter</a>, 
    <a href="http://tapestry.apache.org/component-mixins.html">Component Mixins</a>, 
    <a href="http://tapestry.apache.org/defining-tapestry-ioc-services.html">Defining Tapestry IOC Services</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/ajax/AutocompleteMixin.tml"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/pages/examples/ajax/AutocompleteMixin.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/css/examples/js.css"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/services/CountryNames.java"/>
    <t:sourcecodedisplay src="/web/src/main/java/jumpstart/web/services/AppModule.java"/>
</body>
</html>

AutocompleteMixin.java


package jumpstart.web.pages.examples.ajax;

import java.util.ArrayList;
import java.util.List;

import jumpstart.web.services.CountryNames;

import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;

public class AutocompleteMixin {

    // Screen fields

    @Property
    private String countryName;

    // Generally useful bits and pieces

    @Inject
    private CountryNames countryNames;

    // The code

    List<String> onProvideCompletionsFromCountryName(String partial) {
        List<String> matches = new ArrayList<String>();
        partial = partial.toUpperCase();

        for (String countryName : countryNames.getSet()) {
            if (countryName.contains(partial)) {
                matches.add(countryName);
            }
        }

        return matches;
    }

}

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

CountryNames.java


package jumpstart.web.services;

import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;

import jumpstart.util.StringUtil;

/**
 * Provides a sorted set of country names in upper case built from all available locales
 */

public class CountryNames {
    
    private Set<String> countryNames = new TreeSet<String>();

    public CountryNames() {
        Locale[] availableLocales = Locale.getAvailableLocales();

        for (Locale locale : availableLocales) {
            if (StringUtil.isNotEmpty(locale.getDisplayCountry())) {
                countryNames.add(locale.getDisplayCountry().toUpperCase());
            }
        }
    }

    public Set<String> getSet() {
        return Collections.unmodifiableSet(countryNames);
    }
    
}

AppModule.java


package jumpstart.web.services;

import java.util.Arrays;
import java.util.HashSet;

import jumpstart.util.JodaTimeUtil;
import jumpstart.web.translators.MoneyTranslator;
import jumpstart.web.translators.YesNoTranslator;
import jumpstart.web.validators.Letters;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.Translator;
import org.apache.tapestry5.Validator;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.EagerLoad;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import org.apache.tapestry5.ioc.services.Coercion;
import org.apache.tapestry5.ioc.services.CoercionTuple;
import org.apache.tapestry5.ioc.services.ThreadLocale;
import org.apache.tapestry5.services.BeanBlockContribution;
import org.apache.tapestry5.services.ComponentRequestFilter;
import org.apache.tapestry5.services.DisplayBlockContribution;
import org.apache.tapestry5.services.EditBlockContribution;
import org.apache.tapestry5.services.PageRenderLinkSource;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.RequestFilter;
import org.apache.tapestry5.services.security.WhitelistAnalyzer;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.upload.services.UploadSymbols;
import org.got5.tapestry5.jquery.JQuerySymbolConstants;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import org.slf4j.Logger;

/**
 * This module is automatically included as part of the Tapestry IoC Registry, it's a good place to configure and extend
 * Tapestry, or to place your own service definitions. See http://tapestry.apache.org/5.3.4/tapestry-ioc/module.html
 */
public class AppModule {
    private static final String UPLOADS_PATH = "jumpstart.upload-path";

    @Inject
    @Symbol(SymbolConstants.PRODUCTION_MODE)
    @Property(write = false)
    private static boolean productionMode;

    // Add 2 services to those provided by Tapestry.
    // - CountryNames, and SelectIdModelFactory are used by pages which ask Tapestry to @Inject them.

    public static void bind(ServiceBinder binder) {
        binder.bind(CountryNames.class);
        binder.bind(SelectIdModelFactory.class, SelectIdModelFactoryImpl.class);

        // This next line addresses an issue affecting GlassFish and JBoss - see http://blog.progs.be/?p=52
        javassist.runtime.Desc.useContextClassLoader = true;
    }

    // Tell Tapestry about our custom translators, validators, and their message files.
    // We do this by contributing configuration to Tapestry's TranslatorAlternatesSource service, FieldValidatorSource
    // service, and ComponentMessagesSource service.

    @SuppressWarnings("rawtypes")
    public static void contributeTranslatorAlternatesSource(MappedConfiguration<String, Translator> configuration,
            ThreadLocale threadLocale) {
        configuration.add("yesno", new YesNoTranslator("yesno"));
        configuration.add("money2", new MoneyTranslator("money2", 2, threadLocale));
    }

    @SuppressWarnings("rawtypes")
    public static void contributeFieldValidatorSource(MappedConfiguration<String, Validator> configuration) {
        configuration.add("letters", new Letters());
    }

    public void contributeComponentMessagesSource(OrderedConfiguration<String> configuration) {
        configuration.add("myTranslationMessages", "jumpstart/web/translators/TranslationMessages");
        configuration.add("myValidationMessages", "jumpstart/web/validators/ValidationMessages");
    }

    // Tell Tapestry about our custom ValueEncoders.
    // We do this by contributing configuration to Tapestry's ValueEncoderSource service.

    // @SuppressWarnings("rawtypes")
    // public static void contributeValueEncoderSource(MappedConfiguration<Class, Object> configuration) {
    // configuration.addInstance(Person.class, PersonEncoder.class);
    // }

    // Tell Tapestry which locales we support, and tell Tapestry5jQuery not to suppress Tapestry's built-in Prototype
    // and Scriptaculous (see the JQuery example for more information).
    // We do this by contributing configuration to Tapestry's ApplicationDefaults service.

    public static void contributeApplicationDefaults(MappedConfiguration<String, String> configuration) {
        configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en_US,en_GB,fr");
        // We have Tapestry5jQuery installed. Tell it we don't want it to suppress Prototype and Scriptaculous.
        configuration.add(JQuerySymbolConstants.SUPPRESS_PROTOTYPE, "false");
        // We don't use $j in our javascript - instead we use function scoping (see
        // http://api.jquery.com/jQuery.noConflict/)
        // but we need this next line to keep Tapestry happy (since Tapestry 5.3.4).
        configuration.add(JQuerySymbolConstants.JQUERY_ALIAS, "$j");
    }

    // Tell Tapestry how to block access to WEB-INF/, META-INF/, and assets that are not in our assets "whitelist".
    // We do this by contributing a custom RequestFilter to Tapestry's RequestHandler service.
    // - This is necessary due to https://issues.apache.org/jira/browse/TAP5-815 .
    // - RequestHandler is shown in http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview .
    // - RequestHandler is described in http://tapestry.apache.org/request-processing.html
    // - Based on an entry in the Tapestry Users mailing list by martijn.list on 15 Aug 09.

    public void contributeRequestHandler(OrderedConfiguration<RequestFilter> configuration,
            PageRenderLinkSource pageRenderLinkSource) {
        final HashSet<String> ASSETS_WHITE_LIST = new HashSet<String>(Arrays.asList("jpg", "jpeg", "png", "gif", "js",
                "css", "ico"));
        configuration.add("AssetProtectionFilter", new AssetProtectionFilter(ASSETS_WHITE_LIST, pageRenderLinkSource),
                "before:*");
    }

    // Tell Tapestry how to detect and protect pages that require security.
    // We do this by contributing a custom ComponentRequestFilter to Tapestry's ComponentRequestHandler service.
    // - ComponentRequestHandler is shown in
    // http://tapestry.apache.org/request-processing.html#RequestProcessing-Overview
    // - Based on http://tapestryjava.blogspot.com/2009/12/securing-tapestry-pages-with.html

    public void contributeComponentRequestHandler(OrderedConfiguration<ComponentRequestFilter> configuration) {
        configuration.addInstance("PageProtectionFilter", PageProtectionFilter.class);
    }

    // Tell Tapestry how to handle JBoss 7's classpath URLs - JBoss uses a "virtual file system".
    // See "Running Tapestry on JBoss" in http://wiki.apache.org/tapestry/Tapestry5HowTos .

    @SuppressWarnings("rawtypes")
    public static void contributeServiceOverride(MappedConfiguration<Class, Object> configuration) {
        configuration.add(ClasspathURLConverter.class, new ClasspathURLConverterJBoss7());
    }

    // Tell Tapestry how to handle @EJB in page and component classes.
    // We do this by contributing configuration to Tapestry's ComponentClassTransformWorker service.
    // - Based on http://wiki.apache.org/tapestry/JEE-Annotation.

    @Primary
    public static void contributeComponentClassTransformWorker(
            OrderedConfiguration<ComponentClassTransformWorker2> configuration) {
        configuration.addInstance("EJB", EJBAnnotationWorker.class, "before:Property");
    }

    // Tell Tapestry how to handle pages annotated with @WhitelistAccessOnly, eg. Tapestry's ServiceStatus and
    // PageCatalog.
    // The default WhitelistAnalyzer allows localhost only and only in non-production mode.
    // Our aim is to make the servicestatus page available to ALL clients when not in production mode.
    // We do this by contributing our own WhitelistAnalyzer to Tapestry's ClientWhitelist service.

    public static void contributeClientWhitelist(OrderedConfiguration<WhitelistAnalyzer> configuration) {
        if (!productionMode) {
            configuration.add("NonProductionWhitelistAnalyzer", new WhitelistAnalyzer() {
                @Override
                public boolean isRequestOnWhitelist(Request request) {
                    if (request.getPath().startsWith("/core/servicestatus")) {
                        return true;
                    }
                    else {
                        // This is copied from org.apache.tapestry5.internal.services.security.LocalhostOnly
                        String remoteHost = request.getRemoteHost();
                        return remoteHost.equals("localhost") || remoteHost.equals("127.0.0.1")
                                || remoteHost.equals("0:0:0:0:0:0:0:1%0") || remoteHost.equals("0:0:0:0:0:0:0:1");
                    }
                }
            }, "before:*");
        }
    }

    // Tell Tapestry how to build our Filer service (used in the FileUpload example).
    // Annotate it with EagerLoad to force resolution of symbols at startup rather than when it is first used.

    @EagerLoad
    public static IFiler buildFiler(Logger logger, @Inject @Symbol(UPLOADS_PATH) final String uploadsPath,
            @Inject @Symbol(UploadSymbols.FILESIZE_MAX) final long fileSizeMax) {
        return new Filer(logger, UPLOADS_PATH, uploadsPath, UploadSymbols.FILESIZE_MAX, fileSizeMax);
    }

    // Tell Tapestry how to coerce Joda Time types to and from Java Date types for the TypeCoercers example.
    // We do this by contributing configuration to Tapestry's TypeCoercer service.
    // - Based on http://tapestry.apache.org/typecoercer-service.html

    @SuppressWarnings("rawtypes")
    public static void contributeTypeCoercer(Configuration<CoercionTuple> configuration) {

        // From java.util.Date to DateMidnight

        Coercion<java.util.Date, DateMidnight> toDateMidnight = new Coercion<java.util.Date, DateMidnight>() {
            public DateMidnight coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toDateMidnight(input);
            }
        };

        configuration.add(new CoercionTuple<>(java.util.Date.class, DateMidnight.class, toDateMidnight));

        // From DateMidnight to java.util.Date

        Coercion<DateMidnight, java.util.Date> fromDateMidnight = new Coercion<DateMidnight, java.util.Date>() {
            public java.util.Date coerce(DateMidnight input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(DateMidnight.class, java.util.Date.class, fromDateMidnight));

        // From java.util.Date to LocalDate

        Coercion<java.util.Date, LocalDate> toLocalDate = new Coercion<java.util.Date, LocalDate>() {
            public LocalDate coerce(java.util.Date input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toLocalDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(java.util.Date.class, LocalDate.class, toLocalDate));

        // From LocalDate to java.util.Date

        Coercion<LocalDate, java.util.Date> fromLocalDate = new Coercion<LocalDate, java.util.Date>() {
            public java.util.Date coerce(LocalDate input) {
                // TODO - confirm this conversion always works, esp. across timezones
                return JodaTimeUtil.toJavaDate(input);
            }
        };

        configuration.add(new CoercionTuple<>(LocalDate.class, java.util.Date.class, fromLocalDate));
    }

    // Tell Tapestry how its BeanDisplay and BeanEditor can handle the JodaTime types.
    // We do this by contributing configuration to Tapestry's DefaultDataTypeAnalyzer and BeanBlockSource services.
    // - Based on http://tapestry.apache.org/beaneditform-guide.html .

    public static void contributeDefaultDataTypeAnalyzer(
            @SuppressWarnings("rawtypes") MappedConfiguration<Class, String> configuration) {
        configuration.add(DateTime.class, "dateTime");
        configuration.add(DateMidnight.class, "dateMidnight");
        configuration.add(LocalDateTime.class, "localDateTime");
        configuration.add(LocalDate.class, "localDate");
        configuration.add(LocalTime.class, "localTime");
    }

    public static void contributeBeanBlockSource(Configuration<BeanBlockContribution> configuration) {

        configuration.add(new DisplayBlockContribution("dateTime", "infra/AppPropertyDisplayBlocks", "dateTime"));
        configuration
                .add(new DisplayBlockContribution("dateMidnight", "infra/AppPropertyDisplayBlocks", "dateMidnight"));
        configuration.add(new DisplayBlockContribution("localDateTime", "infra/AppPropertyDisplayBlocks",
                "localDateTime"));
        configuration.add(new DisplayBlockContribution("localDate", "infra/AppPropertyDisplayBlocks", "localDate"));
        configuration.add(new DisplayBlockContribution("localTime", "infra/AppPropertyDisplayBlocks", "localTime"));

        configuration.add(new EditBlockContribution("dateMidnight", "infra/AppPropertyEditBlocks", "dateMidnight"));
        configuration.add(new EditBlockContribution("localDate", "infra/AppPropertyEditBlocks", "localDate"));

    }

}