    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 &lt;select&gt; element on the client side. Any validation
    035     * decorations will go around the entire &lt;select&gt; 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    }