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-site.git
commit 13262925eeedd4be8fab693decd0a216a95b9033 Author: Lukasz Lenart <[email protected]> AuthorDate: Sun Oct 25 16:06:45 2026 +0100 WW-5811 docs: document date min/max and temporal required HTML5 constraints Co-Authored-By: Claude Opus 5.5 <[email protected]> --- source/core-developers/client-side-validation.md | 52 ++++++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/source/core-developers/client-side-validation.md b/source/core-developers/client-side-validation.md index 5432c9b141..1bb90552b4 100644 --- a/source/core-developers/client-side-validation.md +++ b/source/core-developers/client-side-validation.md @@ -59,20 +59,21 @@ whatever `type` the developer gave it. In particular: - The browsers' `email` and `url` input grammars don't match `EmailValidator` and `UrlValidator`. So `min`/`max` range constraints are only ever added to a control the developer *already* made numeric -(`type="number"` or `type="range"`) — Struts will never promote a plain text field into one just because -an `int` or `double` validator is attached to it. +(`type="number"` or `type="range"`) or a date input (`type="date"`, including `<s:datetextfield>` in the +html5 theme) — Struts will never promote a plain text field into one just because an `int`, `double` or +`date` validator is attached to it. ### Mapping table | Validator | Emits | Condition | |---|---|---| | `requiredstring` | `required` | on text-entry controls (`text`, `search`, `tel`, `password`, `email`, `url`) and `textarea` | -| `required` | `required` | only on `radio` and `file`, and only while the bound property holds a value the validator would reject (null, an empty array or an empty collection) | +| `required` | `required` | on `radio` and `file` only while the bound property holds a value the validator would reject (null, an empty array or an empty collection); on temporal controls (`date`, `month`, `week`, `time`, `datetime-local`) when the bound property is a date/time type (since 7.5.0, see below) | | `stringlength` | `minlength` / `maxlength` | on text-entry or `textarea`, and only if the validator has `trim="false"`; each attribute is added only if actually configured | | `regex` | `pattern` | on text-entry controls only, and only if `caseSensitive="true"`, `trim="false"`, the regex is ECMAScript-safe (see below), and the validator is not an `email` or `creditcard` validator (both extend `RegexFieldValidator` but carry grammars the browser does not share) | | `int`, `short`, `long` | `min` / `max` | only when the control is already `type="number"` or `type="range"` | | `double` | `min` / `max` | same as above; only inclusive bounds are emitted — exclusive bounds have no HTML equivalent and are omitted | -| `date` | — | nothing yet; temporal `min`/`max` is deferred to a future release | +| `date`, `localdate` | `min` / `max` | only on `type="date"`, as `yyyy-MM-dd` (since 7.5.0, see below); never on `month`, `week`, `time` or `datetime-local` | | `email`, `url`, `creditcard` | — | never emitted | | `fieldexpression`, `expression`, `conversion` | — | never emitted | | `visitor` | — | nothing for the visitor itself; the visited object's own validators apply to its nested fields (`user.name`) exactly as if they were declared on the action, and their messages resolve as during validation — the visited class's bundle first, then the action's, with `${...}` read from the visited object when it exists | @@ -87,12 +88,19 @@ to emit on any text-entry control. Plain `required`, however, only fails on a nu or an empty collection. That means an empty text input (which submits `""`, not nothing), a `select` with an empty-valued option, and an **unticked checkbox** (`CheckboxInterceptor` substitutes the parameter `"false"` for it) all pass server-side validation while a browser `required` attribute would block them. -Only `radio` and `file` controls omit their parameter entirely when left empty, so those are the only two -control types where plain `required` agrees with the server — which is why the table above emits `required` -for the `required` validator on those two types alone. - -Even there, one more check is needed. An omitted parameter leaves the property at whatever it already -holds, and that is the value the page is rendering. A `private int priority` bound to a `radio` list of +Only `radio` and `file` controls omit their parameter entirely when left empty, so among the controls that +submit a raw string those are the only two where plain `required` agrees with the server. + +Temporal controls are the other case. An empty date or time input submits `""` too, but when the bound +property is a `java.util.Date` (or a subclass), `Calendar`, `LocalDate`, `LocalDateTime`, `LocalTime` or +`OffsetDateTime`, the framework converts that `""` to null, so `required` then fails on the server just as +it does in the browser. Struts reads the property's declared type — through any nested path such as +`user.birthday`, even while `user` is still null — and emits `required` only for those types. A date input +bound to a `String` property keeps the `""`, passes `required`, and therefore gets no attribute. An indexed +path (`items[0].when`) or a property it cannot find gets no attribute either. + +Even for `radio` and `file`, one more check is needed. An omitted parameter leaves the property at whatever +it already holds, and that is the value the page is rendering. A `private int priority` bound to a `radio` list of 1, 2, 3 renders with nothing checked (0 is not in the list), yet the server sees a non-null `Integer` and accepts the empty submit; a `file` property that `prepare()` loaded from an existing entity — the ordinary edit flow — is accepted the same way. So `required` is emitted only while the bound value is one the @@ -132,6 +140,15 @@ trims (its default), which rejects blank input server-side, the emitted pattern `(?:<regex>)|[\x00-\x20]*`: the original regex, or a value made only of the characters `String.trim()` strips. With a trimming `requiredstring` present, the bare regex is emitted. +**Date ranges are written as plain dates, and only on `type="date"`.** A `localdate` validator's bounds +are written as they are (`2026-01-01`). A `date` validator's `java.util.Date` bounds are reduced to their +calendar date in the server's default time zone — the zone the submitted `yyyy-MM-dd` is parsed in — and +their time of day is dropped. That never makes the browser stricter than the server: with a `min` of 10:00 +on 1 January the browser lets the 1st through and the server still rejects it, and a `max` day submits as +midnight, which the server accepts. A bound outside the years 0001–9999, or of any other type, is skipped. +`month`, `week`, `time` and `datetime-local` inputs never get `min`/`max`: none of them has a built-in +converter and range validator that read their submitted value the way the browser constrains it. + ### `data-msg-*` attributes Every validator carrying a message — even one that emits no HTML constraint attribute at all — adds a @@ -170,9 +187,18 @@ conservative, but nothing stops an application from replacing it with one that f locales. A provider receives the field's validators, the kind of control being rendered, the object the validators -run against (the action, or the visited object under a `visitor` validator) and the field's current value -as the tag resolved it — the last is what lets the default implementation judge `required` against the bound -property. +run against (the action, or the visited object under a `visitor` validator), the field's current value +as the tag resolved it — what lets the default implementation judge `required` on `radio` and `file` — and, +since 7.5.0, the declared type of the property the field binds to (null when it cannot be resolved), which +decides `required` on temporal controls. The framework calls the five-argument +`constraintsFor(validators, control, action, value, propertyType)`; its default implementation drops the type +and delegates to the four-argument method, so a provider written for 7.4.0 keeps working unchanged. A +subclass of `StrutsHtmlConstraintProvider` that overrides the four-argument method, however, must move that +override to the five-argument one, because the framework no longer calls the four-argument method on it. + +An application whose own type converter turns an empty string into a value for a date/time property (a +default date, say) should override `StrutsHtmlConstraintProvider#convertsBlankToNull(Class)` to exclude that +type; otherwise the browser would insist on a value the server does not require. Two things a provider cannot do. It cannot change an input's `type` — the templates have already written it by the time the constraint map renders, so a `type` entry is discarded; treating an `email` validator
