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 }