UI & Form Tags
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Struts 7.4.0 (released 2026-10-02) running on Apache Tomcat 11.0.26 and Java 21, the
combination that every example was built, deployed and requested on; the facts were checked on 2026-10-08 and
2026-10-10 against the Tag Developers Guide on https://struts.apache.org/tag-developers/, the Getting Started tutorial
on form tags, the Core Developer Guide pages on the checkbox and multiselect interceptors, the 7.4.0 struts2-core jar
(struts-tags.tld, the FreeMarker templates under template/, default.properties) and the 7.4.0
struts2-javatemplates-plugin. The UI tags are the half of the Struts tag library that writes HTML: unlike a generic tag,
which prints a value, a UI tag hands its attributes to a template, and the template writes the markup, the label, the
error rows and the surrounding table or div. This page walks the form tags one by one, with their attributes and the
HTML each one produced. The running scenario is the Bookshelf, a catalogue of books exposed as Struts actions; here it
is an edit form for one title (title, ISBN, price, publication date, genre, format, tags, stock flag, summary). The
generic and control tags, the themes and the template language are the subjects of other pages of this section
(Tags Overview & Generic Tags, Themes & Templates,
FreeMarker & Velocity Views). The pages on
Actions, Type Conversion, Validation,
Localization and i18n and
Double-Submit Tokens & Wait Pages are linked where they help.
What the UI Tags Do
The Tag Developers Guide describes UI tags as the tags "focused on using data" from the action or the data tags and
"displaying data in rich and reusable HTML"; all of them are driven by templates and themes. A JSP page that says
<s:textfield name="book.title"/> makes the framework do the following:
-
read the attribute values (evaluating OGNL where an attribute is an expression);
-
derive what the attributes leave out: the
idfrom the form and the name, thevaluefrom the property named byname, the label from thekey, the error list from the action’s field errors; -
pick a template (
template/<theme>/text.ftlfor a textfield) and render it with the attributes in a hash; -
write the result to the page.
The same work is done by every tag of the table below, which groups the 7.4.0 UI tags by what they render. The tag
library descriptor of the jar (/struts-tags, tlib-version 6.0, short name s) declares them next to the generic
tags; all of them accept the common attributes described later.
| Group | Tags | What they render |
|---|---|---|
Container |
|
The |
Text input |
|
|
Read-only output |
|
A |
Boolean and single choice |
|
One checkbox, or a group of radio buttons built from a list |
Multiple choice |
|
A group of checkboxes sharing one name, or a multi-select list box |
Choice lists |
|
A drop-down list, option groups inside it, and a text field with a list that fills it |
Linked and transfer lists |
|
Two dependent lists, two lists with buttons that move options between them, a list fed from a text field, and a list that can be re-ordered |
Buttons |
|
|
Anti double click |
|
Two hidden fields that carry the token (see the double submit tokens page of this section) |
Messages |
|
The lists of field errors, action errors and action messages collected in the action |
Page head |
|
A stylesheet link and the Struts JavaScript include for the |
Custom |
|
Any template of your own with the same attribute handling |
Two things are absent from that table although older books show them. The tags s:datetimepicker, s:autocompleter,
s:div and s:tabbedPanel of the Dojo-based ajax theme are not in the 7.4.0 tag library descriptor, and the jar’s
template/ directory holds four themes only: simple, xhtml, css_xhtml and html5 (checked on the jar’s file
list). The section on what changed recently comes back to them.
The Bookshelf Used on This Page
All examples below are classes, views and mappings of one WAR, g10uitags.war, deployed under the context path
/g10uitags on Tomcat 11.0.26 (the Struts BOM struts2-bom 7.4.0, struts2-core, Servlet API 6.1 and JSP API 4.0 as
provided, one StrutsPrepareAndExecuteFilter mapped to /*; see
Getting Started with Struts 2). Every "Observed" block is the literal
response of a request to that WAR, shortened only where it says so. The model is a Book bean with the properties the
form needs, and one action class serves every view:
public class CatalogAction extends ActionSupport {
private Book book = new Book(); // title, isbn, price, publishedOn, genre, format, tags, inStock, summary
private String note;
public CatalogAction() { // the "Dune" record that the form starts from
book.setTitle("Dune");
book.setIsbn("9780441172719");
book.setGenre("sf");
book.getTags().add("classic");
book.setSummary("Spice & sand");
}
@StrutsParameter(depth = 2) // lets the params interceptor fill book.title, book.tags[0], ...
public Book getBook() { return book; }
public String getNote() { return note; }
@StrutsParameter
public void setNote(String note) { this.note = note; }
public Map<String, String> getGenreMap() { /* sf -> "Science fiction", fantasy, crime */ }
public List<String> getTagChoices() { return List.of("classic", "new", "signed", "rare"); }
@Override
public void validate() {
if (book.getTitle() == null || book.getTitle().isBlank()) {
addFieldError("book.title", "Title is required");
}
if (book.getIsbn() != null && !book.getIsbn().matches("\\d{13}")) {
addFieldError("book.isbn", "ISBN needs 13 digits");
}
}
public String save() { return SUCCESS; }
}
The mapping uses one wildcard so that a view called edit.jsp is shown by page-edit and posted to save-edit, and the
OGNL allowlist is opened for the model package, the setting that the value stack page of this section explains (without
it the tags cannot read book.title, and the textfield stays empty):
<struts>
<constant name="struts.allowlist.packageNames" value="org.example.bookshelf"/>
<package name="default" extends="struts-default" namespace="/">
<action name="page-*" class="org.example.bookshelf.CatalogAction">
<result>/WEB-INF/views/{1}.jsp</result>
<result name="input">/WEB-INF/views/{1}.jsp</result>
</action>
<action name="save-*" class="org.example.bookshelf.CatalogAction" method="save">
<result name="input">/WEB-INF/views/{1}.jsp</result>
<result>/WEB-INF/views/saved.jsp</result>
</action>
</package>
</struts>
The Round Trip of a Form
A form tag is one half of a conversation; the other half is the interceptor stack. The tags write field names that the
params interceptor later reads back, hidden helper fields that the checkbox and multiselect interceptors read, and
they read the field errors that the workflow interceptor leaves in the action when it returns input.
Four consequences follow and are used below. A tag needs a name that is a path the params interceptor can set. The
value shown after a failed round trip is the submitted text, not the converted property, when the conversion failed
(the type conversion page of this section describes the mechanism). The error rows come from the action, so a view
rendered by an action other than the one that validated (for example the page that just shows an empty form) has none.
And the helper fields work only when the matching interceptor is in the stack, which defaultStack of 7.4.0 guarantees
(checkbox is the twelfth, datetime the thirteenth and multiselect the fourteenth of its interceptors, all ahead of
params, the seventeenth, as listed on the interceptors page).
The s:form Tag
s:form renders the form element and gives every tag inside it three things: the form’s id (the prefix of the ids of
the fields), the theme (the xhtml theme opens a table that the fields fill with rows), and a place for the
form-wide defaults of the label position and the tooltips. The Tag Developers Guide calls it a different kind of tag
from the others: the "behavior of the form tag is different from the elements enclosed within it".
| Attribute | Meaning and default |
|---|---|
|
The action to submit to, without the |
|
The namespace of that action; defaults to the current namespace |
|
The HTML |
|
The HTML |
|
The HTML |
|
The HTML |
|
Whether the web application’s context path is put in front of the action URL; |
|
Turns on the generated JavaScript validation of the |
|
The id of the element that receives the focus when the page has loaded (the |
|
JavaScript handlers; with |
|
The id and the name of the |
|
Theme and template selection; |
|
Layout defaults for the labelled fields inside (only |
|
The usual HTML attributes of the form element |
|
Tooltip defaults inherited by the fields ( |
|
Portlet attributes; unused outside a portlet |
any other name |
Written to the |
How the action URL is built
The Form Tag page gives the order of the decision. With no action attribute the current request decides the URL. With
an action that is an alias in the configuration the framework asks the configuration for it (in the given or the
current namespace) and builds the URL with the action extension. With an action that is not an alias in that namespace
the value is used as a URL, "separating the namespace from it". The results of seven forms, each in a view rendered by
the action page-misc on a first request that had no session cookie, make the rule concrete:
<s:form><s:textfield name="a"/></s:form>
<s:form action="save-misc"><s:textfield name="a"/></s:form>
<s:form action="save-misc" namespace="/admin"><s:textfield name="a"/></s:form>
<s:form action="nope/save-misc"><s:textfield name="a"/></s:form>
<s:form action="/save-misc.action?x=1"><s:textfield name="a"/></s:form>
<s:form action="save-misc" includeContext="false" namespace="/"><s:textfield name="a"/></s:form>
<s:form action="http://example.com/x"><s:textfield name="a"/></s:form>
Observed (the form start tags, with <sid> standing for the 32-character session id):
<form id="page_misc" name="page_misc" action="/g10uitags/page-misc.action;jsessionid=<sid>" method="post">
<form id="save_misc" name="save_misc" action="/g10uitags/save-misc.action;jsessionid=<sid>" method="post">
<form id="save_misc_jsessionid_<sid>" name="save_misc_jsessionid_<sid>" action="save-misc;jsessionid=<sid>" method="post">
<form id="save_misc_jsessionid_<sid>" name="save_misc_jsessionid_<sid>" action="nope/save-misc;jsessionid=<sid>" method="post">
<form id="save_misc" name="save_misc" action="/g10uitags/save-misc.action;jsessionid=<sid>?x=1" method="post">
<form id="save_misc" name="save_misc" action="/save-misc.action" method="post">
<form id="x" name="x" action="http://example.com/x" method="post">
Four lessons come out of the seven lines. The action name is the whole address: there is no /g10uitags/ anywhere to
write by hand, and a typo or a wrong namespace (third and fourth forms) does not fail; the tag falls back to the
literal text, which becomes a relative URL, and the page works until it is served from a different path. Second, the
;jsessionid=... part exists only on the first response, before the browser has returned the session cookie, because
the tag encodes the URL; once the cookie is known the URL is clean. A third consequence of the same fallback is visible
in the id of the third and fourth forms: the id and name are derived from the text of the URL, so the session id ended
up inside the id (save_misc_jsessionid_<sid>) and the ids of all fields in the form carry it too. Fourth,
includeContext="false" drops the context path (/save-misc.action), which is right only for an application deployed
as the root context; for any other path it posts to a different web application.
Attributes the form passes through
Any attribute that the tag library does not declare is a dynamic attribute and is written to the element. The HTML5
data-* and aria-* attributes, placeholder, required and autofocus therefore work on every tag of the library.
The view below also uses focusElement, which takes the id that the tag generated for the field (<formId>_<fieldName>
with dots turned into underscores), and s:head for the StrutsUtils script that runs the focus code:
<s:form action="save-formattrs" namespace="/" method="get" enctype="multipart/form-data" acceptcharset="UTF-8"
target="_blank" cssClass="stacked" cssStyle="width:20em" onsubmit="return true;" id="bookForm"
includeContext="false" theme="simple" data-test="42" aria-label="Edit">
<s:textfield name="book.title" tabindex="2" accesskey="t" title="Book title" placeholder="Title here"
required="true" data-x="y" autofocus="true"/>
</s:form>
Observed:
<form id="bookForm" name="bookForm" onsubmit="return true;" action="/save-formattrs.action" target="_blank" method="get"
enctype="multipart/form-data" class="stacked" style="width:20em" accept-charset="UTF-8" data-test="42"
aria-label="Edit">
<input type="text" name="book.title" value="Dune" tabindex="2" id="bookForm_book_title" title="Book title" accesskey="t"
data-x="y" placeholder="Title here" autofocus="true" required="true"/>
</form>
The element also shows that required="true" on a tag is a plain HTML attribute (the browser enforces it), not the
Struts notion of a required field: the label star of the xhtml theme needs requiredLabel="true", and the Core
Developer Guide stresses that the two are unrelated. Lower-case spellings of the layout attributes from Struts 2.0-era
documents (labelposition, requiredposition) are not recognised: they too become dynamic attributes and appear on the
element (observed on <s:form labelposition="top">, which printed labelposition="top" on the form tag while the
fields kept the default left layout); the attributes are labelPosition and requiredPosition.
Attributes Common to Every Element Tag
Every tag inside the form extends UIBean, so the attributes of the next table work on a textfield, a select or a
checkbox alike. The Tag Developers Guide sorts them into template-related, JavaScript-related and general groups; the
tooltip attributes come on top. The ones used in daily work are these:
| Attribute | Effect |
|---|---|
|
The field name, and, when |
|
A literal or an OGNL expression that overrides the property ( |
|
Takes |
|
The text of the label, its position ( |
|
Draw a star next to the label ( |
|
Put the field’s error rows above ( |
|
The HTML id; generated as |
|
The HTML attributes of the input element |
|
Class and style that are added to the input when the field has an error |
|
The HTML event attributes |
|
Select another theme or another template for this one tag |
|
The tooltip of the xhtml themes |
any other name |
Written to the input element (dynamic attributes), as |
The name and value relationship
The Tag Developers Guide explains the central rule: name is both the request parameter that the control submits and the
property that fills it. <s:textfield name="book.title"/> therefore calls getBook().getTitle() for the value and, on
submit, the params interceptor calls getBook().setTitle(...). The value attribute is optional; a template that
needs the value reads attributes.nameValue, because the key value is not what it contains. Writing both is not
wrong, but the explicit value="%{...}" hides the property, and after a failed submit the tag still shows the submitted
text for a field that has a conversion error (next sections).
Generated ids
The Guide says "all form tags automatically assign an ID to the control": a form’s id is the action name and a control’s
id is the form’s name joined to the field name. The Bookshelf views above show it: the form save_edit and the field
book.title give save_edit_book_title. Labels point to that id with for, so a label click focuses the field. Two
fields with the same name in one form get the same id (the view of the next sections that repeats book.genre has
duplicates), which is invalid HTML; pass id explicitly when a name repeats.
Label position and inheritance
labelPosition on the form is propagated to every field in it and a field’s own attribute wins (the Guide marks this
"Since 2.3.17"). A form with labelPosition="top" whose first field asks for a star on the left and a different
separator, and whose second field overrides the position, rendered these rows in the xhtml theme:
<s:form action="save-lp" labelPosition="top">
<s:textfield name="book.title" label="Title" requiredLabel="true" requiredPosition="left" labelSeparator=" -"/>
<s:textfield name="book.isbn" label="ISBN" labelPosition="left"/>
</s:form>
Observed (the table rows):
<tr>
<td class="tdLabelTop" colspan="2"><label for="save_lp_book_title" class="label"><span class="required">*</span>Title -</label></td>
</tr>
<tr>
<td class="tdInput"><input type="text" name="book.title" value="Dune" id="save_lp_book_title"/></td>
</tr>
<tr>
<td class="tdLabel"><label for="save_lp_book_isbn" class="label">ISBN:</label></td>
<td class="tdInput"><input type="text" name="book.isbn" value="9780441172719" id="save_lp_book_isbn"/></td>
</tr>
The star came from requiredLabel="true" requiredPosition="left", and labelSeparator=" -" replaced the colon; the
first field took the form’s top position (own row for the label) and the second one its own left. Without
requiredLabel no star is drawn, even for a field that a validator requires: the Guide’s "Required Attribute" section
says the required attribute defaults to true "only if you have client-side validation enabled, and a validator is
associated with that particular field"; a form with validate="true" and a requiredstring validator for the field
printed Title: without a star in the Bookshelf (the label became errorLabel after a failed submit, and nothing else
changed).
Tooltips
The Guide documents the tooltip attributes; in the xhtml and css_xhtml themes a field with tooltip="Hint" gets an
icon in its label and the page gets two extra includes after the form (the domTT script and stylesheet). The tooltip
attributes of the form are inherited by its fields, and a field’s own attribute wins. Observed for
<s:textfield name="book.isbn" label="ISBN" tooltip="Hint"/>:
<label for="save_labels_book_isbn" class="label">ISBN: <img
src='/g10uitags/static/tooltip.gif'
title="Hint"
alt="Hint" />
</label>
...
<!-- javascript that is needed for tooltips -->
<script src="/g10uitags/static/domTT.js">
</script>
<link rel="stylesheet" type="text/css" href="/g10uitags/static/domTT.css"/>
The static URLs are served by the framework itself from struts.ui.staticContentPath (/static), so no file has to be
copied into the web application.
Text Input Tags
The tags of this group write one input or textarea and nothing else; what differs is the HTML type and a few
attributes. The first view uses the xhtml theme (the default) and the fields of the Bookshelf form. The values come
from the Dune record that the action constructor builds:
<s:form action="save-basic" method="post">
<s:textfield name="book.title" label="Title" requiredLabel="true" maxlength="80"/>
<s:textfield name="book.isbn" label="ISBN" size="14"/>
<s:password name="book.secret" label="Secret"/>
<s:textarea name="book.summary" label="Summary" rows="3" cols="30"/>
<s:hidden name="note" value="draft"/>
</s:form>
Observed (each field is one table row; shortened to the cells that hold the control):
<td class="tdInput"><input type="text" name="book.title" maxlength="80" value="Dune" id="save_basic_book_title"/></td>
<td class="tdInput"><input type="text" name="book.isbn" size="14" value="9780441172719" id="save_basic_book_isbn"/></td>
<td class="tdInput"><input type="password" name="book.secret" id="save_basic_book_secret"/></td>
<td class="tdInput"><textarea name="book.summary" cols="30" rows="3" id="save_basic_book_summary">Spice & sand</textarea></td>
<tr style="display:none;"> <td colspan="2">
<input type="hidden" name="note" value="draft" id="save_basic_note"/> </td> </tr>
textfield
s:textfield accepts maxlength, size, readonly and type. The type attribute (documented as "the html5 type
element to display, e.g. text, email, url") turns the tag into any text-like input: type="number", "date",
"email", "url". The attributes of the HTML5 type (step, min, max, placeholder, pattern) travel as dynamic
attributes. Observed, in the simple theme:
<input type="email" name="book.author" size="30" maxlength="60" value="" readonly="readonly" id="save_misc_book_author" placeholder="a@b.c"/>
<input type="number" name="book.price" value="" id="save_date_book_price" step="0.01"/>
<input type="date" name="book.publishedOn" value="" id="save_date_book_publishedOn"/>
A property that is not a string is written with the conversion of the current locale. In the Bookshelf the date
2001-03-15 that a user posted was printed back as 3/15/01 in the text field (observed on the English-locale JVM
of the scratch Tomcat), which neither a user who typed ISO dates nor a type="date" control accepts. The fix is to
format the value yourself, with s:date and its var, or with a message that holds a MessageFormat pattern; both
rendered 2001-03-15 (observed):
<s:date name="book.publishedOn" format="yyyy-MM-dd" var="published"/>
<s:form action="save-datefmt" theme="simple">
<s:textfield name="book.publishedOn"/> <%-- 3/15/01 --%>
<s:textfield name="book.publishedOn" value="%{#published}"/> <%-- 2001-03-15 --%>
<s:textfield name="book.publishedOn" value="%{getText('format.date',{book.publishedOn})}"/> <%-- 2001-03-15 --%>
</s:form>
with format.date={0,date,yyyy-MM-dd} in CatalogAction.properties. A conversion error is the opposite case: the
field keeps the text that was typed (abc in the price field), which the conversion error section below shows.
Source: Apache Struts — Date Tag
password
s:password never writes the property back unless showPassword="true". With the default the input has no value
(the first listing above), and showPassword="true" with value="hunter2" rendered value="hunter2" (observed), a
choice that puts the secret into the page source and should stay off. maxlength and size work as for a text field.
<input type="password" name="book.secret" size="10" maxlength="15" value="hunter2" id="save_misc_book_secret"/> <!-- showPassword="true" -->
<input type="password" name="book.secret" id="save_misc_book_secret"/> <!-- the default -->
textarea
s:textarea takes rows, cols, wrap, minlength, maxlength and readonly, and escapes the property that it
prints as the body. Observed for rows="2" cols="20" wrap="hard" minlength="3" maxlength="200" readonly="true":
<textarea name="book.summary" cols="20" rows="2" wrap="hard" readonly="readonly" id="save_misc_book_summary" maxlength="200" minlength="3">Spice & sand</textarea>
hidden
s:hidden renders input type="hidden" with the property or the value. In the xhtml theme the input sits in a table
row with display:none (shown above), so the table stays valid. The value is HTML-escaped like every attribute (a value
of <script>alert(1)</script> & "quotes" came out with <, & and ", observed), but a hidden field is
still user-editable: never use one to keep a price or a permission.
file
s:file renders input type="file" with no value; accept limits the types offered by the browser (not a check on
the server) and size the width. The form must be multipart/form-data; the file upload and download page of this
section covers the interceptor that reads the part.
<s:form action="save-upload" method="post" enctype="multipart/form-data">
<s:file name="upload" accept="text/html,text/plain" label="Cover"/>
</s:form>
Observed (simple theme): <input type="file" name="upload" accept="text/html,text/plain" id="save_misc_upload"/>.
label
s:label shows a value, read-only, in the same layout as an input. In the simple theme it prints a label element;
in xhtml it fills the two cells of a row with the label attribute on the left and the value on the right. The value
comes from value or from the property named by name, and it is escaped:
<label>Dune</label> <!-- simple, s:label value="%{book.title}" -->
<label class="c" for="x">Dune</label> <!-- simple, with for="x" cssClass="c" -->
<label id="save_label_book_title">Dune</label> <!-- simple, name="book.title" -->
<td class="tdLabel"><label class="label">Title:</label></td>
<td class="tdInput"><label>Dune</label></td> <!-- xhtml, label="Title" value="%{book.title}" -->
Note that the attribute for of s:label is the HTML for of the rendered label (it ties the label to another
field’s id), not a property name.
datetextfield
s:datetextfield is the only form tag that does not work out of the box in 7.4.0. The tag declares a mandatory format
attribute (a JSP without it fails to compile with attribute [format] is mandatory for tag [datetextfield]), and its
templates in the simple, xhtml and html5 themes print a notice instead of a field (observed):
<div>Tag <pre style="display: inline-block"><s:datetextfield/></pre> works only with the JavaTemplates Plugin!</div>
The Java templates come from struts2-javatemplates-plugin, which the plugin’s page documents as a faster Java
implementation of tags in the simple theme, enabled with two constants. The plugin was added to the Bookshelf (the BOM supplies its
version) with the first constant, and the form carried theme="simple"; the second constant makes the theme global:
<constant name="struts.ui.templateSuffix" value="java"/>
<constant name="struts.ui.theme" value="simple"/>
With the plugin the tag in a theme="simple" form wrote three text inputs for day, month and year, formatted from the
Date property by format; the datetime interceptor of the default stack puts the three submitted parts together:
a post of __day_book.publishedOn=15, __month_book.publishedOn=03 and __year_book.publishedOn=2001 made the
action’s property the date shown by the saved page as 2001-03-15 (observed). Inside an xhtml form the same tag still
printed the notice, because the xhtml wrapper includes the ftl template of the simple theme, which the suffix
java does not replace for that file. In plain applications the practical answer to a date field is
<s:textfield type="date"> with a formatted value (shown above) and a converter on the property.
<div id="save_dt_book_publishedOn"><input type="text" class="date_day" size="2" maxlength="2" id="__day_save_dt_book_publishedOn" name="__day_book.publishedOn" value="01"></input>/<input type="text" class="date_month" size="2" maxlength="2" id="__month_save_dt_book_publishedOn" name="__month_book.publishedOn" value="01"></input>/<input type="text" class="date_year" size="4" maxlength="4" id="__year_save_dt_book_publishedOn" name="__year_book.publishedOn" value="1970"></input></div>
Checkbox, Radio and Checkboxlist
These tags write several controls that share one name; the difference between them is whether the property is a boolean, a single value from a list or a collection.
checkbox
s:checkbox renders one input type="checkbox", checked when the property evaluates to true. fieldValue is the
value that is posted when the box is ticked (true by default):
<s:checkbox name="book.inStock" label="In stock"/>
<s:checkbox name="book.inStock" fieldValue="yes"/>
<s:checkbox name="book.inStock" label="In stock" labelPosition="left"/>
Observed. In the xhtml theme the label sits after the box by default (the left cell of the row is empty); with
labelPosition="left" the label moves to the left cell and gets the separator:
<td class="tdCheckboxLabel"> </td>
<td class="tdCheckboxInput"><input type="checkbox" name="book.inStock" value="true" id="save_basic_book_inStock"/><label for="save_basic_book_inStock" class="checkboxLabel">In stock</label></td>
<td class="tdCheckboxLabel"><label for="save_choices_book_inStock" class="checkboxLabel">In stock:</label></td>
<td class="tdCheckboxInput"><input type="checkbox" name="book.inStock" value="true" id="save_choices_book_inStock"/></td>
A browser sends nothing for an unticked checkbox, so a form cannot say "the user cleared it". The Struts answer is a
second, hidden field and an interceptor, and here the 7.4.0 jar differs from every guide. Getting Started shows the
checkbox tag with a hidden __checkbox_... field next to the input; the Checkbox Interceptor page says the tag "automatically
generates this hidden field"; and the Hidden Field Prefix section of the tag page says the tag generates it by default. In
7.4.0 the hidden field is written only when submitUnchecked="true" is set (or the constant
struts.ui.checkbox.submitUnchecked is true); with the default the tag writes the input alone (see the first
observed line above). The table shows what the three variants do to an action that starts with inStock set to true:
<s:checkbox name="book.inStock"/>
<s:checkbox name="book.inStock" submitUnchecked="true"/>
| Rendered by the tag | Posted by the browser with the box unticked | inStock in the action |
|---|---|---|
|
|
|
|
|
|
|
|
|
The second variant wrote <input type="hidden" id="__checkbox_save_choices_book_inStock" name="__checkbox_book.inStock"
value="true" /> after the input, and the third wrote the same field as struts_checkbox_book.inStock (observed). The
checkbox interceptor followed the constant (the class also has a hiddenPrefix setter) and takes the value it puts
in for a missing box from uncheckedValue (false by default); the Guide calls the prefix struts_checkbox_ the
"HTML5-compliant alternative" to the backward-compatible default __checkbox_. A project that keeps boolean
properties therefore sets submitUnchecked on the tag, or globally:
<constant name="struts.ui.checkbox.submitUnchecked" value="true"/>
<constant name="struts.ui.checkbox.hiddenPrefix" value="struts_checkbox_"/>
The interceptors page lists checkbox in defaultStack; a stack of your own that omits it never
sees the helper field and leaves the property untouched.
radio
s:radio renders one input type="radio" per element of its list, all with the same name, and checks the one whose
key equals the property. The tag accepts the list forms described below (a List, a Map, a list of objects with
listKey and listValue), plus listCssClass and listTitle, which are property names of the list objects, not
literal values. Observed for a list of Genre objects (code and label properties) and a Map:
<s:radio name="book.genre" list="genres"/>
<s:radio name="book.genre" list="genreMap"/>
<s:radio name="book.genre" list="genreObjects" listKey="code" listValue="label" listCssClass="code" listTitle="label"/>
<input type="radio" name="book.genre" id="save_choices_book_genresf" checked="checked" value="sf"/><label for="save_choices_book_genresf">sf</label>
<input type="radio" name="book.genre" id="save_choices_book_genresf" checked="checked" value="sf"/><label for="save_choices_book_genresf">Science fiction</label>
<input type="radio" name="book.genre" id="save_misc_book_genrecrime" value="crime" class="crime" title="Crime & thriller"/><label for="save_misc_book_genrecrime">Crime & thriller</label>
The id of each button is the field id followed by the key (save_choices_book_genresf). The Getting Started tutorial
prints an id with a space in it for the key not sure (gendernot sure); the key is appended without escaping, so
keys with spaces give invalid ids; use code-like keys.
checkboxlist
s:checkboxlist renders a checkbox per element of the list, all named after the property; the values that are checked
are the elements of the collection property. The submitted values arrive as repeated parameters that the params
interceptor puts into the List<String> property (the Checkbox Interceptor page insists that @StrutsParameter go on the
setter of the collection). The tag also writes a hidden __multiselect_book.tags field, which lets the multiselect
interceptor set an empty collection when nothing is ticked. Observed:
<s:checkboxlist name="book.tags" list="tagChoices"/>
<input type="checkbox" name="book.tags" value="classic" id="save_choices_book_tags-1" checked="checked" />
<label for="save_choices_book_tags-1" class="checkboxLabel">classic</label>
<input type="checkbox" name="book.tags" value="new" id="save_choices_book_tags-2" />
<label for="save_choices_book_tags-2" class="checkboxLabel">new</label>
...
<input type="hidden" id="__multiselect_save_choices_book_tags" name="__multiselect_book.tags"
value="" />
A post with book.tags=new&book.tags=rare set the list to [new, rare]; a post with only
__multiselect_book.tags= set it to [], and a post with neither left the original [classic] (all observed on
the sticky mapping). Two details of the template deserve a warning. The ids are numbered (-1, -2, …), not
derived from the values as for a radio. And cssClass on a checkboxlist is written next to the class that comes from
listCssClass, producing a duplicated attribute (observed: class="sf" class="c" and, when listCssClass named a
property that does not exist, class="" class="c"), which browsers resolve by ignoring the second; put the class on
the form or on a wrapping element.
Select and the Lists That Feed It
Every tag that offers a choice from several values takes a list attribute, and the Tag Developers Guide uses the same
words for all of them. The list is an OGNL expression, evaluated against the value stack, that must produce a
collection, an array, a map, an enumeration or an iterator; an expression that produces anything else stops the page with
an exception that names the tag, the field and the expression (see the pitfalls). The attributes listKey and
listValue name the properties of each element that give the option value and the option text; listTitle,
listCssClass and listCssStyle name the properties that give the HTML title, class and style of each option; and
listValueKey and listLabelKey are two more property names, of which the descriptor says that listLabelKey is the
property used to look up a localised label (the examples below use listKey and listValue).
The forms of a list
list and companions |
Elements | Rendered options |
|---|---|---|
|
The element itself is key and text |
|
|
The map key is the option value, the map value the text |
|
|
|
The same three options |
|
An inline OGNL list |
Two options whose value and text are the literals |
|
An inline OGNL map |
|
|
Empty |
A |
|
The key as the |
|
|
An explicit |
|
Two checks on the list expression came out differently from what an old tutorial suggests. First, the select tag page
says that the map keys of an inline map are "typed" ('1' a char, '01' a String, "1" a String) and that a value
of another type "WILL NOT MATCH". On 7.4.0 every combination that I tried selected the option: the integer keys of #{1:'One',2:'Two'} with
value="1", value="'1'" and value=""1""; the string keys of #{'01':'Jan','02':'Feb'} with
value="'01'"; and the keys of #{'1':'Jan','2':'Feb'} with value="'1'" and with the integer value="1" (a
#{"1":...} map with "1" too); seven combinations, all with selected="selected" on the
first option. The comparison is made on the text of the values, so the warning of the page is stale for select. Second, a method call inside a list expression is
blocked by the allowlist that is on by default: list="catalogByGenre.keySet()" on a doubleselect stopped the page
with The requested list key 'catalogByGenre.keySet()' could not be resolved as a collection/array/map/enumeration/iterator
type, and listValue="value.size()" produced options with empty text (observed). Expose a getter that returns a list
instead.
select
s:select renders select with its options; the option whose value equals the property (or the value) carries
selected. The attributes that matter, observed on the Bookshelf genre field:
<s:select name="book.genre" list="genreMap" headerKey="" headerValue="-- choose --"/>
<s:select name="book.genre" list="genreMap" emptyOption="true"/>
<s:select name="book.tags" list="tagChoices" multiple="true" size="3"/>
<s:select name="book.genre" list="genres" disabled="true"/>
<select name="book.genre" id="save_select_book_genre">
<option value="" >-- choose --</option>
<option value="sf" selected="selected">Science fiction</option>
...
</select>
<select name="book.genre" id="save_select_book_genre">
<option value=""></option>
<option value="sf" selected="selected">Science fiction</option>
...
</select>
<select name="book.tags" size="3" id="save_select_book_tags" multiple="multiple">
<option value="classic" selected="selected">classic</option>
...
</select>
<input type="hidden" id="__multiselect_save_select_book_tags" name="__multiselect_book.tags" value="" />
<select name="book.genre" disabled="disabled" id="save_select_book_genre"> ...
headerKey and headerValue add a first option; the tag descriptor says the key "must not be empty" (its
own example is garbled: it names '' both as correct and as bad), yet headerKey="" rendered a normal option with an
empty value, and that is the form most projects want together with a required validator so that "choose" fails validation. emptyOption
adds a blank option after the header. multiple="true" adds the hidden __multiselect_... field that the
multiselect interceptor uses (nothing selected then means an empty collection instead of an unchanged property).
optgroup
s:optgroup is nested inside a select and adds a labelled group of options from its own list, listKey and
listValue. Observed:
<s:select name="book.genre" list="genreMap" headerKey="-1" headerValue="Pick one">
<s:optgroup label="More" list="#{'x':'Extra','y':'Why'}"/>
<s:optgroup label="Lib" list="genreObjects" listKey="code" listValue="label"/>
</s:select>
<select name="book.genre" id="save_optgroup_book_genre">
<option value="-1" >Pick one</option>
<option value="sf" selected="selected">Science fiction</option>
...
<optgroup label="More">
<option value="x">Extra</option>
<option value="y">Why</option>
</optgroup>
<optgroup label="Lib">
<option value="sf" selected="selected">Science fiction</option>
...
</optgroup>
</select>
The groups come after the options of the select’s own list, and a group that contains the current value marks it as
selected too.
combobox
s:combobox is a text input with a drop-down that fills it: the user may type a value that is not in the list. The
property is bound to the text input, the select has no name, and a small script copies the chosen option into the
input. Observed for <s:combobox name="book.genre" list="genres"/>:
<script>
function autoPopulate_save_combo_book_genre(targetElement) {
targetElement.form.elements['book.genre'].value=targetElement.options[targetElement.selectedIndex].value;
}
</script>
<input type="text" name="book.genre" value="sf" id="save_combo_book_genre"/><br />
<select onChange="autoPopulate_save_combo_book_genre(this);">
<option value="sf" selected="selected">sf</option>
...
</select>
The tag takes list, listKey, listValue, headerKey, headerValue, emptyOption, size and maxlength. It
writes an inline script and an inline onChange handler, which a strict Content-Security-Policy blocks; an
<input list="..."> with a <datalist> written by hand is the modern replacement.
doubleselect
s:doubleselect renders two selects, the second filled from the choice made in the first, with the data in the page
(no request). The first select has name and list; the second has doubleName and doubleList, in which the
variable top stands for the key chosen in the first; formName must be the name of the enclosing form, because the
generated script reaches the second select as document.<formName>.<id>. Observed in a browser (the Bookshelf’s two
levels: genre, then title):
<s:form theme="simple" action="save-combo2" id="shelfForm" name="shelfForm">
<s:doubleselect label="Genre" name="book.genre" list="{'sf','fantasy'}"
doubleName="book.title"
doubleList="top == 'sf' ? {'Dune','Neuromancer'} : {'The Hobbit','Earthsea'}"
formName="shelfForm" id="ds1"/>
</s:form>
<select name="book.genre" id="ds1" onchange="ds1Redirect(this.selectedIndex)">
<option value="sf" selected="selected">sf</option>
<option value="fantasy">fantasy</option>
</select>
<br/>
<select name="book.title" id="shelfForm_book_title">
</select>
<script>
var ds1Group = new Array(2 + 1);
...
var ds1Temp = document.shelfForm.shelfForm_book_title;
ds1Redirect(1);
function ds1Redirect(x) { ... }
</script>
Choosing fantasy in the browser changed the options of the second select from Dune, Neuromancer to The Hobbit,
Earthsea. With the same tag but formName="f1" in a form that was named save_combo (the default name), the script
failed on load with Uncaught TypeError: Cannot read properties of undefined (reading 'save_combo_book_title'), because
document.f1 does not exist: name the form with name, and give the same text to formName. The second select’s id is
<formId>_<doubleName> with dots turned into underscores, and doubleValue, doubleEmptyOption, doubleHeaderKey and
the double... family of presentation attributes mirror the first select’s.
optiontransferselect, inputtransferselect and updownselect
These three tags build list boxes with buttons for the Bookshelf’s tag editor, all from the static scripts that the
framework serves from /static (optiontransferselect.js, inputtransferselect.js, utils.js):
| Tag | What it renders and the names it posts |
|---|---|
|
Two multi-select lists with |
|
A text input ( |
|
One multi-select list with |
<s:form theme="simple" action="save-transfer">
<s:optiontransferselect name="left" list="genres" doubleName="right" doubleList="tagChoices"
leftTitle="Genres" rightTitle="Tags" id="ots" formName="f2"/>
<s:inputtransferselect name="book.tags" list="tagChoices" id="its"/>
<s:updownselect name="book.tags" list="tagChoices" id="uds"/>
</s:form>
Observed (shortened): the left list <select name="left" size="15" id="ots" multiple="multiple">, the right list
<select name="right" size="15" multiple="multiple" id="save_transfer_right">, a hidden __multiselect_left and
__multiselect_right for each, the text input <input type="text" name="book.tags_input" id="its_input"/>, and a
script that registers selectAllOptionsExceptSome(...) on the form’s submit event for each list. Clicking -> in a
browser with sf selected on the left moved sf to the right list (classic, new, signed, rare, sf), and typing extra
and clicking -> of the input transfer added extra to its list. The submitted values are plain repeated parameters, so
the action receives them like a checkboxlist.
Buttons and the Token
submit
s:submit renders one of three elements, chosen by type: input (default, <input type="submit">), button
(<button type="submit">, whose face may be HTML) and image (<input type="image"> with src). The value is the
text of the button (Submit when omitted), and key takes the text from a bundle and writes the key as the name.
Observed:
<s:submit value="Save"/>
<s:submit key="x.save" value="Plain"/>
<s:submit type="button" value="Button"/>
<s:submit type="button"><b>Rich</b> body</s:submit>
<s:submit type="image" src="/img/go.png" value="Go" alt="go"/>
<s:submit value="Alt" action="other"/>
<s:submit value="Method" method="save"/>
<s:submit value="Both" action="other" method="save"/>
<s:submit value="Off" disabled="true" name="off" onclick="alert(1)"/>
<input type="submit" value="Save" id="save_submit_0"/>
<input type="submit" value="Plain" id="save_submit_x_save" name="x.save"/>
<button type="submit" id="save_submit_2" value="Button">
Button
</button>
<button type="submit" id="save_submit_4" value="Submit">
<b>Rich</b> body
</button>
<input type="image" alt="Go" src="/img/go.png" id="save_submit_6" alt="go"/>
<input type="submit" value="Alt" id="save_submit_other" name="action:other"/>
<input type="submit" value="Method" id="save_submit__save" name="method:save"/>
<input type="submit" value="Both" id="save_submit_other_save" name="action:other"/>
<input type="submit" value="Off" id="save_submit_off" name="off" disabled="disabled" onclick="alert(1)"/>
Four observations. The ids are sequential (save_submit_0, _2, …) unless the button has an action or method in its
name. The src of an image button is written as given, without the context path (/img/go.png), and the image button
printed alt twice (once from value). A button with both action and method carries only the action: name, so the
method is lost. And action and method are the two prefixes of the action mapper: the button posts a parameter named
action:other or method:save, which the mapper turns into another action or another method of the same action. With
the defaults a post of action:save-submit=x or method:save=x to page-basic was ignored and the form was shown
again (observed); with struts.mapper.action.prefix.enabled=true and struts.enable.DynamicMethodInvocation=true (and the
method listed in <allowed-methods>) the post to page-basic with action:save-submit ran save-submit and showed
the saved page, and method:other ran other() (observed: the action set a marker that the saved page printed). Both
constants are off for good reasons (the Submit Tag page warns that dynamic method calls are a security risk, and the
wildcard mappings page of this section shows the safer pattern of one mapping per button); the form that needs several
buttons maps save-* style actions and gives every button its own action name with action, or branches on the
button’s name in a single method.
The body of a type="button" submit is passed through as written by default and is escaped only with
escapeHtmlBody="true"; the escaping section below has the details.
reset
s:reset renders <input type="reset" value="Reset"/> or, with type="button", <button type="reset" value="Clear2">Clear2</button>
(observed). It has the same value, key and type handling as submit, and the TLD description of its type mentions
a third value, image, which the tag page does not list.
token
s:token writes the hidden fields that the token and tokenSession interceptors compare; the
double submit tokens and wait pages page of this section explains the interceptors, the invalid.token result and the session store. The tag
itself, inside an xhtml form, produced (observed; the token values are random):
<input type="hidden" name="struts.token.name" value="token" />
<input type="hidden" name="token" value="JKBNMYLLUS99QIAY3715O305T16K44N0" />
s:token name="x" writes struts.token.name=x and a field called x; the tag declares only the common attributes, and
name is the one that sets the token’s name (an attribute tokenName is rejected at translation time with Attribute
[tokenName] invalid for tag [token] according to TLD). Inside an xhtml form the two inputs are written directly
between the table and its rows, with no tr around them (observed, and the same with theme="simple" on the tag), which
is invalid HTML that browsers accept.
Field Errors, Action Errors and Action Messages
Three tags print the messages that the action collects (addFieldError, addActionError, addActionMessage; the
validation page of this section shows where the workflow interceptor makes them). They differ from the field tags by
printing a list, not a row, and by being independent of the theme’s layout of fields.
<s:actionerror/>
<s:actionmessage/>
<s:fielderror/>
<s:fielderror fieldName="book.isbn"/>
<s:fielderror><s:param>book.title</s:param></s:fielderror>
Observed after a post of book.title=Xdune, book.isbn=12 and note=save, with two field errors on book.isbn, one
action error and one action message (shortened):
<ul class="errorMessage">
<li><span>Titles may not start with X</span></li></ul>
<ul class="actionMessage">
<li><span>Draft kept</span></li>
</ul>
<ul class="errorMessage" >
<li><span>ISBN needs 13 digits</span></li>
<li><span>Second message for the same field</span></li>
</ul>
s:fielderror with no parameter lists every field’s errors, with fieldName (or with nested s:param elements, one per
field) it lists the named ones, and an empty result prints nothing, so a view can include it unconditionally. All three
tags escape the text of the messages by default, and escape="false" (declared on all three) lets markup through; it
was observed on actionerror (see the escaping section). The tag description says empty (null or blank) messages are not printed.
Errors beside the fields
The field tags of the xhtml and css_xhtml themes show their own field errors, keyed by the tag’s name, without any
s:fielderror. Three things decide how:
| Theme | What a field with a field error rendered (observed, after a post with an empty title and book.isbn=12) |
|---|---|
|
One extra table row per message, placed above the field ( |
|
A |
|
Nothing: the theme has no error layout, so the view calls |
|
No label and no error text: a post with an empty title rendered the bare |
An error row appears only when the field error is stored under the same name as the tag. The Bookshelf’s validate()
adds the error under book.title, which is also the name of the textfield, so the row shows; an error added under
title would show in s:fielderror but beside no field. The same rule is why a model-driven action whose validators
are named model.isbn (see the validation page) shows no error beside a field called isbn.
Conversion errors
A value that cannot be converted is not a validation message that the action wrote: the conversionError interceptor
adds the field error Invalid field value for field "book.price". (from the framework’s message bundle) and remembers
the submitted text, and the field tag shows that text, not the property. Observed after a post of book.price=abc and
book.publishedOn=notadate in a css_xhtml form:
<div id="wwerr_save_conv_book_price" class="wwerr"> <div errorFor="save_conv_book_price" class="errorMessage">
Invalid field value for field "book.price".
</div></div>
...
<input type="text" name="book.price" value="abc" id="save_conv_book_price"/>
<input type="text" name="book.publishedOn" value="notadate" id="save_conv_book_publishedOn"/>
The type conversion page of this section explains the message keys (invalid.fieldvalue.<field>) that replace the
default text, and the localization page shows how the same keys are translated.
The s:head Tag and the Static Files
s:head goes in the head element and writes what the theme’s templates assume to be present: for the xhtml theme a
stylesheet link (the classes wwFormTable, tdLabel, errorMessage, … come from it) and the include of the helper
script utils.js (the StrutsUtils object used by focusElement, the transfer selects and the generated validation).
Observed for <s:head/> and for the other themes:
<link rel="stylesheet" type="text/css" href="/g10uitags/static/xhtml/styles.css"/>
<script src="/g10uitags/static/utils.js">
</script>
<!-- s:head theme="css_xhtml": the same with /static/css_xhtml/styles.css -->
<!-- s:head theme="simple": only the script -->
<!-- s:head theme="html5": only <script src="/g10uitags/static/utils.js"></script> -->
The URLs start with struts.ui.staticContentPath (default /static), and the framework answers them from the jar
(/g10uitags/static/optiontransferselect.js returned HTTP 200 text/javascript), so no resource has to be copied into
the web application. A view without s:head renders correct HTML that is unstyled; the helper script is missing too, so
a page that uses focusElement, a transfer select or the generated validation needs the tag.
Escaping
The UI tags escape what they write into attributes and element bodies, so a value taken from the user cannot close the
attribute or open a tag. The table lists what was observed for the value <script>alert(1)</script> & "quotes" and for
markup in messages:
| Output | Observed |
|---|---|
Attribute values ( |
Escaped: |
Body of a |
Escaped: |
The text of |
Escaped: |
Field errors, action errors and action messages |
Escaped by default: a title typed as |
The same messages with |
Raw: |
The body of |
Raw by default ( |
The body of |
Escaped ( |
The body of |
Still raw ( |
|
No effect: the attribute is not declared, so it was written to the element ( |
The rule that results is the one the Tag Developers Guide gives: the tags whose body is passed through (s:a,
s:submit, s:component) escape it with escapeHtmlBody, set per tag or, for the tags that honour it, with the global
struts.ui.escapeHtmlBody. In the Bookshelf the constant covered s:a and left the button alone, so set the attribute
on the button. There is no attribute that switches the escaping of a field’s value off, and none is needed: build the
markup in the view around the field.
The key Attribute and the Resource Bundles
key is the short way to write a labelled field: the field’s name is the key, the label is the text of that key in
the resource bundles, and the value is read from the property of that name. The Getting Started tutorial on message
resource files introduces it with the CatalogAction.properties file next to the class; the localization page of this
section explains the search order of the bundles. The Bookshelf’s bundle and three fields:
book.title=Book title
book.isbn=International Standard Book Number
x.save=Persist
<s:textfield key="book.title"/>
<s:textfield key="book.isbn" label="Override"/>
<s:textfield key="book.author" name="x"/>
Observed (the label cells and the names):
<td class="tdLabel"><label for="save_key_book_title" class="label">Book title:</label></td>
<td class="tdInput"><input type="text" name="book.title" value="Dune" id="save_key_book_title"/></td>
<td class="tdLabel"><label for="save_key_book_isbn" class="label">Override:</label></td>
<td class="tdLabel"><label for="save_key_x" class="label">book.author:</label></td>
<td class="tdInput"><input type="text" name="x" value="" id="save_key_x"/></td>
An explicit label wins over the key; an explicit name wins over the key’s name (the third field posts x); and a
key with no entry in any bundle prints the key itself as the label (book.author:), which is the visible sign of a
missing translation. A button with key takes its caption the same way (<s:submit type="button" key="x.save"/> printed
Persist).
Validation in the Browser
Server-side errors are shown by the tags as described above. Two features try to catch an error before the request.
Generated JavaScript (deprecated)
s:form validate="true" in an xhtml or css_xhtml form includes /static/xhtml/validation.js and generates a
function validateForm_<id> that is called from onsubmit. The page of the Core Developer Guide on client side
validation marks it "deprecated since Struts 7.4.0 (WW-5694), removed in Struts 8.0.0 (WW-5696)" and the templates carry
the same comment. It understands only the validators required, requiredstring, stringlength, regex, email,
url, int and double. On the Bookshelf, a CatalogAction-clientform-validation.xml file with requiredstring for
book.title and stringlength for book.isbn produced, in the page that the action clientform rendered after an
invalid post, rules like these:
// field name: book.title
// validator name: requiredstring
if (form.elements['book.title']) {
field = form.elements['book.title'];
var error = "Title is required (client)";
var fieldValue = getFieldValue(field);
if (continueValidation && fieldValue !== null && (fieldValue === "" || fieldValue.replace(/^\s+|\s+$/g,"").length === 0)) {
addError(field, error);
errors = true;
}
}
In the same experiment a plain page-client view, rendered by an action that has no validation file (the form’s action
pointed at save-client, which had one), produced the function with no rules at all, and so did the view client2.jsp
whose form pointed at clientform but was rendered by page-client2; the rules appeared only when the page was
rendered by the action that owns the validators (here clientform, by a post that failed validation). The Guide says
the code is generated "from the same validation configuration used server-side"; in 7.4.0 the validators are those of the
current action. Together with the removal announced for 8.0, a new project should not use it.
HTML5 constraints
The replacement is the html5 theme with struts.ui.html5.constraints=true, which derives required, minlength,
maxlength, pattern, min and max from the action’s validators. The validation page of this section has the
mapping table and the worked example, so only the pointer is repeated here: the browser enforces the attributes, the
server validation still runs, and requiredLabel is a different thing (a star on the label; it never makes a field
required).
The Bookshelf Edit Form End to End
The view below puts the tags together: the form of the Bookshelf’s "edit a title" page, in the default xhtml theme,
labelled through key, with a drop-down that fails validation until a genre is chosen, a radio group, a checkbox list,
a checkbox that submits its false, a text area and the token.
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html>
<html>
<head>
<title>Edit a title</title>
<s:head/>
</head>
<body>
<s:actionerror/>
<s:form action="save-edit" method="post">
<s:textfield key="book.title" requiredLabel="true" maxlength="80"/>
<s:textfield key="book.isbn" size="14"/>
<s:textfield key="book.price" type="number" step="0.01"/>
<s:textfield key="book.publishedOn" placeholder="yyyy-mm-dd"/>
<s:select key="book.genre" list="genreMap" headerKey="" headerValue="-- choose --"/>
<s:radio key="book.format" list="{'paperback','hardcover'}"/>
<s:checkboxlist key="book.tags" list="tagChoices"/>
<s:checkbox key="book.inStock" submitUnchecked="true"/>
<s:textarea key="book.summary" rows="3" cols="40"/>
<s:token/>
<s:submit value="Save"/>
<s:reset value="Reset"/>
</s:form>
</body>
</html>
Observed for page-edit (the rows that show the mechanisms; the table, the cells and the remaining rows are as in the
earlier listings):
<link rel="stylesheet" type="text/css" href="/g10uitags/static/xhtml/styles.css"/>
<script src="/g10uitags/static/utils.js"></script>
...
<label for="save_edit_book_title" class="label">Book title<span class="required">*</span>:</label>
<input type="text" name="book.title" maxlength="80" value="Dune" id="save_edit_book_title"/>
...
<input type="number" name="book.price" value="" id="save_edit_book_price" step="0.01"/>
<input type="text" name="book.publishedOn" value="" id="save_edit_book_publishedOn" placeholder="yyyy-mm-dd"/>
<select name="book.genre" id="save_edit_book_genre">
<option value="" >-- choose --</option>
<option value="sf" selected="selected">Science fiction</option>
...
<input type="checkbox" name="book.inStock" value="true" id="save_edit_book_inStock"/><input type="hidden" id="__checkbox_save_edit_book_inStock" name="__checkbox_book.inStock" value="true" />
...
<input type="hidden" name="struts.token.name" value="token" />
<input type="hidden" name="token" value="3U7I26HGR759QQ9MAV8L08EYQ99LIMWT" />
A post that breaks four rules (an empty title, book.isbn=12, book.price=abc, an empty genre) with the
__multiselect_book.tags and __checkbox_book.inStock helper fields and no ticked tag, returned the view again with the
errors beside the fields (shortened to the changed parts):
<tr errorFor="save_edit_book_title">
<td class="tdErrorMessage" colspan="2"><span class="errorMessage">Title is required</span></td>
</tr>
<tr>
<td class="tdLabel"><label for="save_edit_book_title" class="errorLabel">Book title<span class="required">*</span>:</label></td>
<td class="tdInput"><input type="text" name="book.title" maxlength="80" value="" id="save_edit_book_title"/></td>
</tr>
<tr errorFor="save_edit_book_isbn"> ... ISBN needs 13 digits ...</tr>
<tr errorFor="save_edit_book_isbn"> ... Second message for the same field ...</tr>
<tr errorFor="save_edit_book_price">
<td class="tdErrorMessage" colspan="2"><span class="errorMessage">Invalid field value for field "book.price".</span></td>
</tr>
...
<input type="number" name="book.price" value="abc" id="save_edit_book_price" step="0.01"/>
<input type="text" name="book.publishedOn" value="3/15/01" id="save_edit_book_publishedOn" placeholder="yyyy-mm-dd"/>
<tr errorFor="save_edit_book_genre"> ... Choose a genre ...</tr>
<option value="" selected="selected" >-- choose --</option>
<input type="radio" name="book.format" id="save_edit_book_formathardcover" checked="checked" value="hardcover"/>
The radio group kept hardcover, the checkbox list lost its classic tick (the submitted list was empty, as posted), the
price kept the rejected abc and the date, which was valid, came back as 3/15/01 (see the textfield section for the
fix). A valid post (title Dune, a 13-digit ISBN, price 9.5, genre sf, tags new and rare, format hardcover,
inStock ticked) ran save() and the saved view printed Saved Dune (hardcover, in stock: true, tags: [new, rare]).
From the Struts 1 html Tags to the Struts 2 Form Tags
Struts in Action (2002) teaches the html tag library of Struts 1, in which a form binds to an ActionForm bean and
html:errors prints the messages. The Struts 2 tags cover the same needs with a different shape: the "form bean" is the
action itself or a model object reached by a path, and a tag that has a label also lays out its own error rows.
The Struts 1 chapter on dynamic content corresponds to this page and to the generic tags page of this section;
the page Struts 1 (Classic) & Migration carries the full mapping.
| Struts 1 | Struts 2 (7.4.0) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Pitfalls
| Symptom or error | Cause | Fix |
|---|---|---|
Every field is empty although the action holds values, and the log has |
The OGNL allowlist of Struts 7 does not cover the model class’s package |
Add the package to |
A property that was |
The 7.4.0 |
Set |
A checkbox list cannot be cleared: the old selection returns (observed) |
No |
Use |
A valid date comes back as |
The property is written with the locale’s short format |
Format the value ( |
The form posts to a relative URL and its ids contain |
The |
Fix the name or the namespace; keep ids out of identifiers by giving the form an explicit |
The form posts to |
|
Remove the attribute unless the application is the root context |
|
|
Give the form |
|
A method call such as |
Expose a getter ( |
Option text is empty (observed with |
The same: a method call in |
Use a property name that exists |
The |
7.4.0’s FreeMarker templates do not render the tag |
Use |
A duplicated |
|
Style the group through the form or a wrapper; give |
|
The attribute is |
Use the camel-case spellings of the Tag Developers Guide |
No star next to a required field although a validator requires it (observed) |
A star is drawn only when |
Set |
|
The mapper ignores the |
Prefer one mapping per button, or test the button’s name in the action |
|
The messages are escaped by default |
Keep it; use |
Markup in the body of a button is not escaped (observed) |
|
Set |
The generated validation JavaScript has no rules (observed) |
The rules come from the validators of the action that renders the page, not from the action named in |
Render the form from the validating action, or switch to the |
Two fields have the same HTML id (observed with a repeated |
The id is |
Pass |
The page has no styles and |
|
Add |
A |
The value is not in the list, or the list is built from objects without |
Check that |
Source: Apache Struts — Form Tags
What Changed Recently
| Release | Change |
|---|---|
Struts 2.3.17 |
A |
Struts 2.6 |
FreeMarker auto-escaping is on; |
Struts 6.1.1 |
The constant |
Struts 7.0 |
Java 17 and the Jakarta namespace; the classes of the tags and the interceptors live in |
Struts 7.2.0 |
The |
Struts 7.4.0 |
HTML5 constraint validation for the |
Gone by 7.4.0 |
The Dojo-based |
Where the Books Are Outdated
Struts 2 in Action (Manning, 2008, chapter 7, UI component tags) is written for Struts 2.0 and Struts in Action (Manning, 2002, chapter 10) for Struts 1. The tag names and the attribute names of the common tags survived; the following did not, or changed meaning.
| The book’s era | Today (Struts 7.4) |
|---|---|
Four themes |
Four themes |
|
Gone; |
|
Deprecated in 7.4, removed in 8.0; HTML5 constraints in the |
The checkbox tag always writes a hidden field |
|
Imports and examples from |
|
Any public setter fills the field |
The |
|
Unchanged |
A Struts 1 |
The action or a model object behind |
Form tags emit a |
Same; |
|
The action name without the extension; the tag builds the URL |
Notes on the Official Documentation
Reading the official pages against the 7.4.0 jar showed inconsistencies that a reader will meet; they are listed so that a mismatch is not mistaken for a mistake in the example code.
-
The Getting Started "Form Tags" tutorial, the Checkbox Interceptor page and the Hidden Field Prefix section of the Checkbox Tag page say the checkbox tag writes the hidden
__checkbox_...field by itself; in 7.4.0 it does so only withsubmitUnchecked="true"(the attribute’s own row says so and gives the constant "since Struts 6.1.1"). -
The Form Tags page says the framework "bundles three themes"; the 7.4.0 jar has four (
html5since 7.2.0), and the html5 Theme page lists "Validation and error reporting" as a feature although itscontrolheaderandcontrolfootertemplates are empty. -
The Form Tags page lists the general attribute as
tabIndex; the tag library descriptor declarestabindex(a camel-casetabIndexis not a declared attribute). -
The Form Tag page, and the descriptor of
s:form, still sayvalidateis "only useful with theme xhtml/ajax"; there is noajaxtheme, and the page’s own "Validation" section says the attribute is deprecated since 7.4.0. -
The Form Tags page’s tooltip examples 3 to 5 open their comments with
<--instead of<!--. -
The Select Tag page warns that a map key of another type than the
valuedoes not match ('1'against'01'or"1"); on 7.4.0 all seven combinations tried selected the option. -
The Fielderror Tag page’s last example repeats
<s:fielderror fieldName="field1" />twice. -
The Datetextfield Tag page and the descriptor say "Render an HTML input fields with the date time" and give no example, while the 7.4.0 templates print "works only with the JavaTemplates Plugin!" and the plugin page asks for
struts.ui.themeset tosimple. -
The Getting Started "Form Tags" page shows a radio id
save_personBean_gendernot sure: ids built from list keys keep the spaces. -
The Submit Tag description still calls it a tag "to provide asynchronous form submissions" and warns about Internet Explorer 6; the Reset Tag descriptor lists
imageas a validtypealthough the tag page lists two types.
References
-
Apache Struts — Checkbox Interceptor, Checkbox lists usage with @StrutsParameter
-
Apache Struts — Client Side Validation, HTML5 Constraint Validation
-
Apache Struts — Client Side Validation, Pure JavaScript Client Side Validation (deprecated)
-
Apache Struts — Client Side Validation, requiredLabel is unrelated to the required attribute
-
Apache Struts — Getting Started, Form Tags, Struts Checkbox Tag
-
Apache Struts — Getting Started, Message Resource Files, Struts key Attribute
-
Apache Struts — Getting Started, Coding Actions, Processing Form Input In The Action Class