This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 98ee4e1d6 WW-5781 fix(tags): render a single class and style on
checkboxlist and radio inputs (#2026)
98ee4e1d6 is described below
commit 98ee4e1d6e478af9dd8ee31878e5eaebd9e00506
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Oct 16 09:11:42 2026 +0200
WW-5781 fix(tags): render a single class and style on checkboxlist and
radio inputs (#2026)
checkboxlist always, and radio whenever listCssClass or listCssStyle was
set,
wrote class/style themselves and then again through css.ftl. Browsers keep
the
first duplicate attribute, so cssErrorClass and cssErrorStyle never applied.
A shared list-item-css.ftl now computes one class and one style per item:
the item value replaces the tag value, the error class is appended and the
error style applied when the field has errors, following the css.ftl rules.
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../main/resources/template/html5/checkboxlist.ftl | 16 +-----
.../src/main/resources/template/html5/radiomap.ftl | 8 +--
.../resources/template/simple/checkboxlist.ftl | 16 +-----
.../resources/template/simple/list-item-css.ftl | 41 ++++++++++++++
.../main/resources/template/simple/radiomap.ftl | 8 +--
.../freemarker/FreemarkerResultMockedTest.java | 4 ++
.../struts2/views/jsp/ui/CheckboxListTest.java | 64 ++++++++++++++++++++++
.../org/apache/struts2/views/jsp/ui/RadioTest.java | 48 ++++++++++++++++
.../apache/struts2/views/jsp/ui/CheckboxList-5.txt | 5 ++
.../apache/struts2/views/jsp/ui/CheckboxList-6.txt | 5 ++
.../apache/struts2/views/jsp/ui/CheckboxList-7.txt | 5 ++
.../org/apache/struts2/views/jsp/ui/Radio-12.txt | 4 ++
.../org/apache/struts2/views/jsp/ui/Radio-13.txt | 4 ++
13 files changed, 184 insertions(+), 44 deletions(-)
diff --git a/core/src/main/resources/template/html5/checkboxlist.ftl
b/core/src/main/resources/template/html5/checkboxlist.ftl
index 0a2550aed..680edcfc7 100644
--- a/core/src/main/resources/template/html5/checkboxlist.ftl
+++ b/core/src/main/resources/template/html5/checkboxlist.ftl
@@ -72,20 +72,7 @@
<#if attributes.disabled!false>
disabled="disabled"<#rt/>
</#if>
-<#if itemCssClass??>
- class="${itemCssClass}"<#rt/>
-<#else>
-<#if attributes.cssClass?has_content>
- class="${attributes.cssClass}"<#rt/>
-</#if>
-</#if>
-<#if itemCssStyle??>
- style="${itemCssStyle}"<#rt/>
-<#else>
-<#if attributes.cssStyle?has_content>
- style="${attributes.cssStyle}"<#rt/>
-</#if>
-</#if>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl"
/><#rt/>
<#if itemTitle??>
title="${itemTitle}"<#rt/>
<#else>
@@ -93,7 +80,6 @@
title="${attributes.title}"<#rt/>
</#if>
</#if>
-<#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl"
/><#rt/>
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl"
/><#rt/>
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl"
/><#rt/>
<#global evaluate_dynamic_attributes = true/><#t/>
diff --git a/core/src/main/resources/template/html5/radiomap.ftl
b/core/src/main/resources/template/html5/radiomap.ftl
index 5826fe47f..55fd97c95 100644
--- a/core/src/main/resources/template/html5/radiomap.ftl
+++ b/core/src/main/resources/template/html5/radiomap.ftl
@@ -79,12 +79,7 @@ file for it's localized value. This is then used as a label
-->
<#if attributes.tabindex?has_content>
tabindex="${attributes.tabindex}"<#rt/>
</#if>
-<#if itemCssClass?has_content>
- class="${itemCssClass}"<#rt/>
-</#if>
-<#if itemCssStyle?has_content>
- style="${itemCssStyle}"<#rt/>
-</#if>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl"
/><#rt/>
<#if itemTitle?has_content>
title="${itemTitle}"<#rt/>
<#else>
@@ -92,7 +87,6 @@ file for it's localized value. This is then used as a label
-->
title="${attributes.title}"<#rt/>
</#if>
</#if>
-<#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl"
/><#rt/>
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl"
/><#rt/>
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl"
/><#rt/>
<#global evaluate_dynamic_attributes = true/><#t/>
diff --git a/core/src/main/resources/template/simple/checkboxlist.ftl
b/core/src/main/resources/template/simple/checkboxlist.ftl
index 63cf52e32..7fa09ddf8 100644
--- a/core/src/main/resources/template/simple/checkboxlist.ftl
+++ b/core/src/main/resources/template/simple/checkboxlist.ftl
@@ -71,20 +71,7 @@
<#if attributes.disabled!false>
disabled="disabled"<#rt/>
</#if>
- <#if itemCssClass??>
- class="${itemCssClass}"<#rt/>
- <#else>
- <#if attributes.cssClass?has_content>
- class="${attributes.cssClass}"<#rt/>
- </#if>
- </#if>
- <#if itemCssStyle??>
- style="${itemCssStyle}"<#rt/>
- <#else>
- <#if attributes.cssStyle?has_content>
- style="${attributes.cssStyle}"<#rt/>
- </#if>
- </#if>
+ <#include
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl" />
<#if itemTitle??>
title="${itemTitle}"<#rt/>
<#else>
@@ -92,7 +79,6 @@
title="${attributes.title}"<#rt/>
</#if>
</#if>
- <#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" />
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl" />
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl" />
<#global evaluate_dynamic_attributes = true/>
diff --git a/core/src/main/resources/template/simple/list-item-css.ftl
b/core/src/main/resources/template/simple/list-item-css.ftl
new file mode 100644
index 000000000..3d67bb99f
--- /dev/null
+++ b/core/src/main/resources/template/simple/list-item-css.ftl
@@ -0,0 +1,41 @@
+<#--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<#assign hasFieldErrors = attributes.name?? && fieldErrors?? &&
fieldErrors.get(attributes.name)??/>
+<#assign hasErrorClass = hasFieldErrors && attributes.cssErrorClass??/>
+<#assign hasErrorStyle = hasFieldErrors && attributes.cssErrorStyle??/>
+<#if itemCssClass?has_content>
+ <#assign listItemClass = itemCssClass/>
+<#else>
+ <#assign listItemClass = attributes.cssClass!''/>
+</#if>
+<#if hasErrorClass>
+ <#assign listItemClass = (listItemClass + ' ' +
attributes.cssErrorClass)?trim/>
+</#if>
+<#if listItemClass?has_content>
+ class="${listItemClass}"<#rt/>
+</#if>
+<#if hasErrorStyle>
+ style="${attributes.cssErrorStyle}"<#rt/>
+<#elseif itemCssStyle?has_content>
+ style="${itemCssStyle}"<#rt/>
+<#elseif attributes.cssStyle?has_content && !hasErrorClass>
+ style="${attributes.cssStyle}"<#rt/>
+</#if>
diff --git a/core/src/main/resources/template/simple/radiomap.ftl
b/core/src/main/resources/template/simple/radiomap.ftl
index 524bc7f7f..a091d25a7 100644
--- a/core/src/main/resources/template/simple/radiomap.ftl
+++ b/core/src/main/resources/template/simple/radiomap.ftl
@@ -78,12 +78,7 @@
<#if attributes.tabindex?has_content>
tabindex="${attributes.tabindex}"<#rt/>
</#if>
-<#if itemCssClass?has_content>
- class="${itemCssClass}"<#rt/>
-</#if>
-<#if itemCssStyle?has_content>
- style="${itemCssStyle}"<#rt/>
-</#if>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/list-item-css.ftl" />
<#if itemTitle?has_content>
title="${itemTitle}"<#rt/>
<#else>
@@ -91,7 +86,6 @@
title="${attributes.title}"<#rt/>
</#if>
</#if>
-<#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" />
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl" />
<#include
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl" />
<#global evaluate_dynamic_attributes = true/>
diff --git
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
index 50e3313ae..70a3493c7 100644
---
a/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
+++
b/core/src/test/java/org/apache/struts2/views/freemarker/FreemarkerResultMockedTest.java
@@ -134,6 +134,10 @@ public class FreemarkerResultMockedTest extends
StrutsInternalTestCase {
file = new
File(ClassLoaderUtil.getResource("template/simple/radiomap.ftl",
getClass()).toURI());
EasyMock.expect(servletContext.getRealPath("/template/simple/radiomap.ftl")).andReturn(file.getAbsolutePath());
+ file = new
File(ClassLoaderUtil.getResource("template/simple/list-item-css.ftl",
getClass()).toURI());
+
EasyMock.expect(servletContext.getRealPath("/template/simple/list-item-css.ftl")).andReturn(file.getAbsolutePath());
+
EasyMock.expect(servletContext.getRealPath("/template/~~~simple/list-item-css.ftl")).andReturn(file.getAbsolutePath());
+
file = new File(ClassLoaderUtil.getResource("template/simple/css.ftl",
getClass()).toURI());
EasyMock.expect(servletContext.getRealPath("/template/simple/css.ftl")).andReturn(file.getAbsolutePath());
EasyMock.expect(servletContext.getRealPath("/template/~~~simple/css.ftl")).andReturn(file.getAbsolutePath());
diff --git
a/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java
b/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java
index 72ce56933..a16d2abc8 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/CheckboxListTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.struts2.views.jsp.ui;
+import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.TestAction;
import org.apache.struts2.views.jsp.AbstractUITagTest;
@@ -383,4 +384,67 @@ public class CheckboxListTest extends AbstractUITagTest {
"May indicate that clearTagStateForTagPoolingServers() calls
are not working properly.",
strutsBodyTagsAreReflectionEqual(tag, freshTag));
}
+
+ public void testCssClassAndStyleRenderedOnce() throws Exception {
+ CheckboxListTag tag = createStyledTag("simple");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ verify(CheckboxListTag.class.getResource("CheckboxList-5.txt"));
+ }
+
+ public void testListCssClassAndStyleReplaceTagCss() throws Exception {
+ CheckboxListTag tag = createStyledTag("simple");
+ tag.setListCssClass("'itemClass'");
+ tag.setListCssStyle("'itemstyle'");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ verify(CheckboxListTag.class.getResource("CheckboxList-6.txt"));
+ }
+
+ public void testCssErrorClassAndStyleWithFieldErrors() throws Exception {
+ ((TestAction) action).addFieldError("collection", "collection error");
+ CheckboxListTag tag = createStyledTag("simple");
+ tag.setCssErrorClass("errclass");
+ tag.setCssErrorStyle("errstyle");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ verify(CheckboxListTag.class.getResource("CheckboxList-7.txt"));
+ }
+
+ public void testCssClassAndStyleRenderedOnceHtml5() throws Exception {
+ CheckboxListTag tag = createStyledTag("html5");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ String output = writer.toString();
+ assertEquals(2, StringUtils.countMatches(output, "class=\"myClass\""));
+ assertEquals(2, StringUtils.countMatches(output, "style=\"mystyle\""));
+ }
+
+ private CheckboxListTag createStyledTag(String theme) {
+ TestAction testAction = (TestAction) action;
+ testAction.setCollection(List.of("hello"));
+ testAction.setList(new String[][]{
+ {"hello", "world"},
+ {"foo", "bar"}
+ });
+
+ CheckboxListTag tag = new CheckboxListTag();
+ tag.setPageContext(pageContext);
+ tag.setTheme(theme);
+ tag.setName("collection");
+ tag.setList("list");
+ tag.setListKey("top[0]");
+ tag.setListValue("top[1]");
+ tag.setCssClass("myClass");
+ tag.setCssStyle("mystyle");
+ return tag;
+ }
}
diff --git a/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java
b/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java
index bc86a9909..ba7913b8a 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/RadioTest.java
@@ -191,6 +191,54 @@ public class RadioTest extends AbstractUITagTest {
strutsBodyTagsAreReflectionEqual(tag, freshTag));
}
+ public void testListCssClassAndStyleReplaceTagCss() throws Exception {
+ RadioTag tag = createStyledTag("simple");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ verify(RadioTag.class.getResource("Radio-12.txt"));
+ }
+
+ public void testListCssClassAndStyleReplaceTagCssHtml5() throws Exception {
+ RadioTag tag = createStyledTag("html5");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ verify(RadioTag.class.getResource("Radio-12.txt"));
+ }
+
+ public void testCssErrorClassAndStyleWithListCssClass() throws Exception {
+ ((TestAction) action).addFieldError("status", "status error");
+ RadioTag tag = createStyledTag("simple");
+ tag.setCssErrorClass("errclass");
+ tag.setCssErrorStyle("errstyle");
+
+ tag.doStartTag();
+ tag.doEndTag();
+
+ verify(RadioTag.class.getResource("Radio-13.txt"));
+ }
+
+ private RadioTag createStyledTag(String theme) {
+ TestAction testAction = (TestAction) action;
+ testAction.setEnumList(new
ArrayList<>(Arrays.asList(SomeEnum.values())));
+ testAction.setStatus(SomeEnum.INIT);
+
+ RadioTag tag = new RadioTag();
+ tag.setTheme(theme);
+ tag.setPageContext(pageContext);
+ tag.setName("status");
+ tag.setValue("status");
+ tag.setList("enumList");
+ tag.setCssClass("myClass");
+ tag.setCssStyle("mystyle");
+ tag.setListCssClass("'itemClass'");
+ tag.setListCssStyle("'itemstyle'");
+ return tag;
+ }
+
public void testMapCheckedUsingEnum() throws Exception {
TestAction testAction = (TestAction) action;
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-5.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-5.txt
new file mode 100644
index 000000000..e6c0c54f4
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-5.txt
@@ -0,0 +1,5 @@
+<input type="checkbox" name="collection" value="hello" id="collection-1"
checked="checked" class="myClass" style="mystyle"/>
+<label for="collection-1" class="checkboxLabel">world</label>
+<input type="checkbox" name="collection" value="foo" id="collection-2"
class="myClass" style="mystyle"/>
+<label for="collection-2" class="checkboxLabel">bar</label>
+<input type="hidden" id="__multiselect_collection"
name="__multiselect_collection" value=""/>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-6.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-6.txt
new file mode 100644
index 000000000..23f46f4c0
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-6.txt
@@ -0,0 +1,5 @@
+<input type="checkbox" name="collection" value="hello" id="collection-1"
checked="checked" class="itemClass" style="itemstyle"/>
+<label for="collection-1" class="checkboxLabel">world</label>
+<input type="checkbox" name="collection" value="foo" id="collection-2"
class="itemClass" style="itemstyle"/>
+<label for="collection-2" class="checkboxLabel">bar</label>
+<input type="hidden" id="__multiselect_collection"
name="__multiselect_collection" value=""/>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-7.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-7.txt
new file mode 100644
index 000000000..0d7b86f49
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/CheckboxList-7.txt
@@ -0,0 +1,5 @@
+<input type="checkbox" name="collection" value="hello" id="collection-1"
checked="checked" class="myClass errclass" style="errstyle"/>
+<label for="collection-1" class="checkboxLabel">world</label>
+<input type="checkbox" name="collection" value="foo" id="collection-2"
class="myClass errclass" style="errstyle"/>
+<label for="collection-2" class="checkboxLabel">bar</label>
+<input type="hidden" id="__multiselect_collection"
name="__multiselect_collection" value=""/>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-12.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-12.txt
new file mode 100644
index 000000000..c561c4317
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-12.txt
@@ -0,0 +1,4 @@
+<input type="radio" name="status" id="statusINIT" checked="checked"
value="INIT" class="itemClass" style="itemstyle"/>
+<label for="statusINIT" class="myClass" style="mystyle">INIT</label>
+<input type="radio" name="status" id="statusCOMPLETED" value="COMPLETED"
class="itemClass" style="itemstyle"/>
+<label for="statusCOMPLETED" class="myClass" style="mystyle">COMPLETED</label>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-13.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-13.txt
new file mode 100644
index 000000000..8129f8c08
--- /dev/null
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/Radio-13.txt
@@ -0,0 +1,4 @@
+<input type="radio" name="status" id="statusINIT" checked="checked"
value="INIT" class="itemClass errclass" style="errstyle"/>
+<label for="statusINIT" class="myClass errclass" style="errstyle">INIT</label>
+<input type="radio" name="status" id="statusCOMPLETED" value="COMPLETED"
class="itemClass errclass" style="errstyle"/>
+<label for="statusCOMPLETED" class="myClass errclass"
style="errstyle">COMPLETED</label>