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 }