Home

Version 0.1

SeletWithAutoComplete.java (AutoComplete to Objects)

// Copyright 2010 The Apache Software Foundation

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gsoc.web.components;

import org.apache.tapestry5.Binding;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.CSSClassConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.FieldValidationSupport;
import org.apache.tapestry5.FieldValidator;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.RenderSupport;
import org.apache.tapestry5.SelectModel;
import org.apache.tapestry5.ValidationException;
import org.apache.tapestry5.ValidationTracker;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Mixin;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.corelib.base.AbstractField;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.services.ComponentDefaultProvider;
import org.apache.tapestry5.services.FieldValidatorDefaultSource;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ValueEncoderSource;
import org.apache.tapestry5.util.EnumSelectModel;

import com.google.gsoc.web.mixins.MixinSelectWithAutoComplete;


/**
 * Select an item from a list of values, using an <input type="text"> element on the client side.
 * The value parameter will be editted. Then after that is start an autocomplete dynamic.
 * <p/>
 * A core part of this component is the {@link ValueEncoder} (the encoder parameter) that is used to convert between
 * server-side values and client-side strings. In many cases, a {@link ValueEncoder} can be generated automatically from
 * the type of the value parameter. The {@link ValueEncoderSource} service provides an encoder in these situations; it
 * can be overriden by binding the encoder parameter, or extended by contributing a {@link ValueEncoderFactory} into the
 * service's configuration.
 */


/**
 *
 * @author Pablo Henrique dos Reis
 *
 */
public class SelectWithAutoComplete extends AbstractField
{

        static final String PREFIX_COMPONENTE = "autocomplete:";

        /**
         * Mixin to control the selection based in Ajax.AutoCompleter of Scriptaculous
         */
        @SuppressWarnings("unused")
        @Mixin
        private MixinSelectWithAutoComplete mixinSelectWithAutoComplete;

        @Parameter(required = false, defaultPrefix = BindingConstants.LITERAL, value = "false")
        private boolean textArea;

        /**
     * Allows a specific implementation of {@link ValueEncoder} to be supplied. This is used to create client-side
     * string values for the different options.
     *
     * @see ValueEncoderSource
     */
        @Parameter
    private ValueEncoder<Object> encoder;

        /**
     * The value to read or update.
     */
    @Parameter(required = true, principal = true, autoconnect = true)
    private Object value;

    /**
     * Label to start autocomplete
     */
    @Parameter(required = false)
    private String defaultLabel;

    /**
     * Performs input validation on the value supplied by the user in the form submission.
     */
    @Parameter(defaultPrefix = BindingConstants.VALIDATE)
    private FieldValidator<Object> validate;

    @Inject
    private Request request;

    @Environmental
    private RenderSupport renderSupport;

    @Inject
    private ComponentResources resources;

    @Environmental
    private ValidationTracker tracker;

    @Inject
    private FieldValidationSupport fieldValidationSupport;

    @Inject
    private ComponentDefaultProvider defaultProvider;

    private String textValue;

        String toClient(Object value)
    {
        return encoder.toClient(value);
    }

        Object toValue(String submittedValue)
        {
                return InternalUtils.isBlank(submittedValue) ? null : this.encoder.toValue(submittedValue);
        }

        @SuppressWarnings("unchecked")
        ValueEncoder defaultEncoder()
    {
        return defaultProvider.defaultValueEncoder("value", resources);
    }

    @SuppressWarnings("unchecked")
    SelectModel defaultModel()
    {
        Class valueType = resources.getBoundType("value");

        if (valueType == null)
            return null;

        if (Enum.class.isAssignableFrom(valueType))
            return new EnumSelectModel(valueType, resources.getContainerMessages());

        return null;
    }

    /**
     * Return the id property with the first character uppercase
     *
     */
        public String getIdProperty()
        {
                String id = getClientId();
                String initial = id.substring(0, 1);

                return initial.toUpperCase() + id.substring(1);
        }

        /**
     * This is where we write the &lt;div&gt; element and the JavaScript to control the selection.
     *
     * @param writer
     */
        protected void beginRender(MarkupWriter writer)
    {
                String autoCompleteElement = PREFIX_COMPONENTE + getClientId();
                String functionRecordFields = "function recordFields"+ getIdProperty() + "(li) { ";
                functionRecordFields += "               var comboBox = $('" + getClientId() + "'); ";
                functionRecordFields += "               var element = $('" + autoCompleteElement + "'); ";
                functionRecordFields += "               addElement(comboBox, li.id, li.innerHTML); ";
                functionRecordFields += "               element.value = comboBox.textContent; ";
        functionRecordFields += " } " ;

        renderSupport.addScript(functionRecordFields);

        if(defaultLabel == null)
                defaultLabel = value != null ? value.toString() : "";


                String textValue = defaultLabel;

                // Save until needed in after()
                this.textValue = textValue;

                if(textArea)
                {
                         writer.element("textarea",

                     "name", autoCompleteElement,

                     "id", autoCompleteElement,

                     "onBlur", "verifyContent(this,"+ getClientId() + ")");

                }

                else

                {
                        writer.element("input",

                           "type", "text",

                           "name", autoCompleteElement,

                           "id", autoCompleteElement,

                           "value", textValue,

                           "onBlur", "verifyContent(this,"+ getClientId() + ")");
                }

                validate.render(writer);

                resources.renderInformalParameters(writer);

        }

        /**
     * This is where we write the &lt;div&gt; element Select to control the submitted value
     *
     * @param writer

     */
        protected void afterRender(MarkupWriter writer)
    {
        if (textValue != null && textArea) writer.write(textValue);
        writer.end(); // input or textarea

        writer.element("Select",

                               "name",  getControlName(),

                               "class", CSSClassConstants.INVISIBLE,

                               "id",  getClientId());

                writer.element("option",
                                           "value", toClient(value));

                        writer.write(textValue);

                writer.end(); //end option


        writer.end();//end select

    }

        /**
     * Computes a default value for the "validate" parameter using {@link FieldValidatorDefaultSource}.
     */
    Binding defaultValidate()
    {
        return defaultProvider.defaultValidatorBinding("value", resources);
    }

    @Override
    public boolean isRequired()
    {
        return validate.isRequired();
    }


        @Override
        protected void processSubmission(String elementName)
        {

            String submittedValue = request.getParameter(elementName);

        tracker.recordInput(this, submittedValue);

        Object selectedValue = InternalUtils.isBlank(submittedValue)
                               ? null :
                               encoder.toValue(submittedValue);

        try
        {
            fieldValidationSupport.validate(selectedValue, resources, validate);

            value = selectedValue;
        }
        catch (ValidationException ex)
        {
            tracker.recordError(this, ex.getMessage());
        }

        }



}

MixinSeletWithAutoComplete.java (Mixin to AutoComplete with Objects )

// Copyright 2010 The Apache Software Foundation

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gsoc.web.mixins;

import org.apache.tapestry5.Asset;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.CSSClassConstants;
import org.apache.tapestry5.ComponentEventCallback;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.ContentType;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.Field;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.OptionModel;
import org.apache.tapestry5.RenderSupport;
import org.apache.tapestry5.SelectModel;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.IncludeJavaScriptLibrary;
import org.apache.tapestry5.annotations.InjectContainer;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Path;
import org.apache.tapestry5.annotations.RequestParameter;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.services.TypeCoercer;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.MarkupWriterFactory;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ResponseRenderer;
import org.apache.tapestry5.util.TextStreamResponse;

/**
 * A mixin for a SelectWithAutoComplete that allows for autocompletion of text fields. This is based on Prototype's autocompleter
 * control.
 * <p/>
 * The mixin renders an (initially invisible) progress indicator after the field (it will also be after the error icon
 * most fields render). The progress indicator is made visible during the request to the server. The mixin then renders
 * a &lt;div&gt; that will be filled in on the client side with dynamically obtained selections.
 * <p/>
 *
 * The container is responsible for providing an event handler for event "providecompletions". The context will be the
 * partial input string sent from the client. The return value must be a SelectModel with the possibles options
 * <p/>
 *
 * <pre>
 * SelectModel onProvideCompletionsFromMyField(String input)
 * {
 *   return . . .;
 * }
 * </pre>
 */

/**
 *
 * @author Pablo Henrique dos Reis
 *
 */
@IncludeJavaScriptLibrary({ "${tapestry.scriptaculous}/controls.js", "selectautocomplete.js" })
@Events(EventConstants.PROVIDE_COMPLETIONS)
public class MixinSelectWithAutoComplete
{
        static final String PREFIX_COMPONENTE = "autocomplete:";

        static final String EVENT_NAME = "autocomplete";

    private static final String PARAM_NAME = "t:input";

    /**
     * The field component to which this mixin is attached.
     */
    @InjectContainer
    private Field field;

    @Inject
    private ComponentResources resources;

    @Environmental
    private RenderSupport renderSupport;

    @Inject
    private Request request;

    @Inject
    private TypeCoercer coercer;

    @Inject
    private MarkupWriterFactory factory;

    @Inject
    @Path("${tapestry.spacer-image}")
    private Asset spacerImage;

    /**
     * Overwrites the default minimum characters to trigger a server round trip (the default is 1).
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private int minChars;

    @Inject
    private ResponseRenderer responseRenderer;

    /**
     * Overrides the default check frequency for determining whether to send a server request. The default is .4
     * seconds.
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private double frequency;

    private SelectModel model;


    /**
     * Mixin afterRender phrase occurs after the component itself. This is where we write the &lt;div&gt; element and
     * the JavaScript.
     *
     * @param writer
     */
    void afterRender(MarkupWriter writer)
    {
        String id = PREFIX_COMPONENTE + field.getClientId();

        String menuId = id + ":menu";
        String loaderId = id + ":loader";

        // The spacer image is used as a placeholder, allowing CSS to determine what image
        // is actually displayed.
        writer.element("img",

                       "src", spacerImage.toClientURL(),

                       "class", "t-autoloader-icon " + CSSClassConstants.INVISIBLE,

                       "alt", "",

                       "id", loaderId);
        writer.end();

        writer.element("div",

                       "id", menuId,

                       "class", "t-autocomplete-menu");
        writer.end();

        Link link = resources.createEventLink(EVENT_NAME);


        JSONObject config = new JSONObject();
        config.put("paramName", PARAM_NAME);
        config.put("indicator", loaderId);

        if (resources.isBound("minChars")) config.put("minChars", minChars);

        if (resources.isBound("frequency")) config.put("frequency", frequency);

        String methodAfterUpdate = "recordFields" + getIdField();
        config.put("updateElement", methodAfterUpdate);


        String configString = config.toString();
        configString = configString.toString().replace("\""+methodAfterUpdate + "\"" ,methodAfterUpdate);

        // Let subclasses do more.
        configure(config);


        renderSupport.addScript(
                                "new Ajax.Autocompleter('%s', '%s', '%s', %s);", id, menuId,
                                link.toAbsoluteURI(), configString );

    }

    /**
     * Return the id field with the first character uppercase
     *
     */
    protected String getIdField() {
        String id = field.getClientId();
                String initial = id.substring(0, 1);

                return initial.toUpperCase() + id.substring(1);
    }

    /**
     * Invoked to allow subclasses to further configure the parameters passed to the JavaScript Ajax.Autocompleter
     * options. The values minChars and frequency my be pre-configured. Subclasses may override this method to
     * configure additional features of the Ajax.Autocompleter.
     * <p/>
     * <p/>
     * This implementation does nothing.
     *
     * @param config
     *            parameters object
     */
    protected void configure(JSONObject config)
    {

    }

    Object onAutocomplete(@RequestParameter(PARAM_NAME) String input)
    {

        ComponentEventCallback<SelectModel> callback = new ComponentEventCallback<SelectModel>()
        {
            public boolean handleResult(SelectModel result)
            {
                SelectModel matches = coercer.coerce(result, SelectModel.class);

                model = matches;

                return true;
            }
        };

        resources.triggerEvent(EventConstants.PROVIDE_COMPLETIONS, new Object[] { input }, callback);

        ContentType contentType = responseRenderer.findContentType(this);

        MarkupWriter writer = factory.newPartialMarkupWriter(contentType);

        generateResponseMarkup(writer, model);

        return new TextStreamResponse(contentType.toString(), writer.toString());
    }


    /**
     * Generates the markup response that will be returned to the client; this should be an &lt;ul&gt; element with
     * nested &lt;li&gt; elements. Subclasses may override this to produce more involved markup (including images and
     * CSS class attributes).
     *
     * @param writer
     *            to write the list to
     * @param model
     *            to write each option
     */
        protected void generateResponseMarkup(MarkupWriter writer, SelectModel model)
    {

        writer.element("ul");

        if(model != null)
        {
                for (OptionModel o : model.getOptions())
                {
                    writer.element("li",
                                           "id", o.getValue());
                    writer.write(o.getLabel());
                    writer.end();
                }
        }

        writer.end(); // ul
    }

}

!selectautocomplete.js (JavaScript)

// Copyright 2008 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

function clearSelect(combo) {

        combo.options[0] = new Option('','');

}

function addElement(combo, id, text) {

        var option = new Option(text, id);
        option.selected = true;
        combo.options[0] =  option;
}

function verifyContent(obj, idElement) {

        var text = obj.value;
        var idSelected = idElement;

        var selected = $(idSelected);

        if(text != selected.textContent) {
                clearSelect(selected);
                obj.value = '';
        }

}

Notes

PabloGSOC2010SelectWithAutoComplete (last edited 2010-07-12 16:48:13 by PabloGSOC2010)