diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java
index 2954d16f226..2062110b087 100644
--- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java
+++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/tags/form/AbstractHtmlElementTag.java
@@ -16,20 +16,31 @@
package org.springframework.web.servlet.tags.form;
-import javax.servlet.jsp.JspException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.servlet.jsp.JspException;
+import javax.servlet.jsp.tagext.DynamicAttributes;
+
+import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for databinding-aware JSP tags that render HTML element. Provides
* a set of properties corresponding to the set of HTML attributes that are common
- * across elements.
+ * across elements.
+ *
+ *
Additionally, this base class allows for rendering non-standard attributes
+ * as part of the tag's output. These attributes are accessible to subclasses if
+ * needed via the {@link AbstractHtmlElementTag#getDynamicAttributes() dynamicAttributes}
+ * map.
*
* @author Rob Harrop
+ * @author Jeremy Grelle
* @since 2.0
*/
-public abstract class AbstractHtmlElementTag extends AbstractDataBoundFormElementTag {
+public abstract class AbstractHtmlElementTag extends AbstractDataBoundFormElementTag implements DynamicAttributes{
public static final String CLASS_ATTRIBUTE = "class";
@@ -97,6 +108,8 @@ public abstract class AbstractHtmlElementTag extends AbstractDataBoundFormElemen
private String onkeyup;
private String onkeydown;
+
+ private Map dynamicAttributes;
/**
@@ -369,7 +382,23 @@ public abstract class AbstractHtmlElementTag extends AbstractDataBoundFormElemen
protected String getOnkeydown() {
return this.onkeydown;
}
-
+
+ /**
+ * Get the map of dynamic attributes.
+ */
+ protected Map getDynamicAttributes() {
+ return this.dynamicAttributes;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setDynamicAttribute(String uri, String localName, Object value ) throws JspException {
+ if (this.dynamicAttributes == null) {
+ this.dynamicAttributes = new HashMap();
+ }
+ dynamicAttributes.put(localName, value);
+ }
/**
* Writes the default attributes configured via this base class to the supplied {@link TagWriter}.
@@ -383,6 +412,7 @@ public abstract class AbstractHtmlElementTag extends AbstractDataBoundFormElemen
/**
* Writes the optional attributes configured via this base class to the supplied {@link TagWriter}.
+ * The set of optional attributes that will be rendered includes any non-standard dynamic attributes.
* Called by {@link #writeDefaultAttributes(TagWriter)}.
*/
protected void writeOptionalAttributes(TagWriter tagWriter) throws JspException {
@@ -403,6 +433,12 @@ public abstract class AbstractHtmlElementTag extends AbstractDataBoundFormElemen
writeOptionalAttribute(tagWriter, ONKEYPRESS_ATTRIBUTE, getOnkeypress());
writeOptionalAttribute(tagWriter, ONKEYUP_ATTRIBUTE, getOnkeyup());
writeOptionalAttribute(tagWriter, ONKEYDOWN_ATTRIBUTE, getOnkeydown());
+
+ if (!CollectionUtils.isEmpty(this.dynamicAttributes)) {
+ for (String attr : this.dynamicAttributes.keySet()) {
+ tagWriter.writeOptionalAttributeValue(attr, getDisplayString(this.dynamicAttributes.get(attr)));
+ }
+ }
}
/**
diff --git a/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld b/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld
index 0726f98b4a3..953476bf59b 100644
--- a/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld
+++ b/org.springframework.web.servlet/src/main/resources/META-INF/spring-form.tld
@@ -190,6 +190,7 @@
falsetrue
+ true
@@ -383,6 +384,7 @@
falsetrue
+ true
@@ -582,6 +584,7 @@
falsetrue
+ true
@@ -794,6 +797,7 @@
falsetrue
+ true
@@ -944,6 +948,7 @@
falsetrue
+ true
@@ -1089,6 +1094,7 @@
falsetrue
+ true
@@ -1258,6 +1264,7 @@
falsetrue
+ true
@@ -1445,6 +1452,7 @@
falsetrue
+ true
@@ -1614,6 +1622,7 @@
falsetrue
+ true
@@ -1801,6 +1810,7 @@
falsetrue
+ true
@@ -1982,6 +1992,7 @@
falsetrue
+ true
@@ -2119,6 +2130,7 @@
falsetrue
+ true
@@ -2252,6 +2264,7 @@
falsetrue
+ true
\ No newline at end of file
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxTagTests.java
index 0d81c67f52a..0ad6553f742 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxTagTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxTagTests.java
@@ -39,6 +39,7 @@ import org.springframework.validation.BindingResult;
/**
* @author Rob Harrop
* @author Juergen Hoeller
+ * @author Jeremy Grelle
*/
public class CheckboxTagTests extends AbstractFormTagTests {
@@ -75,6 +76,35 @@ public class CheckboxTagTests extends AbstractFormTagTests {
assertEquals("checked", checkboxElement.attribute("checked").getValue());
assertEquals("true", checkboxElement.attribute("value").getValue());
}
+
+ public void testWithSingleValueBooleanObjectCheckedAndDynamicAttributes() throws Exception {
+ String dynamicAttribute1 = "attr1";
+ String dynamicAttribute2 = "attr2";
+
+ this.tag.setPath("someBoolean");
+ this.tag.setDynamicAttribute(null, dynamicAttribute1, dynamicAttribute1);
+ this.tag.setDynamicAttribute(null, dynamicAttribute2, dynamicAttribute2);
+
+ int result = this.tag.doStartTag();
+ assertEquals(Tag.SKIP_BODY, result);
+ String output = getOutput();
+
+ // wrap the output so it is valid XML
+ output = "" + output + "";
+
+ SAXReader reader = new SAXReader();
+ Document document = reader.read(new StringReader(output));
+ Element rootElement = document.getRootElement();
+ assertEquals("Both tag and hidden element not rendered", 2, rootElement.elements().size());
+ Element checkboxElement = (Element) rootElement.elements().get(0);
+ assertEquals("input", checkboxElement.getName());
+ assertEquals("checkbox", checkboxElement.attribute("type").getValue());
+ assertEquals("someBoolean", checkboxElement.attribute("name").getValue());
+ assertEquals("checked", checkboxElement.attribute("checked").getValue());
+ assertEquals("true", checkboxElement.attribute("value").getValue());
+ assertEquals(dynamicAttribute1, checkboxElement.attribute(dynamicAttribute1).getValue());
+ assertEquals(dynamicAttribute2, checkboxElement.attribute(dynamicAttribute2).getValue());
+ }
public void testWithSingleValueBooleanChecked() throws Exception {
this.tag.setPath("jedi");
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxesTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxesTagTests.java
index 3bf0bcfce86..cc333d7c64d 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxesTagTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/CheckboxesTagTests.java
@@ -46,6 +46,7 @@ import org.springframework.validation.BindingResult;
* @author Mark Fisher
* @author Juergen Hoeller
* @author Benjamin Hoffmann
+ * @author Jeremy Grelle
*/
public class CheckboxesTagTests extends AbstractFormTagTests {
@@ -99,6 +100,59 @@ public class CheckboxesTagTests extends AbstractFormTagTests {
assertEquals("baz", checkboxElement3.attribute("value").getValue());
assertEquals("baz", spanElement3.getStringValue());
}
+
+ public void testWithMultiValueArrayAndDynamicAttributes() throws Exception {
+ String dynamicAttribute1 = "attr1";
+ String dynamicAttribute2 = "attr2";
+
+ this.tag.setPath("stringArray");
+ this.tag.setItems(new Object[] {"foo", "bar", "baz"});
+ this.tag.setDynamicAttribute(null, dynamicAttribute1, dynamicAttribute1);
+ this.tag.setDynamicAttribute(null, dynamicAttribute2, dynamicAttribute2);
+
+ int result = this.tag.doStartTag();
+ assertEquals(Tag.SKIP_BODY, result);
+
+ String output = getOutput();
+
+ // wrap the output so it is valid XML
+ output = "" + output + "";
+ SAXReader reader = new SAXReader();
+ Document document = reader.read(new StringReader(output));
+ Element spanElement1 = (Element) document.getRootElement().elements().get(0);
+ Element checkboxElement1 = (Element) spanElement1.elements().get(0);
+ assertEquals("input", checkboxElement1.getName());
+ assertEquals("checkbox", checkboxElement1.attribute("type").getValue());
+ assertEquals("stringArray", checkboxElement1.attribute("name").getValue());
+ assertEquals("checked", checkboxElement1.attribute("checked").getValue());
+ assertEquals("foo", checkboxElement1.attribute("value").getValue());
+ assertEquals("foo", spanElement1.getStringValue());
+ assertEquals(dynamicAttribute1, checkboxElement1.attribute(dynamicAttribute1).getValue());
+ assertEquals(dynamicAttribute2, checkboxElement1.attribute(dynamicAttribute2).getValue());
+
+ Element spanElement2 = (Element) document.getRootElement().elements().get(1);
+ Element checkboxElement2 = (Element) spanElement2.elements().get(0);
+ assertEquals("input", checkboxElement2.getName());
+ assertEquals("checkbox", checkboxElement2.attribute("type").getValue());
+ assertEquals("stringArray", checkboxElement2.attribute("name").getValue());
+ assertEquals("checked", checkboxElement2.attribute("checked").getValue());
+ assertEquals("bar", checkboxElement2.attribute("value").getValue());
+ assertEquals("bar", spanElement2.getStringValue());
+ assertEquals(dynamicAttribute1, checkboxElement2.attribute(dynamicAttribute1).getValue());
+ assertEquals(dynamicAttribute2, checkboxElement2.attribute(dynamicAttribute2).getValue());
+
+ Element spanElement3 = (Element) document.getRootElement().elements().get(2);
+ Element checkboxElement3 = (Element) spanElement3.elements().get(0);
+ assertEquals("input", checkboxElement3.getName());
+ assertEquals("checkbox", checkboxElement3.attribute("type").getValue());
+ assertEquals("stringArray", checkboxElement3.attribute("name").getValue());
+ assertNull("not checked", checkboxElement3.attribute("checked"));
+ assertEquals("baz", checkboxElement3.attribute("value").getValue());
+ assertEquals("baz", spanElement3.getStringValue());
+ assertEquals(dynamicAttribute1, checkboxElement3.attribute(dynamicAttribute1).getValue());
+ assertEquals(dynamicAttribute2, checkboxElement3.attribute(dynamicAttribute2).getValue());
+
+ }
public void testWithMultiValueArrayWithDelimiter() throws Exception {
this.tag.setDelimiter(" ");
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/ErrorsTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/ErrorsTagTests.java
index 9d2e5faafc6..ccea7230e93 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/ErrorsTagTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/ErrorsTagTests.java
@@ -40,6 +40,7 @@ import org.springframework.web.servlet.tags.RequestContextAwareTag;
* @author Rick Evans
* @author Juergen Hoeller
* @author Mark Fisher
+ * @author Jeremy Grelle
*/
public class ErrorsTagTests extends AbstractFormTagTests {
@@ -159,6 +160,40 @@ public class ErrorsTagTests extends AbstractFormTagTests {
assertBlockTagContains(output, "Default Message");
assertBlockTagContains(output, "Too Short");
}
+
+ public void testWithErrorsAndDynamicAttributes() throws Exception {
+ String dynamicAttribute1 = "attr1";
+ String dynamicAttribute2 = "attr2";
+
+ this.tag.setDynamicAttribute(null, dynamicAttribute1, dynamicAttribute1);
+ this.tag.setDynamicAttribute(null, dynamicAttribute2, dynamicAttribute2);
+
+ // construct an errors instance of the tag
+ TestBean target = new TestBean();
+ target.setName("Rob Harrop");
+ Errors errors = new BeanPropertyBindingResult(target, COMMAND_NAME);
+ errors.rejectValue("name", "some.code", "Default Message");
+ errors.rejectValue("name", "too.short", "Too Short");
+
+ exposeBindingResult(errors);
+
+ int result = this.tag.doStartTag();
+ assertEquals(BodyTag.EVAL_BODY_BUFFERED, result);
+
+ result = this.tag.doEndTag();
+ assertEquals(Tag.EVAL_PAGE, result);
+
+ String output = getOutput();
+ assertElementTagOpened(output);
+ assertElementTagClosed(output);
+
+ assertContainsAttribute(output, "id", "name.errors");
+ assertContainsAttribute(output, dynamicAttribute1, dynamicAttribute1);
+ assertContainsAttribute(output, dynamicAttribute2, dynamicAttribute2);
+ assertBlockTagContains(output, " ");
+ assertBlockTagContains(output, "Default Message");
+ assertBlockTagContains(output, "Too Short");
+ }
public void testWithEscapedErrors() throws Exception {
// construct an errors instance of the tag
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/FormTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/FormTagTests.java
index 54175918a38..9bf708967f8 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/FormTagTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/FormTagTests.java
@@ -26,6 +26,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
* @author Rick Evans
* @author Juergen Hoeller
* @author Scott Andrews
+ * @author Jeremy Grelle
*/
public class FormTagTests extends AbstractHtmlElementTagTests {
@@ -68,6 +69,8 @@ public class FormTagTests extends AbstractHtmlElementTagTests {
String autocomplete = "off";
String cssClass = "myClass";
String cssStyle = "myStyle";
+ String dynamicAttribute1 = "attr1";
+ String dynamicAttribute2 = "attr2";
this.tag.setName(name);
this.tag.setCssClass(cssClass);
@@ -81,6 +84,8 @@ public class FormTagTests extends AbstractHtmlElementTagTests {
this.tag.setOnsubmit(onsubmit);
this.tag.setOnreset(onreset);
this.tag.setAutocomplete(autocomplete);
+ this.tag.setDynamicAttribute(null, dynamicAttribute1, dynamicAttribute1);
+ this.tag.setDynamicAttribute(null, dynamicAttribute2, dynamicAttribute2);
int result = this.tag.doStartTag();
assertEquals(Tag.EVAL_BODY_INCLUDE, result);
@@ -110,6 +115,8 @@ public class FormTagTests extends AbstractHtmlElementTagTests {
assertContainsAttribute(output, "autocomplete", autocomplete);
assertContainsAttribute(output, "id", commandName);
assertContainsAttribute(output, "name", name);
+ assertContainsAttribute(output, dynamicAttribute1, dynamicAttribute1);
+ assertContainsAttribute(output, dynamicAttribute2, dynamicAttribute2);
}
public void testWithActionFromRequest() throws Exception {
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/InputTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/InputTagTests.java
index b91543b7c33..73e6e699624 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/InputTagTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/InputTagTests.java
@@ -29,6 +29,7 @@ import org.springframework.web.servlet.tags.NestedPathTag;
/**
* @author Rob Harrop
* @author Rick Evans
+ * @author Jeremy Grelle
*/
public class InputTagTests extends AbstractFormTagTests {
@@ -149,6 +150,8 @@ public class InputTagTests extends AbstractFormTagTests {
String onselect = "doSelect()";
String readonly = "true";
String autocomplete = "off";
+ String dynamicAttribute1 = "attr1";
+ String dynamicAttribute2 = "attr2";
this.tag.setId(id);
this.tag.setPath("name");
@@ -179,6 +182,8 @@ public class InputTagTests extends AbstractFormTagTests {
this.tag.setOnselect(onselect);
this.tag.setReadonly(readonly);
this.tag.setAutocomplete(autocomplete);
+ this.tag.setDynamicAttribute(null, dynamicAttribute1, dynamicAttribute1);
+ this.tag.setDynamicAttribute(null, dynamicAttribute2, dynamicAttribute2);
assertEquals(Tag.SKIP_BODY, this.tag.doStartTag());
@@ -217,6 +222,8 @@ public class InputTagTests extends AbstractFormTagTests {
assertContainsAttribute(output, "onselect", onselect);
assertContainsAttribute(output, "readonly", "readonly");
assertContainsAttribute(output, "autocomplete", autocomplete);
+ assertContainsAttribute(output, dynamicAttribute1, dynamicAttribute1);
+ assertContainsAttribute(output, dynamicAttribute2, dynamicAttribute2);
}
public void testWithNestedBind() throws Exception {
diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/LabelTagTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/LabelTagTests.java
index 5cc598d1f11..ac8a8bf379e 100644
--- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/LabelTagTests.java
+++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/tags/form/LabelTagTests.java
@@ -27,6 +27,7 @@ import org.springframework.web.servlet.tags.NestedPathTag;
* @author Rob Harrop
* @author Rick Evans
* @author Juergen Hoeller
+ * @author Jeremy Grelle
*/
public class LabelTagTests extends AbstractFormTagTests {
@@ -70,6 +71,33 @@ public class LabelTagTests extends AbstractFormTagTests {
assertTrue(output.startsWith("