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 91c250a98 WW-5778 feat(tags): support datetextfield in the html5 theme
91c250a98 is described below
commit 91c250a98dfef9b9db95d0ee5f3176f90abd92dd
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 24 15:09:15 2026 +0200
WW-5778 feat(tags): support datetextfield in the html5 theme
The html5 theme renders a native input type="date" instead of the
"works only with the JavaTemplates Plugin" placeholder. A java.util.Date
value is rendered as ISO yyyy-MM-dd in the server's default time zone;
any other value is rendered as-is so a conversion error re-renders the
submitted string. The format attribute is now optional: it only lays out
the JavaTemplates split fields and the html5 theme ignores it.
DateTextField reports the DATE control type, so derived constraints
apply. A required validator yields only data-msg-required: an empty
date input submits "", and emitting required for temporal controls
needs the bound type (WW-5811).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/struts2/components/DateTextField.java | 21 +++-
.../resources/template/html5/datetextfield.ftl | 31 +++++-
.../resources/tags/datetextfield-attributes.html | 4 +-
.../resources/tags/datetextfield-description.html | 2 +-
.../struts2/components/ConstraintAction.java | 12 +++
.../struts2/views/jsp/ui/DateTextFieldTest.java | 118 +++++++++++++++++++++
.../views/jsp/ui/Html5ConstraintRenderingTest.java | 15 +++
.../components/ConstraintAction-validation.xml | 5 +
.../struts2/views/jsp/ui/DateTextField-1-html5.txt | 1 +
.../struts2/views/jsp/ui/DateTextField-2-html5.txt | 1 +
.../struts2/views/jsp/ui/DateTextField-3-html5.txt | 1 +
.../struts2/views/jsp/ui/DateTextField-4-html5.txt | 1 +
12 files changed, 206 insertions(+), 6 deletions(-)
diff --git
a/core/src/main/java/org/apache/struts2/components/DateTextField.java
b/core/src/main/java/org/apache/struts2/components/DateTextField.java
index 314e37638..aaa04410d 100644
--- a/core/src/main/java/org/apache/struts2/components/DateTextField.java
+++ b/core/src/main/java/org/apache/struts2/components/DateTextField.java
@@ -24,10 +24,17 @@ import jakarta.servlet.http.HttpServletResponse;
import org.apache.struts2.views.annotations.StrutsTag;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Date;
+
+import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
+
@StrutsTag(
name="datetextfield",
tldTagClass="org.apache.struts2.views.jsp.ui.DateTextFieldTag",
- description="Render an HTML input fields with the date time",
+ description="Render an HTML input for a date: a native input type=\"date\"
in the html5 theme,"
+ + " split input fields with the JavaTemplates plugin",
allowDynamicAttributes=true)
public class DateTextField extends UIBean {
/**
@@ -51,9 +58,19 @@ public class DateTextField extends UIBean {
if (format != null) {
addParameter("format", findValue(format, String.class));
}
+
+ if (getAttributes().get(ATTR_NAME_VALUE) instanceof Date date) {
+ addParameter("isoDate",
ISO_LOCAL_DATE.format(Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault())));
+ }
+ }
+
+ @Override
+ protected HtmlControlType getControlType() {
+ return HtmlControlType.DATE;
}
- @StrutsTagAttribute(description="Date format attribute", required=true)
+ @StrutsTagAttribute(description="Date format used to lay out the split
input fields of the JavaTemplates plugin;"
+ + " ignored by the html5 theme, which always uses yyyy-MM-dd")
public void setFormat(String format) {
this.format = format;
}
diff --git a/core/src/main/resources/template/html5/datetextfield.ftl
b/core/src/main/resources/template/html5/datetextfield.ftl
index 25f198f44..35845fc48 100644
--- a/core/src/main/resources/template/html5/datetextfield.ftl
+++ b/core/src/main/resources/template/html5/datetextfield.ftl
@@ -18,4 +18,33 @@
* under the License.
*/
-->
-<div>Tag <pre style="display: inline-block"><s:datetextfield/></pre>
works only with the JavaTemplates Plugin!</div>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/controlheader.ftl"
/><#rt/>
+<input<#rt/>
+ type="date"<#rt/>
+ name="${(attributes.name!"")}"<#rt/>
+<#if attributes.isoDate??>
+ value="${attributes.isoDate}"<#rt/>
+<#elseif attributes.nameValue??>
+ value="${attributes.nameValue}"<#rt/>
+</#if>
+<#if attributes.disabled!false>
+ disabled="disabled"<#rt/>
+</#if>
+<#if attributes.readonly!false>
+ readonly="readonly"<#rt/>
+</#if>
+<#if attributes.tabindex?has_content>
+ tabindex="${attributes.tabindex}"<#rt/>
+</#if>
+<#if attributes.id?has_content>
+ id="${attributes.id}"<#rt/>
+</#if>
+<#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" />
+<#if attributes.title?has_content>
+ title="${attributes.title}"<#rt/>
+</#if>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/scripting-events.ftl"
/><#rt/>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/common-attributes.ftl"
/><#rt/>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/dynamic-attributes.ftl"
/><#rt/>
+/><#rt/>
+<#include
"/${attributes.templateDir}/${attributes.expandTheme}/controlfooter.ftl"
/><#rt/>
diff --git a/core/src/site/resources/tags/datetextfield-attributes.html
b/core/src/site/resources/tags/datetextfield-attributes.html
index e69e04ad0..496271099 100644
--- a/core/src/site/resources/tags/datetextfield-attributes.html
+++ b/core/src/site/resources/tags/datetextfield-attributes.html
@@ -71,11 +71,11 @@
</tr>
<tr>
<td class="tag-attribute">format</td>
- <td class="tag-attribute"><strong>true</strong></td>
+ <td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">String</td>
- <td class="tag-attribute">Date format attribute</td>
+ <td class="tag-attribute">Date format used to lay out the split input
fields of the JavaTemplates plugin; ignored by the html5 theme, which always
uses yyyy-MM-dd</td>
</tr>
<tr>
<td class="tag-attribute">id</td>
diff --git a/core/src/site/resources/tags/datetextfield-description.html
b/core/src/site/resources/tags/datetextfield-description.html
index a26aa1a62..34506b8e0 100644
--- a/core/src/site/resources/tags/datetextfield-description.html
+++ b/core/src/site/resources/tags/datetextfield-description.html
@@ -1 +1 @@
-Render an HTML input fields with the date time
+Render an HTML input for a date: a native input type="date" in the html5
theme, split input fields with the JavaTemplates plugin
diff --git
a/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
index 1f26af95f..8f2a5dc37 100644
--- a/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
+++ b/core/src/test/java/org/apache/struts2/components/ConstraintAction.java
@@ -21,6 +21,8 @@ package org.apache.struts2.components;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
+import java.util.Date;
+
public class ConstraintAction extends ActionSupport {
private String username;
@@ -34,6 +36,7 @@ public class ConstraintAction extends ActionSupport {
private String choice;
private int priority;
private Object attachment;
+ private Date birthday;
public String getUsername() {
return username;
@@ -106,6 +109,15 @@ public class ConstraintAction extends ActionSupport {
this.attachment = attachment;
}
+ public Date getBirthday() {
+ return birthday;
+ }
+
+ @StrutsParameter
+ public void setBirthday(Date birthday) {
+ this.birthday = birthday;
+ }
+
public ConstraintUser getOwner() {
return owner;
}
diff --git
a/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTextFieldTest.java
b/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTextFieldTest.java
new file mode 100644
index 000000000..0c177f643
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/DateTextFieldTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+package org.apache.struts2.views.jsp.ui;
+
+import org.apache.struts2.action.Action;
+import org.apache.struts2.components.ConstraintAction;
+import org.apache.struts2.views.jsp.AbstractUITagTest;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+
+public class DateTextFieldTest extends AbstractUITagTest {
+
+ private static final Date OCTOBER_24 = Date.from(LocalDate.of(2026, 10,
24).atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+ @Override
+ public Action getAction() {
+ return new ConstraintAction();
+ }
+
+ public void testGenericHtml5() throws Exception {
+ DateTextFieldTag tag = new DateTextFieldTag();
+ verifyGenericProperties(tag, "html5", new String[]{"value"});
+ }
+
+ public void testHtml5RendersTheDateAsIsoIgnoringFormat() throws Exception {
+ ((ConstraintAction) action).setBirthday(OCTOBER_24);
+
+ DateTextFieldTag tag = html5Tag();
+ tag.setFormat("MM/dd/yyyy");
+ tag.doStartTag();
+ tag.doEndTag();
+
+
verify(DateTextFieldTag.class.getResource("DateTextField-1-html5.txt"));
+ }
+
+ public void testHtml5RendersSqlDateAsIso() throws Exception {
+ ((ConstraintAction)
action).setBirthday(java.sql.Date.valueOf(LocalDate.of(2026, 10, 24)));
+
+ DateTextFieldTag tag = html5Tag();
+ tag.doStartTag();
+ tag.doEndTag();
+
+
verify(DateTextFieldTag.class.getResource("DateTextField-1-html5.txt"));
+ }
+
+ public void testHtml5RendersNoValueWithoutADate() throws Exception {
+ DateTextFieldTag tag = html5Tag();
+ tag.doStartTag();
+ tag.doEndTag();
+
+
verify(DateTextFieldTag.class.getResource("DateTextField-2-html5.txt"));
+ }
+
+ /**
+ * After a conversion error the field re-renders the raw submitted string,
which must come back unchanged.
+ */
+ public void testHtml5RendersAStringValueAsIs() throws Exception {
+ DateTextFieldTag tag = html5Tag();
+ tag.setValue("%{'2026-10-24'}");
+ tag.doStartTag();
+ tag.doEndTag();
+
+
verify(DateTextFieldTag.class.getResource("DateTextField-1-html5.txt"));
+ }
+
+ public void testHtml5RendersDynamicMinAndMax() throws Exception {
+ DateTextFieldTag tag = html5Tag();
+ tag.setDynamicAttribute(null, "min", "2026-01-01");
+ tag.setDynamicAttribute(null, "max", "2026-12-31");
+ tag.doStartTag();
+ tag.doEndTag();
+
+
verify(DateTextFieldTag.class.getResource("DateTextField-3-html5.txt"));
+ }
+
+ public void testHtml5RendersCommonAttributes() throws Exception {
+ ((ConstraintAction) action).setBirthday(OCTOBER_24);
+
+ DateTextFieldTag tag = html5Tag();
+ tag.setId("dob");
+ tag.setCssClass("date");
+ tag.setCssStyle("width: 10em");
+ tag.setTitle("Date of birth");
+ tag.setTabindex("3");
+ tag.setDisabled("true");
+ tag.setOnchange("check()");
+ tag.doStartTag();
+ tag.doEndTag();
+
+
verify(DateTextFieldTag.class.getResource("DateTextField-4-html5.txt"));
+ }
+
+ private DateTextFieldTag html5Tag() {
+ DateTextFieldTag tag = new DateTextFieldTag();
+ tag.setPageContext(pageContext);
+ tag.setTheme("html5");
+ tag.setName("birthday");
+ return tag;
+ }
+}
diff --git
a/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
b/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
index 84fb7e6f7..a0fa997c4 100644
---
a/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
+++
b/core/src/test/java/org/apache/struts2/views/jsp/ui/Html5ConstraintRenderingTest.java
@@ -143,6 +143,21 @@ public class Html5ConstraintRenderingTest extends
AbstractUITagTest {
assertFalse("expected no required in: " + output,
output.contains("required=\"required\""));
}
+ /**
+ * An empty date input submits "", which only fails {@code required} when
the bound type converts it to null.
+ */
+ public void testRendersNoRequiredOnADateTextField() throws Exception {
+ String output = renderTag("true", () -> {
+ DateTextFieldTag date = new DateTextFieldTag();
+ date.setName("birthday");
+ return date;
+ });
+
+ assertTrue("expected the required message in: " + output,
+ output.contains("type=\"date\" name=\"birthday\"") &&
output.contains("data-msg-required=\"pick a date\""));
+ assertFalse("expected no required in: " + output,
output.contains("required=\"required\""));
+ }
+
public void testRendersRequiredOnAFileInput() throws Exception {
String output = renderTag("true", () -> {
FileTag file = new FileTag();
diff --git
a/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
b/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
index 1eb3dd111..260822407 100644
---
a/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
+++
b/core/src/test/resources/org/apache/struts2/components/ConstraintAction-validation.xml
@@ -63,6 +63,11 @@
<message>attach a file</message>
</field-validator>
</field>
+ <field name="birthday">
+ <field-validator type="required">
+ <message>pick a date</message>
+ </field-validator>
+ </field>
<field name="user">
<field-validator type="visitor">
<message/>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-1-html5.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-1-html5.txt
new file mode 100644
index 000000000..cef5b8202
--- /dev/null
+++
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-1-html5.txt
@@ -0,0 +1 @@
+<input type="date" name="birthday" value="2026-10-24" id="birthday"/>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-2-html5.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-2-html5.txt
new file mode 100644
index 000000000..a7d2a3b34
--- /dev/null
+++
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-2-html5.txt
@@ -0,0 +1 @@
+<input type="date" name="birthday" id="birthday"/>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-3-html5.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-3-html5.txt
new file mode 100644
index 000000000..b51cc2a28
--- /dev/null
+++
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-3-html5.txt
@@ -0,0 +1 @@
+<input type="date" name="birthday" id="birthday" min="2026-01-01"
max="2026-12-31"/>
diff --git
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-4-html5.txt
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-4-html5.txt
new file mode 100644
index 000000000..4ae3f69b5
--- /dev/null
+++
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/DateTextField-4-html5.txt
@@ -0,0 +1 @@
+<input type="date" name="birthday" value="2026-10-24" disabled="disabled"
tabindex="3" id="dob" class="date" style="width: 10em" title="Date of birth"
onchange="check()"/>