blob: d7645f121ad664aae1603c57300f8bf2bc1276f3 [file] [log] [blame]
// Copyright 2007, 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.
package org.apache.tapestry5.corelib.components;
import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.BeforeRenderTemplate;
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.corelib.data.BlankOption;
import org.apache.tapestry5.corelib.mixins.RenderDisabled;
import org.apache.tapestry5.internal.TapestryInternalUtils;
import org.apache.tapestry5.internal.util.SelectModelRenderer;
import org.apache.tapestry5.ioc.Messages;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.services.*;
import org.apache.tapestry5.util.EnumSelectModel;
import java.util.Locale;
/**
* Select an item from a list of values, using an [X]HTML <select> element on the client side. An validation
* decorations will go around the entire <select> element.
* <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.
*/
public final class Select extends AbstractField
{
private class Renderer extends SelectModelRenderer
{
public Renderer(MarkupWriter writer)
{
super(writer, encoder);
}
@Override
protected boolean isOptionSelected(OptionModel optionModel, String clientValue)
{
return isSelected(clientValue);
}
}
/**
* 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 encoder;
@Inject
private ComponentDefaultProvider defaultProvider;
@Inject
private FieldValidatorDefaultSource fieldValidatorDefaultSource;
@Inject
private Locale locale;
// Maybe this should default to property "<componentId>Model"?
/**
* The model used to identify the option groups and options to be presented to the user. This can be generated
* automatically for Enum types.
*/
@Parameter(required = true)
private SelectModel model;
/**
* Controls whether an additional blank option is provided. The blank option precedes all other options and is never
* selected. The value for the blank option is always the empty string, the label may be the blank string; the
* label is from the blankLabel parameter (and is often also the empty string).
*/
@Parameter(value = "auto", defaultPrefix = BindingConstants.LITERAL)
private BlankOption blankOption;
/**
* The label to use for the blank option, if rendered. If not specified, the container's message catalog is
* searched for a key, <code><em>id</em>-blanklabel</code>.
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private String blankLabel;
@Inject
private Request request;
@Inject
private ComponentResources resources;
@Environmental
private ValidationTracker tracker;
/**
* Performs input validation on the value supplied by the user in the form submission.
*/
@Parameter(defaultPrefix = "validate")
@SuppressWarnings("unchecked")
private FieldValidator<Object> validate = NOOP_VALIDATOR;
/**
* The value to read or update.
*/
@Parameter(required = true, principal = true)
private Object value;
@Inject
private FieldValidationSupport fieldValidationSupport;
@SuppressWarnings("unused")
@Mixin
private RenderDisabled renderDisabled;
private String selectedClientValue;
private boolean isSelected(String clientValue)
{
return TapestryInternalUtils.isEqual(clientValue, selectedClientValue);
}
@SuppressWarnings({ "unchecked" })
@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());
}
}
void afterRender(MarkupWriter writer)
{
writer.end();
}
void beginRender(MarkupWriter writer)
{
writer.element("select", "name", getControlName(), "id", getClientId());
validate.render(writer);
resources.renderInformalParameters(writer);
// Disabled is via a mixin
}
@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;
}
/**
* Computes a default value for the "validate" parameter using {@link FieldValidatorDefaultSource}.
*/
FieldValidator defaultValidate()
{
Class type = resources.getBoundType("value");
if (type == null) return null;
return fieldValidatorDefaultSource.createDefaultValidator(this, resources.getId(),
resources.getContainerMessages(), locale, type,
resources.getAnnotationProvider("value"));
}
Binding defaultValue()
{
return createDefaultParameterBinding("value");
}
Object defaultBlankLabel()
{
Messages containerMessages = resources.getContainerMessages();
String key = resources.getId() + "-blanklabel";
if (containerMessages.contains(key)) return containerMessages.get(key);
return null;
}
/**
* Renders the options, including the blank option.
*/
@BeforeRenderTemplate
void options(MarkupWriter writer)
{
selectedClientValue = tracker.getInput(this);
// Use the value passed up in the form submission, if available.
// Failing that, see if there is a current value (via the value parameter), and
// convert that to a client value for later comparison.
if (selectedClientValue == null) selectedClientValue = value == null ? null : encoder.toClient(value);
if (showBlankOption())
{
writer.element("option", "value", "");
writer.write(blankLabel);
writer.end();
}
SelectModelVisitor renderer = new Renderer(writer);
model.visit(renderer);
}
@Override
public boolean isRequired()
{
return validate.isRequired();
}
private boolean showBlankOption()
{
switch (blankOption)
{
case ALWAYS:
return true;
case NEVER:
return false;
default:
return !isRequired();
}
}
// For testing.
void setModel(SelectModel model)
{
this.model = model;
blankOption = BlankOption.NEVER;
}
void setValue(Object value)
{
this.value = value;
}
void setValueEncoder(ValueEncoder encoder)
{
this.encoder = encoder;
}
void setValidationTracker(ValidationTracker tracker)
{
this.tracker = tracker;
}
void setBlankOption(BlankOption option, String label)
{
blankOption = option;
blankLabel = label;
}
}