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    
015    package org.apache.tapestry5.corelib.components;
016    
017    import org.apache.tapestry5.*;
018    import org.apache.tapestry5.annotations.*;
019    import org.apache.tapestry5.corelib.base.AbstractField;
020    import org.apache.tapestry5.corelib.data.BlankOption;
021    import org.apache.tapestry5.corelib.mixins.RenderDisabled;
022    import org.apache.tapestry5.internal.TapestryInternalUtils;
023    import org.apache.tapestry5.internal.util.CaptureResultCallback;
024    import org.apache.tapestry5.internal.util.SelectModelRenderer;
025    import org.apache.tapestry5.ioc.Messages;
026    import org.apache.tapestry5.ioc.annotations.Inject;
027    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
028    import org.apache.tapestry5.json.JSONObject;
029    import org.apache.tapestry5.services.*;
030    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
031    import 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"})
047    public 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    }