001// Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.corelib.components;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.annotations.*;
019import org.apache.tapestry5.corelib.base.AbstractField;
020import org.apache.tapestry5.corelib.data.BlankOption;
021import org.apache.tapestry5.corelib.mixins.RenderDisabled;
022import org.apache.tapestry5.internal.TapestryInternalUtils;
023import org.apache.tapestry5.internal.util.CaptureResultCallback;
024import org.apache.tapestry5.internal.util.SelectModelRenderer;
025import org.apache.tapestry5.ioc.Messages;
026import org.apache.tapestry5.ioc.annotations.Inject;
027import org.apache.tapestry5.ioc.internal.util.InternalUtils;
028import org.apache.tapestry5.json.JSONObject;
029import org.apache.tapestry5.services.*;
030import org.apache.tapestry5.services.javascript.JavaScriptSupport;
031import org.apache.tapestry5.util.EnumSelectModel;
032
033/**
034 * Select an item from a list of values, using an [X]HTML <select> element on the client side. Any validation
035 * decorations will go around the entire <select> element.
036 * <p/>
037 * A core part of this component is the {@link ValueEncoder} (the encoder parameter) that is used to convert between
038 * server-side values and unique client-side strings. In some cases, a {@link ValueEncoder} can be generated automatically from
039 * the type of the value parameter. The {@link ValueEncoderSource} service provides an encoder in these situations; it
040 * can be overriden by binding the encoder parameter, or extended by contributing a {@link ValueEncoderFactory} into the
041 * service's configuration.
042 *
043 * @tapestrydoc
044 */
045@Events(
046        {EventConstants.VALIDATE, EventConstants.VALUE_CHANGED + " when 'zone' parameter is bound"})
047public class Select extends AbstractField
048{
049    public static final String CHANGE_EVENT = "change";
050
051    private class Renderer extends SelectModelRenderer
052    {
053
054        public Renderer(MarkupWriter writer)
055        {
056            super(writer, encoder);
057        }
058
059        @Override
060        protected boolean isOptionSelected(OptionModel optionModel, String clientValue)
061        {
062            return isSelected(clientValue);
063        }
064    }
065
066    /**
067     * A ValueEncoder used to convert the server-side object provided by the
068     * "value" parameter into a unique client-side string (typically an ID) and
069     * back. Note: this parameter may be OMITTED if Tapestry is configured to
070     * provide a ValueEncoder automatically for the type of property bound to
071     * the "value" parameter.
072     *
073     * @see ValueEncoderSource
074     */
075    @Parameter
076    private ValueEncoder encoder;
077
078    @Inject
079    private ComponentDefaultProvider defaultProvider;
080
081    // Maybe this should default to property "<componentId>Model"?
082    /**
083     * The model used to identify the option groups and options to be presented to the user. This can be generated
084     * automatically for Enum types.
085     */
086    @Parameter(required = true, allowNull = false)
087    private SelectModel model;
088
089    /**
090     * Controls whether an additional blank option is provided. The blank option precedes all other options and is never
091     * selected. The value for the blank option is always the empty string, the label may be the blank string; the
092     * label is from the blankLabel parameter (and is often also the empty string).
093     */
094    @Parameter(value = "auto", defaultPrefix = BindingConstants.LITERAL)
095    private BlankOption blankOption;
096
097    /**
098     * The label to use for the blank option, if rendered. If not specified, the container's message catalog is
099     * searched for a key, <code><em>id</em>-blanklabel</code>.
100     */
101    @Parameter(defaultPrefix = BindingConstants.LITERAL)
102    private String blankLabel;
103
104    @Inject
105    private Request request;
106
107    @Inject
108    private ComponentResources resources;
109
110    @Environmental
111    private ValidationTracker tracker;
112
113    /**
114     * Performs input validation on the value supplied by the user in the form submission.
115     */
116    @Parameter(defaultPrefix = BindingConstants.VALIDATE)
117    private FieldValidator<Object> validate;
118
119    /**
120     * The value to read or update.
121     */
122    @Parameter(required = true, principal = true, autoconnect = true)
123    private Object value;
124
125    /**
126     * Binding the zone parameter will cause any change of Select's value to be handled as an Ajax request that updates
127     * the
128     * indicated zone. The component will trigger the event {@link EventConstants#VALUE_CHANGED} to inform its
129     * container that Select's value has changed.
130     *
131     * @since 5.2.0
132     */
133    @Parameter(defaultPrefix = BindingConstants.LITERAL)
134    private String zone;
135
136    @Inject
137    private FieldValidationSupport fieldValidationSupport;
138
139    @Environmental
140    private FormSupport formSupport;
141
142    @Inject
143    private JavaScriptSupport javascriptSupport;
144
145    @SuppressWarnings("unused")
146    @Mixin
147    private RenderDisabled renderDisabled;
148
149    private String selectedClientValue;
150
151    private boolean isSelected(String clientValue)
152    {
153        return TapestryInternalUtils.isEqual(clientValue, selectedClientValue);
154    }
155
156    @SuppressWarnings(
157            {"unchecked"})
158    @Override
159    protected void processSubmission(String controlName)
160    {
161        String submittedValue = request.getParameter(controlName);
162
163        tracker.recordInput(this, submittedValue);
164
165        Object selectedValue = toValue(submittedValue);
166
167        putPropertyNameIntoBeanValidationContext("value");
168
169        try
170        {
171            fieldValidationSupport.validate(selectedValue, resources, validate);
172
173            value = selectedValue;
174        } catch (ValidationException ex)
175        {
176            tracker.recordError(this, ex.getMessage());
177        }
178
179        removePropertyNameFromBeanValidationContext();
180    }
181
182    void afterRender(MarkupWriter writer)
183    {
184        writer.end();
185    }
186
187    void beginRender(MarkupWriter writer)
188    {
189        writer.element("select", "name", getControlName(), "id", getClientId());
190
191        putPropertyNameIntoBeanValidationContext("value");
192
193        validate.render(writer);
194
195        removePropertyNameFromBeanValidationContext();
196
197        resources.renderInformalParameters(writer);
198
199        decorateInsideField();
200
201        // Disabled is via a mixin
202
203        if (this.zone != null)
204        {
205            Link link = resources.createEventLink(CHANGE_EVENT);
206
207            JSONObject spec = new JSONObject("selectId", getClientId(), "zoneId", zone, "url", link.toURI());
208
209            javascriptSupport.addInitializerCall("linkSelectToZone", spec);
210        }
211    }
212
213    Object onChange(@RequestParameter(value = "t:selectvalue", allowBlank = true)
214                    final String selectValue)
215    {
216        final Object newValue = toValue(selectValue);
217
218        CaptureResultCallback<Object> callback = new CaptureResultCallback<Object>();
219
220        this.resources.triggerEvent(EventConstants.VALUE_CHANGED, new Object[]
221                {newValue}, callback);
222
223        this.value = newValue;
224
225        return callback.getResult();
226    }
227
228    protected Object toValue(String submittedValue)
229    {
230        return InternalUtils.isBlank(submittedValue) ? null : this.encoder.toValue(submittedValue);
231    }
232
233    @SuppressWarnings("unchecked")
234    ValueEncoder defaultEncoder()
235    {
236        return defaultProvider.defaultValueEncoder("value", resources);
237    }
238
239    @SuppressWarnings("unchecked")
240    SelectModel defaultModel()
241    {
242        Class valueType = resources.getBoundType("value");
243
244        if (valueType == null)
245            return null;
246
247        if (Enum.class.isAssignableFrom(valueType))
248            return new EnumSelectModel(valueType, resources.getContainerMessages());
249
250        return null;
251    }
252
253    /**
254     * Computes a default value for the "validate" parameter using {@link FieldValidatorDefaultSource}.
255     */
256    Binding defaultValidate()
257    {
258        return defaultProvider.defaultValidatorBinding("value", resources);
259    }
260
261    Object defaultBlankLabel()
262    {
263        Messages containerMessages = resources.getContainerMessages();
264
265        String key = resources.getId() + "-blanklabel";
266
267        if (containerMessages.contains(key))
268            return containerMessages.get(key);
269
270        return null;
271    }
272
273    /**
274     * Renders the options, including the blank option.
275     */
276    @BeforeRenderTemplate
277    void options(MarkupWriter writer)
278    {
279        selectedClientValue = tracker.getInput(this);
280
281        // Use the value passed up in the form submission, if available.
282        // Failing that, see if there is a current value (via the value parameter), and
283        // convert that to a client value for later comparison.
284
285        if (selectedClientValue == null)
286            selectedClientValue = value == null ? null : encoder.toClient(value);
287
288        if (showBlankOption())
289        {
290            writer.element("option", "value", "");
291            writer.write(blankLabel);
292            writer.end();
293        }
294
295        SelectModelVisitor renderer = new Renderer(writer);
296
297        model.visit(renderer);
298    }
299
300    @Override
301    public boolean isRequired()
302    {
303        return validate.isRequired();
304    }
305
306    private boolean showBlankOption()
307    {
308        switch (blankOption)
309        {
310            case ALWAYS:
311                return true;
312
313            case NEVER:
314                return false;
315
316            default:
317                return !isRequired();
318        }
319    }
320
321    // For testing.
322
323    void setModel(SelectModel model)
324    {
325        this.model = model;
326        blankOption = BlankOption.NEVER;
327    }
328
329    void setValue(Object value)
330    {
331        this.value = value;
332    }
333
334    void setValueEncoder(ValueEncoder encoder)
335    {
336        this.encoder = encoder;
337    }
338
339    void setValidationTracker(ValidationTracker tracker)
340    {
341        this.tracker = tracker;
342    }
343
344    void setBlankOption(BlankOption option, String label)
345    {
346        blankOption = option;
347        blankLabel = label;
348    }
349}