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:

  1. read the attribute values (evaluating OGNL where an attribute is an expression);

  2. derive what the attributes leave out: the id from the form and the name, the value from the property named by name, the label from the key, the error list from the action’s field errors;

  3. pick a template (template/<theme>/text.ftl for a textfield) and render it with the attributes in a hash;

  4. 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.

Table 1. The UI tags of Struts 7.4.0 by purpose (the s: prefix is the one declared by <%@ taglib prefix="s" uri="/struts-tags" %>)
Group Tags What they render

Container

s:form

The form element, its action URL, the surrounding table of the xhtml theme and the optional client-side validation script

Text input

s:textfield, s:password, s:textarea, s:hidden, s:file, s:datetextfield

input elements of type text, password, hidden and file, a textarea, and a date made of three fields

Read-only output

s:label

A label element that shows a value in the same row layout as an input

Boolean and single choice

s:checkbox, s:radio

One checkbox, or a group of radio buttons built from a list

Multiple choice

s:checkboxlist, s:select with multiple="true"

A group of checkboxes sharing one name, or a multi-select list box

Choice lists

s:select, s:optgroup, s:combobox

A drop-down list, option groups inside it, and a text field with a list that fills it

Linked and transfer lists

s:doubleselect, s:optiontransferselect, s:inputtransferselect, s:updownselect

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

s:submit, s:reset

input or button elements (and an image button for s:submit)

Anti double click

s:token

Two hidden fields that carry the token (see the double submit tokens page of this section)

Messages

s:fielderror, s:actionerror, s:actionmessage

The lists of field errors, action errors and action messages collected in the action

Page head

s:head

A stylesheet link and the Struts JavaScript include for the head section

Custom

s:component

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.

flowchart LR A["page-edit: action runs, JSP renders s:form and the field tags"] --> B["Browser shows the form (names, values, hidden helper fields)"] B --> C["POST save-edit"] C --> D["checkbox and multiselect interceptors add false and empty lists for missing controls"] D --> E["params interceptor: book.title, book.tags and so on, with type conversion"] E --> F["validation interceptor: validate() and -validation.xml add field errors"] F --> G{"errors or conversion errors?"} G -- "yes: workflow returns input" --> H["same JSP again: tags read fieldErrors and the submitted values"] G -- "no" --> I["save() runs, success result"] H --> B

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".

Table 2. The attributes of s:form (from the 7.4.0 tag library descriptor and the Tag Developers Guide)
Attribute Meaning and default

action

The action to submit to, without the .action suffix; defaults to the action that rendered the page (the current request’s alias)

namespace

The namespace of that action; defaults to the current namespace

method

The HTML method attribute (post when omitted, observed)

enctype

The HTML enctype; set multipart/form-data for a form with s:file (see the file upload page of this section)

acceptcharset

The HTML accept-charset attribute, rendered as accept-charset="UTF-8"

target

The HTML target attribute

includeContext

Whether the web application’s context path is put in front of the action URL; true by default

validate

Turns on the generated JavaScript validation of the xhtml and css_xhtml themes; deprecated since 7.4.0 (see the validation section below)

focusElement

The id of the element that receives the focus when the page has loaded (the xhtml template emits a small script)

onsubmit, onreset

JavaScript handlers; with validate="true" the validation call is appended to onsubmit

id, name

The id and the name of the form; both default to the action name with - and . turned into _

theme, template, templateDir, openTemplate

Theme and template selection; openTemplate names the template that writes the opening tag

labelPosition, requiredPosition, errorPosition, requiredLabel, labelSeparator

Layout defaults for the labelled fields inside (only labelPosition is inherited by the fields)

cssClass, cssStyle, style, cssErrorClass, cssErrorStyle, title, disabled, tabindex, accesskey

The usual HTML attributes of the form element

tooltip, tooltipConfig, tooltipCssClass, tooltipDelay, tooltipIconPath, javascriptTooltip

Tooltip defaults inherited by the fields (tooltipConfig is deprecated)

portletMode, windowState

Portlet attributes; unused outside a portlet

any other name

Written to the form element as it is (dynamic attributes allowed)

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:

Table 3. Common attributes of the UI tags (the labels need the xhtml or css_xhtml theme; the rest work in simple)
Attribute Effect

name

The field name, and, when value is absent, the property that supplies the value; also the key into the action’s field errors

value

A literal or an OGNL expression that overrides the property (%{...} forces evaluation; see the value stack page)

key

Takes name, label and value from one key: the name and value from the key as a property path, the label from the resource bundles (see the localization section below)

label, labelPosition, labelSeparator

The text of the label, its position (left or top) and the string after it (: by default)

requiredLabel, requiredPosition

Draw a star next to the label (true or false), on the left or the right (default right)

errorPosition

Put the field’s error rows above (top, default) or below (bottom) the field

id

The HTML id; generated as <formId>_<name> when omitted

cssClass, cssStyle, style, title, disabled, tabindex, accesskey

The HTML attributes of the input element

cssErrorClass, cssErrorStyle

Class and style that are added to the input when the field has an error

onclick, onchange, onfocus, onblur, onkeyup, …​ (the full set of on... events)

The HTML event attributes

theme, template, templateDir

Select another theme or another template for this one tag

tooltip, tooltipDelay, tooltipIconPath, tooltipCssClass, javascriptTooltip

The tooltip of the xhtml themes

any other name

Written to the input element (dynamic attributes), as placeholder, required, autofocus, data-*

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 &amp; 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.

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 &amp; 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">&lt;s:datetextfield/&gt;</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"/>
Table 4. The unticked checkbox, observed on an action whose inStock starts as true (a POST to the sticky mapping)
Rendered by the tag Posted by the browser with the box unticked inStock in the action

submitUnchecked absent (input only)

book.title=Dune

true (the old value stays; nothing says it was cleared)

submitUnchecked="true"

book.title=Dune and __checkbox_book.inStock=true

false

struts.ui.checkbox.hiddenPrefix set to struts_checkbox_ and submitUnchecked on

book.title=Dune and struts_checkbox_book.inStock=true

false

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 &amp; thriller"/><label for="save_misc_book_genrecrime">Crime &amp; 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

Table 5. What the list expression can be, with the options each form produced (observed, simple theme; the second column is the option value and text)
list and companions Elements Rendered options

list="genres" (a List<String>)

The element itself is key and text

<option value="sf" selected="selected">sf</option> and two more

list="genreMap" (a Map<String,String>)

The map key is the option value, the map value the text

<option value="sf" selected="selected">Science fiction</option>, Fantasy, Crime & thriller

list="genreObjects" listKey="code" listValue="label"

Genre objects with getCode() and getLabel()

The same three options

list="{'sf','fantasy'}"

An inline OGNL list

Two options whose value and text are the literals

list="#{'a':'A <b>','b':'B'}"

An inline OGNL map

<option value="a">A <b></option> and <option value="b" selected="selected">B</option> with value="'b'"

list="{}"

Empty

A select element with no option

list="genreMap" listTitle="key"

The key as the title of each option

<option value="sf" selected="selected" title="sf">Science fiction</option>

list="genreMap" value="'fantasy'"

An explicit value overrides the property

fantasy is the selected option

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="&quot;1&quot;"; 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 #{&quot;1&quot;:...} map with &quot;1&quot; 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

s:optiontransferselect

Two multi-select lists with ->, <-, -->>, <<-- and <*> (select all) buttons and ^/v buttons per list. name/list feed the left list, doubleName/doubleList the right one; leftTitle and rightTitle are the captions; every button label (addToRightLabel, addAllToLeftLabel, selectAllLabel, leftUpLabel, …​) and allowAddToLeft, allowAddAllToRight, allowSelectAll, allowUpDownOnLeft …​ switch the buttons off; a submit listener selects the options of both lists so that all of them are posted

s:inputtransferselect

A text input (<name>_input) with ->, <- and <<-- buttons that add what was typed to a list, remove the selected options or empty the list; allowUpDown adds the order buttons

s:updownselect

One multi-select list with ^ and v buttons that re-order its options and a * button that selects all; allowMoveUp, allowMoveDown, allowSelectAll and the ...Label attributes control the buttons

<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)

xhtml

One extra table row per message, placed above the field (errorPosition="top", the default) or below it (errorPosition="bottom"), and the label’s class changed from label to errorLabel: <tr errorFor="save_errors_book_title"> <td class="tdErrorMessage" colspan="2"><span class="errorMessage">Title is required</span></td></tr>

css_xhtml

A div with class wwerr above the label, one div class="errorMessage" per message inside it, the label class="errorLabel", and the input gets cssErrorClass and cssErrorStyle when given: <input type="text" name="book.title" value="" id="save_errors_css_book_title" class="bad" style="color:red"/>

simple

Nothing: the theme has no error layout, so the view calls s:fielderror fieldName="book.title" beside the field, and cssErrorClass is appended to the field’s class (class="x bad")

html5

No label and no error text: a post with an empty title rendered the bare <input type="text" name="book.title" value="" .../> (the theme’s controlheader and controlfooter templates are empty in 7.4.0), so errors are shown with s:fielderror

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 &quot;book.price&quot;.
</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.

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 (value, label, title, hidden value)

Escaped: value="<script>alert(1)</script> & "quotes""

Body of a textarea and the text of an option

Escaped: Spice & sand, Crime & thriller

The text of s:label and of a labelled field

Escaped: <label>&lt;script&gt;...</label>

Field errors, action errors and action messages

Escaped by default: a title typed as Xd<i>une came back in the message as Xd<i>une

The same messages with escape="false" on s:actionerror

Raw: <b>Xd<i>une</b> reached the page

The body of s:submit type="button"

Raw by default (<b>Rich</b> & body); escaped with escapeHtmlBody="true" (<b>Rich</b> & body)

The body of s:a with struts.ui.escapeHtmlBody=true

Escaped (<b>bold</b> &amp; body); the attribute escapeHtmlBody="false" on the tag brings the raw body back

The body of s:submit type="button" with struts.ui.escapeHtmlBody=true

Still raw (<b>Rich</b> body); only the tag attribute escaped it

escapeHtml="false" on s:textfield

No effect: the attribute is not declared, so it was written to the element (... escapeHtml="false"/>) and the value stayed escaped

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 &quot;book.price&quot;.</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.

Table 6. Struts 1 html tags and their Struts 2 counterparts
Struts 1 Struts 2 (7.4.0)

html:form action="/save"

s:form action="save" (an alias, not a path; no .do)

html:text, html:password, html:textarea, html:hidden, html:file

s:textfield, s:password, s:textarea, s:hidden, s:file

html:checkbox (the reset() method of the form bean cleared it)

s:checkbox with submitUnchecked and the checkbox interceptor

html:multibox (an array of checked values)

s:checkboxlist with a collection property and the hidden __multiselect_ field

html:radio (one tag per button)

s:radio (one tag for the whole group, from a list)

html:select with html:option, html:options or html:optionsCollection

s:select with list, listKey and listValue (and s:optgroup)

html:submit, html:cancel, html:reset, html:image

s:submit, s:submit with its own action (or a button name the action tests), s:reset, s:submit type="image"

html:errors

s:fielderror, s:actionerror and the error rows of the field tags

html:messages

s:actionmessage

html:link, html:rewrite

s:a and s:url (generic tags, see the tags overview page)

bean:message, bean:write, logic:iterate, logic:present

s:text, s:property, s:iterator, s:if (generic tags)

Pitfalls

Table 7. UI tag pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Struts 7.4.0 and Tomcat 11.0.26)
Symptom or error Cause Fix

Every field is empty although the action holds values, and the log has is not allowlisted warnings (observed: value="" for book.title and book.isbn)

The OGNL allowlist of Struts 7 does not cover the model class’s package

Add the package to struts.allowlist.packageNames (or the class to struts.allowlist.classes); see the value stack page of this section

A property that was true stays true after the user unticks the box (observed)

The 7.4.0 s:checkbox writes no hidden helper field unless submitUnchecked="true", so the unticked box posts nothing

Set submitUnchecked="true" on the tag, or the constant struts.ui.checkbox.submitUnchecked; keep the checkbox interceptor in the stack

A checkbox list cannot be cleared: the old selection returns (observed)

No __multiselect_ field reached the action (a hand-written form, or a stack without multiselect)

Use s:checkboxlist or s:select multiple="true", which write the field, and keep multiselect in the stack

A valid date comes back as 3/15/01 (observed), which a type="date" input cannot show

The property is written with the locale’s short format

Format the value (s:date ... var and value="%{#published}", or a getText pattern), and give the property a converter for that format

The form posts to a relative URL and its ids contain jsessionid (observed)

The action (or its namespace) is not a mapping in the configuration, so the tag uses the text as a URL and encodes it

Fix the name or the namespace; keep ids out of identifiers by giving the form an explicit id

The form posts to /save.action and the application answers 404 (observed)

includeContext="false" removed the context path

Remove the attribute unless the application is the root context

TypeError ... (reading 'save_combo_book_title') in the browser console (observed)

formName of a doubleselect or optiontransferselect is not the name of the form

Give the form name="shelfForm" and the tag formName="shelfForm"

The requested list key '...' could not be resolved as a collection/array/map/enumeration/iterator type (observed)

A method call such as .keySet() in a list expression is blocked by the OGNL allowlist

Expose a getter (getGenreNames()) and use it in list

Option text is empty (observed with listValue="value.size()")

The same: a method call in listValue is blocked

Use a property name that exists

The s:datetextfield shows a sentence about the JavaTemplates plugin (observed)

7.4.0’s FreeMarker templates do not render the tag

Use s:textfield type="date" with a formatted value, or the plugin in a simple-theme form

A duplicated class attribute on a checkbox list (observed: class="sf" class="c")

cssClass and listCssClass are both written; listCssClass is a property name of the list objects

Style the group through the form or a wrapper; give listCssClass the name of a property or leave it out

labelposition="top" shows up on the form element and nothing changes (observed)

The attribute is labelPosition; lower-case names are dynamic attributes

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 requiredLabel="true"; required="true" is the HTML attribute

Set requiredLabel="true" on each field

s:submit action="other" posts action:other, and the form just re-renders (observed)

The mapper ignores the action: and method: prefixes unless struts.mapper.action.prefix.enabled (and, for method:, struts.enable.DynamicMethodInvocation) are on

Prefer one mapping per button, or test the button’s name in the action

<script> or markup typed by a user appears escaped in a message (observed)

The messages are escaped by default

Keep it; use escape="false" only for markup that the application itself builds

Markup in the body of a button is not escaped (observed)

s:submit type="button" passes its body through unless escapeHtmlBody="true", and the global constant did not cover it

Set escapeHtmlBody="true" on the tag

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 s:form

Render the form from the validating action, or switch to the html5 theme constraints

Two fields have the same HTML id (observed with a repeated name)

The id is <formId>_<name>

Pass id explicitly

The page has no styles and StrutsUtils is undefined

s:head is missing from the head element

Add <s:head/>, or include /static/utils.js and your own stylesheet

A select shows no option as selected

The value is not in the list, or the list is built from objects without listKey

Check that listKey names the property that matches the field’s value

What Changed Recently

Table 8. Changes in the UI tags between the books' Struts 2.0 and Struts 7.4
Release Change

Struts 2.3.17

A labelPosition on s:form is propagated to the fields that do not set their own

Struts 2.6

FreeMarker auto-escaping is on; ?html in templates goes away, and the tags s:a, s:submit and s:component can escape their body with escapeHtmlBody or struts.ui.escapeHtmlBody

Struts 6.1.1

The constant struts.ui.checkbox.submitUnchecked sets the attribute submitUnchecked of s:checkbox globally

Struts 7.0

Java 17 and the Jakarta namespace; the classes of the tags and the interceptors live in org.apache.struts2.*; @StrutsParameter decides which request parameters (and so which field names) reach the action; the taglib URI stays /struts-tags

Struts 7.2.0

The html5 theme (simple markup, HTML5 input types) and the constant struts.ui.checkbox.hiddenPrefix (the alternative prefix struts_checkbox_)

Struts 7.4.0

HTML5 constraint validation for the html5 theme (struts.ui.html5.constraints, struts.htmlConstraintProvider); the generated-JavaScript validation of s:form validate="true" is deprecated (WW-5694) and scheduled for removal in 8.0.0 (WW-5696); in the 7.4.0 jar s:datetextfield renders only with the JavaTemplates plugin, and s:checkbox writes its hidden field only with submitUnchecked

Gone by 7.4.0

The Dojo-based ajax theme and its tags (s:datetimepicker, s:autocompleter, s:div, s:tabbedPanel): absent from the 7.4.0 tag library descriptor and from the jar’s template/ directory

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.

Table 9. Struts 2.0 and Struts 1 material against Struts 7.4
The book’s era Today (Struts 7.4)

Four themes simple, xhtml, css_xhtml and ajax, with the Dojo ajax tags

Four themes simple, xhtml, css_xhtml and html5; no ajax theme (theme="ajax" ended in HTTP 500 with Template not found for name "/template/ajax/text.ftl", observed) and no Dojo tags

s:datetimepicker, s:autocompleter, s:div, s:tabbedPanel

Gone; s:datetextfield needs the JavaTemplates plugin; use type="date" or your own JavaScript widget over a JSON endpoint

s:form validate="true" as the way to validate in the browser

Deprecated in 7.4, removed in 8.0; HTML5 constraints in the html5 theme

The checkbox tag always writes a hidden field

submitUnchecked="true" (or the global constant) is needed in 7.4.0

Imports and examples from com.opensymphony.xwork2 around the tags (ActionSupport for getText and field errors)

org.apache.struts2.ActionSupport

Any public setter fills the field name from the request

The @StrutsParameter rule: the setter or the getter of every field path must be annotated, and the OGNL allowlist must cover the model class

<%@ taglib prefix="s" uri="/struts-tags" %>

Unchanged

A Struts 1 ActionForm bean behind html:text property="title"; html:errors for all messages; html:multibox and html:options

The action or a model object behind s:textfield name="title"; the field tags show their errors; s:checkboxlist and s:select list

Form tags emit a table only in xhtml; a div layout needs css_xhtml

Same; simple and html5 emit the bare control

.action and .do URLs hard-coded in action

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 with submitUnchecked="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 (html5 since 7.2.0), and the html5 Theme page lists "Validation and error reporting" as a feature although its controlheader and controlfooter templates are empty.

  • The Form Tags page lists the general attribute as tabIndex; the tag library descriptor declares tabindex (a camel-case tabIndex is not a declared attribute).

  • The Form Tag page, and the descriptor of s:form, still say validate is "only useful with theme xhtml/ajax"; there is no ajax theme, 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 value does 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.theme set to simple.

  • 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 image as a valid type although the tag page lists two types.

References