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 <div> 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 <div> 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 <div> 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 <div> 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 <ul> element with
* nested <li> 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
0.1 Created the component and mixin