} tag renders an HTML 'select' element.
* Supports data binding to the selected option.
*
* Inner '{@code option}' tags can be rendered using one of the
* approaches supported by the OptionWriter class.
*
*
Also supports the use of nested {@link OptionTag OptionTags} or
* (typically one) nested {@link OptionsTag}.
*
*
*
* Attribute Summary
*
*
* Attribute
* Required?
* Runtime Expression?
* Description
*
*
*
*
* accesskey
* false
* true
* HTML Standard Attribute
*
*
* cssClass
* false
* true
* HTML Optional Attribute
*
*
* cssErrorClass
* false
* true
* HTML Optional Attribute. Used when the bound field has errors.
*
*
* cssStyle
* false
* true
* HTML Optional Attribute
*
*
* dir
* false
* true
* HTML Standard Attribute
*
*
* disabled
* false
* true
* HTML Optional Attribute. Setting the value of this attribute to 'true'
* will disable the HTML element.
*
*
* htmlEscape
* false
* true
* Enable/disable HTML escaping of rendered values.
*
*
* id
* false
* true
* HTML Standard Attribute
*
*
* itemLabel
* false
* true
* Name of the property mapped to the inner text of the 'option' tag
*
*
* items
* false
* true
* The Collection, Map or array of objects used to generate the inner
* 'option' tags
*
*
* itemValue
* false
* true
* Name of the property mapped to 'value' attribute of the 'option'
* tag
*
*
* lang
* false
* true
* HTML Standard Attribute
*
*
* multiple
* false
* true
* HTML Optional Attribute
*
*
* onblur
* false
* true
* HTML Event Attribute
*
*
* onchange
* false
* true
* HTML Event Attribute
*
*
* onclick
* false
* true
* HTML Event Attribute
*
*
* ondblclick
* false
* true
* HTML Event Attribute
*
*
* onfocus
* false
* true
* HTML Event Attribute
*
*
* onkeydown
* false
* true
* HTML Event Attribute
*
*
* onkeypress
* false
* true
* HTML Event Attribute
*
*
* onkeyup
* false
* true
* HTML Event Attribute
*
*
* onmousedown
* false
* true
* HTML Event Attribute
*
*
* onmousemove
* false
* true
* HTML Event Attribute
*
*
* onmouseout
* false
* true
* HTML Event Attribute
*
*
* onmouseover
* false
* true
* HTML Event Attribute
*
*
* onmouseup
* false
* true
* HTML Event Attribute
*
*
* path
* true
* true
* Path to property for data binding
*
*
* size
* false
* true
* HTML Optional Attribute
*
*
* tabindex
* false
* true
* HTML Standard Attribute
*
*
* title
* false
* true
* HTML Standard Attribute
*
*
*
*
* @author Rob Harrop
* @author Juergen Hoeller
* @since 2.0
* @see OptionTag
*/
@SuppressWarnings("serial")
public class SelectTag extends AbstractHtmlInputElementTag {
/**
* The {@link jakarta.servlet.jsp.PageContext} attribute under
* which the bound value is exposed to inner {@link OptionTag OptionTags}.
*/
public static final String LIST_VALUE_PAGE_ATTRIBUTE =
"org.springframework.web.servlet.tags.form.SelectTag.listValue";
/**
* Marker object for items that have been specified but resolve to null.
* Allows to differentiate between 'set but null' and 'not set at all'.
*/
private static final Object EMPTY = new Object();
/**
* The {@link Collection}, {@link Map} or array of objects used to generate
* the inner '{@code option}' tags.
*/
@Nullable
private Object items;
/**
* The name of the property mapped to the '{@code value}' attribute
* of the '{@code option}' tag.
*/
@Nullable
private String itemValue;
/**
* The name of the property mapped to the inner text of the
* '{@code option}' tag.
*/
@Nullable
private String itemLabel;
/**
* The value of the HTML '{@code size}' attribute rendered
* on the final '{@code select}' element.
*/
@Nullable
private String size;
/**
* Indicates whether the '{@code select}' tag allows
* multiple-selections.
*/
@Nullable
private Object multiple;
/**
* The {@link TagWriter} instance that the output is being written.
* Only used in conjunction with nested {@link OptionTag OptionTags}.
*/
@Nullable
private TagWriter tagWriter;
/**
* Set the {@link Collection}, {@link Map} or array of objects used to
* generate the inner '{@code option}' tags.
*
Required when wishing to render '{@code option}' tags from
* an array, {@link Collection} or {@link Map}.
*
Typically a runtime expression.
* @param items the items that comprise the options of this selection
*/
public void setItems(@Nullable Object items) {
this.items = (items != null ? items : EMPTY);
}
/**
* Get the value of the '{@code items}' attribute.
*
May be a runtime expression.
*/
@Nullable
protected Object getItems() {
return this.items;
}
/**
* Set the name of the property mapped to the '{@code value}'
* attribute of the '{@code option}' tag.
*
Required when wishing to render '{@code option}' tags from
* an array or {@link Collection}.
*
May be a runtime expression.
*/
public void setItemValue(String itemValue) {
this.itemValue = itemValue;
}
/**
* Get the value of the '{@code itemValue}' attribute.
*
May be a runtime expression.
*/
@Nullable
protected String getItemValue() {
return this.itemValue;
}
/**
* Set the name of the property mapped to the label (inner text) of the
* '{@code option}' tag.
*
May be a runtime expression.
*/
public void setItemLabel(String itemLabel) {
this.itemLabel = itemLabel;
}
/**
* Get the value of the '{@code itemLabel}' attribute.
*
May be a runtime expression.
*/
@Nullable
protected String getItemLabel() {
return this.itemLabel;
}
/**
* Set the value of the HTML '{@code size}' attribute rendered
* on the final '{@code select}' element.
*/
public void setSize(String size) {
this.size = size;
}
/**
* Get the value of the '{@code size}' attribute.
*/
@Nullable
protected String getSize() {
return this.size;
}
/**
* Set the value of the HTML '{@code multiple}' attribute rendered
* on the final '{@code select}' element.
*/
public void setMultiple(Object multiple) {
this.multiple = multiple;
}
/**
* Get the value of the HTML '{@code multiple}' attribute rendered
* on the final '{@code select}' element.
*/
@Nullable
protected Object getMultiple() {
return this.multiple;
}
/**
* Renders the HTML '{@code select}' tag to the supplied
* {@link TagWriter}.
*
Renders nested '{@code option}' tags if the
* {@link #setItems items} property is set, otherwise exposes the
* bound value for the nested {@link OptionTag OptionTags}.
*/
@Override
protected int writeTagContent(TagWriter tagWriter) throws JspException {
tagWriter.startTag("select");
writeDefaultAttributes(tagWriter);
if (isMultiple()) {
tagWriter.writeAttribute("multiple", "multiple");
}
tagWriter.writeOptionalAttributeValue("size", getDisplayString(evaluate("size", getSize())));
Object items = getItems();
if (items != null) {
// Items specified, but might still be empty...
if (items != EMPTY) {
Object itemsObject = evaluate("items", items);
if (itemsObject != null) {
final String selectName = getName();
String valueProperty = (getItemValue() != null ?
ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null);
String labelProperty = (getItemLabel() != null ?
ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null);
OptionWriter optionWriter =
new OptionWriter(itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape()) {
@Override
protected String processOptionValue(String resolvedValue) {
return processFieldValue(selectName, resolvedValue, "option");
}
};
optionWriter.writeOptions(tagWriter);
}
}
tagWriter.endTag(true);
writeHiddenTagIfNecessary(tagWriter);
return SKIP_BODY;
}
else {
// Using nested tags, so just expose the value in the PageContext...
tagWriter.forceBlock();
this.tagWriter = tagWriter;
this.pageContext.setAttribute(LIST_VALUE_PAGE_ATTRIBUTE, getBindStatus());
return EVAL_BODY_INCLUDE;
}
}
/**
* If using a multi-select, a hidden element is needed to make sure all
* items are correctly unselected on the server-side in response to a
* {@code null} post.
*/
private void writeHiddenTagIfNecessary(TagWriter tagWriter) throws JspException {
if (isMultiple()) {
tagWriter.startTag("input");
tagWriter.writeAttribute("type", "hidden");
String name = WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName();
tagWriter.writeAttribute("name", name);
tagWriter.writeAttribute("value", processFieldValue(name, "1", "hidden"));
tagWriter.endTag();
}
}
private boolean isMultiple() throws JspException {
Object multiple = getMultiple();
if (multiple != null) {
String stringValue = multiple.toString();
return ("multiple".equalsIgnoreCase(stringValue) || Boolean.parseBoolean(stringValue));
}
return forceMultiple();
}
/**
* Returns '{@code true}' if the bound value requires the
* resultant '{@code select}' tag to be multi-select.
*/
private boolean forceMultiple() throws JspException {
BindStatus bindStatus = getBindStatus();
Class valueType = bindStatus.getValueType();
if (valueType != null && typeRequiresMultiple(valueType)) {
return true;
}
else if (bindStatus.getEditor() != null) {
Object editorValue = bindStatus.getEditor().getValue();
if (editorValue != null && typeRequiresMultiple(editorValue.getClass())) {
return true;
}
}
return false;
}
/**
* Returns '{@code true}' for arrays, {@link Collection Collections}
* and {@link Map Maps}.
*/
private static boolean typeRequiresMultiple(Class type) {
return (type.isArray() || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type));
}
/**
* Closes any block tag that might have been opened when using
* nested {@link OptionTag options}.
*/
@Override
public int doEndTag() throws JspException {
if (this.tagWriter != null) {
this.tagWriter.endTag();
writeHiddenTagIfNecessary(this.tagWriter);
}
return EVAL_PAGE;
}
/**
* Clears the {@link TagWriter} that might have been left over when using
* nested {@link OptionTag options}.
*/
@Override
public void doFinally() {
super.doFinally();
this.tagWriter = null;
this.pageContext.removeAttribute(LIST_VALUE_PAGE_ATTRIBUTE);
}
}