Level: Intermediate Jan J. Kratky (kratky@us.ibm.com), Advisory Software Engineer, IBM Steve K Speicher (sspeiche@us.ibm.com), Senior Software Engineer, IBM
21 Nov 2006 XForms provides many powerful mechanisms for working with XML data. One such mechanism is the "repeat" element,
which allows you to quickly and easily implement iteration over homogeneous data sets in your XML. In addition, you can format
the presentation of such sets as tables, as well as provide dynamic behavior like the insertion and deletion of specific
pieces of data within the repeating set. Read on for some tips and tricks on how to make the most of your XForms repeats.
What is an XForms repeat?
The repeat element allows you to map your XForms user interface (UI) elements to a homogeneous collection in your XForms
document's XML data instance. A "homogenous collection" is taken to be a series of nodes of the same datatype
at the same level in the document.
For example, in Listing 1, below, the set of bread elements is a homogeneous collection of nodes within the XML instance.
Listing 1. A homogeneous collection of XML nodes
<xforms:instance id="in-stock">
<in-stock>
<groceries>
<perishable>
<bread brand="Wonder" quantity="1"/>
<bread brand="Merita" quantity="2"/>
<bread brand="Arnold" quantity="4"/>
</perishable>
</groceries>
</in-stock>
</xforms:instance> |
To associate a repeat element with the homogeneous set, you simply refer to it
by specifying XPath in the nodeset attribute of a repeat element, as in Listing 2, below.
Listing 2. Binding a repeat to the homogeneous collection
<xforms:repeat id="bread-repeat" nodeset="instance('in-stock')/perishable/bread">
... |
Notice also in Listing 2 that the repeat was given an id value, so we can refer to it later
from other XForms constructs.
Now, you can place UI elements within the repeat element, and when you view the form
the UI elements will be "unrolled", so that one control will appear for
each member in the homogeneous set. Listing 3 shows an XForms input UI element
bound to an attribute within the homogeneous set from Listing 1. Notice
that the reference into the data instance from the input element is made relative to the nodeset
defined for the surrounding repeat.
Listing 3. UI elements within a repeat
<xforms:repeat id="bread-repeat"
nodeset="instance('in-stock')/groceries/perishable/bread">
<xforms:input ref="@brand">
<xforms:label>Brand of bread: </xforms:label>
</xforms:input>
</xforms:repeat> |
Listing 4 gives an indication of the "unrolled" version that
really would be rendered. (You would never see this source when developing the form; it's just here to demonstrate a renderer's
interpretation of the repeat construct.)
Listing 4. The repeat from Listing 3, unrolled
<xforms:input ref="instance('in-stock')/groceries/perishable/bread[1]/@brand">
<xforms:label>Brand of bread: </xforms:label>
</xforms:input>
<xforms:input ref="instance('in-stock')/groceries/perishable/bread[2]/@brand">
<xforms:label>Brand of bread: </xforms:label>
</xforms:input>
<xforms:input ref="instance('in-stock')/groceries/perishable/bread[3]/@brand">
<xforms:label>Brand of bread: </xforms:label>
</xforms:input> |
Figure 1 shows what this construct looks like when rendered in Mozilla Firefox with the XForms Extension
(see Resources for information on how to get the Firefox XForms Extension).
Figure 1. The repeat, rendered
Formatting repeats as tables
Very often, it makes sense to display data from a homogeneous set in a table format.
In the XForms Extension for Firefox, formatting repeats into a display having the appearance of a table
requires some work with cascading stylesheets (CSS).
First, the source in Listing 5 shows the markup in the body of the document, to which the CSS definitions will apply.
Note that the table and column headers are declared with XHTML markup. However, the "body" of the table consists
entirely of an XForms repeat.
Listing 5. Markup for presenting a repeat as a table
<xhtml:table>
<xhtml:tr>
<xhtml:thead>
<xhtml:th>Brand Name</xhtml:th>
<xhtml:th>Quantity</xhtml:th>
</xhtml:thead>
</xhtml:tr>
<xforms:repeat id="bread-repeat"
nodeset="instance('in-stock')/groceries/perishable/bread">
<xforms:output ref="@brand">
<xforms:label/>
</xforms:output>
<xforms:input ref="@quantity">
<xforms:label/>
</xforms:input>
</xforms:repeat>
</xhtml:table>
|
Listing 6 gives the style definition we'll use for our repeat. The CSS declarations are placed within an XHTML
style tag (you can also refer to CSS stored in an external file using the src
attribute on the style tag).
Listing 6. CSS for styling a repeat as a table
<xhtml:style type="text/css">
@namespace xforms url("http://www.w3.org/2002/xforms");
@namespace xhtml url("http://www.w3.org/1999/xhtml");
xhtml|th
{
width: 240px;
}
xforms|repeat .xf-repeat-item
{
display: table-row;
width: 480px;
}
xforms|repeat xforms|output, xforms|repeat xforms|input
{
display: table-cell;
border: thin;
border-style: solid;
width: 240px;
text-align: center;
background-color: lightgray;
}
</xhtml:style> |
Listing 6
approaches the styling by declaring that each item (that is, each row) in the repeat is to be displayed
as a row in a table. Then, the next declaration asserts that each XForms output and input control within
a repeat will be treated as a table cell, having a solid border and a light gray background.
Figure 2 shows the result when rendered in Mozilla Firefox with the XForms Extension.
Figure 2. A repeat rendered as a table
There are other stylings that could be used to get a similar effect in Firefox. Furthermore, other XForms renderers
might permit different approaches altogether. One example of an alternate approach is the use of some optional
attributes defined in the XForms 1.0 specification, particularly repeat-nodeset. These attributes
could be placed on a non-XForms element to define a repeating structure that applies to that element's children.
Listing 7 shows an example of the usage of repeat-nodeset on
the XHTML table element.
Listing 7. Using the repeat-nodeset attribute
<xhtml:table>
<xhtml:tr>
<xhtml:thead>
<xhtml:th>Brand Name</xhtml:th>
<xhtml:th>Quantity</xhtml:th>
</xhtml:thead>
</xhtml:tr>
</xhtml:table>
<xhtml:table xforms:repeat-nodeset="instance('in-stock')/groceries/perishable/bread">
<xhtml:tr>
<xhtml:td>
<xforms:output ref="@brand">
<xforms:label/>
</xforms:output>
</xhtml:td>
<xhtml:td>
<xforms:input ref="@quantity">
<xforms:label/>
</xforms:input>
</xhtml:td>
</xhtml:tr>
</xhtml:table>
|
Using the repeat attributes on non-XForms elements is an elegant approach. It's supported by some XForms implementations,
including the XForms Extension for Firefox.
Inserting and deleting rows
So far, we've looked at presenting repeating structures as they exist in the XML data instance. However, XForms allows users to do much, much
more with that data. In particular, it's very common to want to add a new row to a table, or delete an existing row.
Listing 8 shows the markup for permitting the user to trigger deletion and insertion of rows in your repeating set.
Listing 8. Enabling insert and delete of rows
<xforms:trigger>
<xforms:label>Insert Row</xforms:label>
<xforms:insert ev:event="DOMActivate"
at="index('bread-repeat')" position="after"
nodeset="instance('in-stock')/groceries/perishable/bread"/>
</xforms:trigger>
<xforms:trigger>
<xforms:label>Delete Row</xforms:label>
<xforms:delete ev:event="DOMActivate"
at="index('bread-repeat')"
nodeset="instance('in-stock')/groceries/perishable/bread"/>
</xforms:trigger>
|
Listing 8 introduces the concept of a repeat "index". Notice
the use of the index() function within the at attribute of the
insert and delete actions. The value given in the value of
the at attribute
represents the 1-based index into the repeat's data set at which the insert or delete will occur.
By using the index() function within the value of this attribute, we are specifying that the
insert or delete will occur at the currently selected row in the repeat. In the case of the delete action,
this means that the currently selected row will be deleted. For the insert action, this means that the
new row will be inserted either directly before or after the row at the selected index. Whether the new row is inserted
before or after the currently selected row depends on the value given in the insert element's
position attribute.
When an insert action is triggered, the last row of the repeat is cloned, and it is that clone of the
repeat that is inserted. This generally means that the values for the elements and attributes of that row
are brought along as well. As this may not be the desired behavior, you may want to follow an insert
with a setvalue action to reset the value, as in Listing 9. The index()
function is again used to specify the currently-selected row of the repeat as the one on which
the setvalue action will occur. This suits the intended purpose -- to target the just-inserted row -- since the insert action
resets the repeat's index to be that of the just-inserted row.
Listing 9. Resetting the inserted value
<xforms:trigger>
<xforms:label>Insert Row</xforms:label>
<xforms:action ev:event="DOMActivate">
<xforms:insert at="index('bread-repeat')"
position="after"
nodeset="instance('in-stock')/groceries/perishable/bread"/>
<xforms:setvalue
ref="instance('in-stock')/
groceries/perishable/bread[index('bread-repeat')]/@quantity"
value="0"/>
</xforms:action>
</xforms:trigger>
|
Implementing triggers for inserting and deleting rows in a repeat is a very common pattern. In fact,
automated XForms-generation tools, specifically the IBM® XML Forms Generator (see Resources) will automatically generate
triggers for addition and deletion with every repeat.
Preventing deletion of the last row
In the previous section, we established that an insert action clones the last row in the repeat.
This is very bad news when the repeat no longer has any rows, possibly because the user has
triggered delete actions on all of them. The net result is that, in the earlier examples,
if all rows are deleted, the insert action will no longer do anything.
Therefore, you may wish to prevent deletion of the last row. Listing 10
shows one approach, which is to bind the "Delete" trigger to a node in the data model,
and then to make that node non-relevant when the count of nodes in the set reaches 1.
Listing 10. Preventing deletion of the last row
<xforms:instance id="trigger-controller">
<trigger-sets>
<trigger-set>
<insert-trigger/>
<delete-trigger/>
</trigger-set>
</trigger-sets>
</xforms:instance>
<xforms:bind nodeset="instance('trigger-controller')/trigger-set/delete-trigger"
relevant="count(instance('in-stock')/groceries/perishable/bread) > 1"/>
...
<xforms:trigger ref="instance('trigger-controller')/trigger-set/insert-trigger">
<xforms:label>Insert Row</xforms:label>
<xforms:insert ev:event="DOMActivate" at="index('bread-repeat')"
position="after"
nodeset="instance('in-stock')/groceries/perishable/bread"/>
</xforms:trigger>
<xforms:trigger ref="instance('trigger-controller')/trigger-set/delete-trigger">
<xforms:label>Delete Row</xforms:label>
<xforms:delete ev:event="DOMActivate" at="index('bread-repeat')"
nodeset="instance('in-stock')/groceries/perishable/bread"/>
</xforms:trigger>
|
When the node referenced by the trigger becomes non-relevant, the trigger becomes "unavailable".
In Firefox, the default behavior is for the button to disappear from the page, as shown in
Figure 3, which shows the form before and after the next-to-last row is deleted.
Figure 3. Preventing deletion of the last row
You may wish to experiment with other approaches using the "relevant" property; for
example, always making the last row of the repeat non-relevant. That way, the last row can never be
selected, and therefore can never be deleted when you are depending on the repeat index
to determine the delete location.
Listing 11 shows how you might add a "prototype" entry to your data instance,
and then add binds to make that last entry always non-relevant. To ensure that the last
row is never displayed, a new CSS entry is made, keying on the ".disabled" CSS cue.
The repeat element and its contents require no change at all for this approach.
Listing 11. Data with two-deep nesting
<xforms:model>
<xforms:instance id="in-stock">
<in-stock>
<groceries>
<perishable>
<bread brand="Wonder" quantity="1" />
<bread brand="Merita" quantity="2" />
<bread brand="Arnold" quantity="4" />
<bread brand="Not sure yet" quantity="0" />
</perishable>
</groceries>
</in-stock>
</xforms:instance>
<xforms:bind nodeset="instance('in-stock')/groceries/perishable/bread
[count(instance('in-stock')/groceries/perishable/bread)]/
@brand"
relevant="false()"/>
<xforms:bind nodeset="instance('in-stock')/groceries/perishable/bread
[count(instance('in-stock')/groceries/perishable/bread)]/
@quantity"
relevant="false()"/>
</xforms:model>
<xhtml:style type="text/css">
@namespace xforms url("http://www.w3.org/2002/xforms");
xforms|*:disabled { display: none; }
</xhtml:style>
|
For now, using a technique like this is necessary to prevent deletion of your insert prototype.
However, there is help on the horizon. The XForms 1.1 specification will address this
and other shortcomings with the insert and delete actions, making workarounds like the ones shown here
no longer necessary.
Nesting repeats
The power of XForms really shows itself when you have data that contains homogeneous data
sets at different levels, with one nested within the other. For example, Listing 12
shows a data instance in which a top-level homogeneous set of elements consists of the three
item elements. Then, each item element contains a set of its own
info elements.
Listing 12. Data with two-deep nesting
<xforms:instance id="in-stock">
<in-stock>
<groceries>
<perishable>
<item type="bread">
<info brand="Wonder" quantity="1"/>
<info brand="Merita" quantity="2"/>
<info brand="Arnold" quantity="4"/>
</item>
<item type="peanut butter"">
<info brand="Skippy" quantity="2"/>
<info brand="Jif" quantity="1"/>
</item>
<item type="jelly">
<info brand="Smuckers" quantity="1"/>
<info brand="Welch's" quantity="3"/>
</item>
</perishable>
</groceries>
</in-stock>
</xforms:instance>
|
XForms makes it easy to iterate over nested sets like this through the nesting of the corresponding repeats.
Listing 13 shows how to do it.
Listing 13. Defining nested repeats
<xforms:repeat id="bread-repeat" nodeset="instance('in-stock')/groceries/perishable/item">
<xforms:output ref="@type">
<xforms:label>Item type: </xforms:label>
</xforms:output>
<xforms:repeat id="bread-repeat" nodeset="info">
<xforms:output ref="@brand">
<xforms:label>Brand: </xforms:label>
</xforms:output>
<xforms:input ref="@quantity">
<xforms:label>Quantity: </xforms:label>
</xforms:input>
</xforms:repeat>
<xhtml:hr/>
</xforms:repeat>
|
Listing 13 defines an output within the outermost repeat, for which each item type is
displayed. Then, an inner repeat iterates over the info elements within each item.
Notice that the XPath in the inner repat's nodeset attribute is defined relative to the
nodeset of its containing repeat. Figure 4 shows how these nested repeats would look in Firefox.
Figure 4. The nested repeat, rendered
The power of XForms shows itself once again with the fact that there are no limits to the depth with which
you can nest repeats. The only constraints are renderer performance and the desire to maintain a clean,
easy-to-navigate interface for the end-user.
Managing a repeat's index
During our discussion of the insert and delete actions,
we touched on the concept of the repeat's index -- that is, the one-based index of
the currently selected "row".
The user may make the selection, or you can force a selection programmatically in
one of two ways.
Firstly, you can specify the index of the repeat at the time the form is first rendered
by specifying an integer value for the attribute of repeat called startindex.
Unlike some of the other XForms facilities for dealing with indices, you cannot specify an XPath
expression here; according to the XForms 1.0 specification, only a positive integer value will do.
Secondly, you may also set the index of a repeat with the setindex action in response to
the user activating a trigger, or perhaps as part of some other action (for example, resetting
the index to a desired value after an insert or delete). Listing 14
gives some markup which initially sets the index to 2, and then allows the user to enter a value
for the index and set it to that value by activating the trigger labeled "Select".
Listing 14. Using setindex
<xforms:instance id="target-index">
<target-index>
<value>1</value>
</target-index>
</xforms:instance>
...
<xhtml:table>
<xhtml:tr>
<xhtml:thead>
<xhtml:th>Brand Name</xhtml:th>
<xhtml:th>Quantity</xhtml:th>
</xhtml:thead>
</xhtml:tr>
<xforms:repeat id="bread-repeat"
nodeset="instance('in-stock')/groceries/perishable/bread"
startindex="2">
<xforms:output ref="@brand">
<xforms:label/>
</xforms:output>
<xforms:input ref="@quantity">
<xforms:label/>
</xforms:input>
</xforms:repeat>
</xhtml:table>
<xforms:input ref="instance('target-index')/value">
<xforms:label>Target Index: </xforms:label>
</xforms:input>
<xforms:trigger>
<xforms:label>Select</xforms:label>
<xforms:setindex ev:event="DOMActivate"
repeat="bread-repeat"
index="instance('target-index')/value"/>
</xforms:trigger>
|
In Listing 14, a data instance has been added to hold the desired value
of the repeat index. The input control is bound to this data, so whenever a user enters a new value,
that value gets placed in the value element. Finally, the setvalue action
tied to the trigger references that same value element in its index
attribute, which can take an XPath expression. Figure 5 shows the resulting form in Firefox.
Figure 5. Allowing the user to input the repeat index
Figure 5 shows the selected row having a different appearance from the other rows.
Without a visual cue like this, it's likely to be unclear to the user which row is currently selected.
You can use CSS to provide the user with an indicator of the currently selected row.
In Firefox, CSS such as that shown in Listing 15 targets the
selected index.
Listing 15. Styling for the repeat index
xforms|repeat .xf-repeat-index > xforms|*
{
color: white;
background-color: black;
border: none;
}
|
The number attribute
The number attribute on the XForms repeat element is a hint to the renderer as to the
number of repeat items (or rows) to display at any one time. Support for this attribute is optional for
XForms processors, so many renderers do not support it. However, some renderers, such as the FormsPlayer plug-in for
Microsoft® Internet Explorer (see Resources), do support the number attribute.
Use of the number attribute can permit a cleaner presentation, particularly of repeats over
very large sets of data. It may also improve performance, as not every item in the repeat
will need to have its associated controls redrawn when the data changes.
Let's say that, using the data instance from Listing 1, we want only one
row of data to be presented at a time. We would simply add a value of 1 to the repeat
element as defined in Listing 2. The result would be as shown in Listing 16.
Listing 16. Applying the number attribute to the repeat
<xforms:repeat id="bread-repeat"
nodeset="instance('in-stock')/groceries/perishable/bread"
number="1">
... |
Now, it's great that so much space is saved by showing only one repeat row at a time, but a user of this form
may want to manipulate the data for a row other than the one initially shown. You can give users the opportunity
to access other repeat items by allowing the user to set the current index of the repeat.
Listing 17 shows how to enable this by leveraging the setindex action.
Listing 17. Allowing navigation of the repeat
<xforms:trigger>
<xforms:label>< < Previous</xforms:label>
<xforms:setindex ev:event="DOMActivate" repeat="bread-repeat"
index="index('bread-repeat') - 1"/>
</xforms:trigger>
<xforms:trigger>
<xforms:label>Next > ></xforms:label>
<xforms:setindex ev:event="DOMActivate" repeat="bread-repeat"
index="index('bread-repeat') + 1"/>
</xforms:trigger>
|
Listing 17 provides buttons labeled "< < Previous"
and "Next > >", which when pressed will decrement and increment, respectively,
the index of the repeat. This allows the form's user to scroll back and forth through the data in the
repeat.
Figure 6 shows what our scrollable repeat would look like in Internet Explorer,
using the FormsPlayer plug-in.
Figure 6. The scrollable repeat
The power of XForms
XForms repeats are one of the more powerful constructs in XForms. If you have not done so already,
go ahead and grab a renderer -- such as the free download of the XForms Extension for Mozilla Firefox
(see Resources) -- and try
these techniques out for yourself.
Resources Learn
- Dig into XForms at the home page of W3C XForms,
which has links to the official XForms specification, as well as to a wide variety of XForms rendering options.
- Find out more about XHTML, Cascading Style Sheets (CSS), XML, XML Events, XPath,
and other related standards at the W3C site.
Get products and technologies
-
Mozilla XForms:
Render your standards-compliant forms in Mozilla Firefox using this plug-in.
-
Get MozzIE, an open-source control that allows you to render XForms
in Internet Explorer.
-
Try FormsPlayer, a plug-in for Internet Explorer that renders XForms.
-
XML Forms Generator:
Create functional, standards-compliant forms with a click of the mouse using this Eclipse-based tool from alphaWorks.
-
Visual XForms Designer:
Check out the home page, with links to installation instructions, prerequisites, and the forum.
-
Compound XML Document Toolkit:
Explore other open-standard XML markups, including Scalable Vector Graphics (SVG), MathML, VoiceXML, and Synchronized Multimedia Integration Language (SMIL).
Discuss
-
Get answers to your questions about the Visual XForms Designer
on its discussion forum.
About the authors  | |  | Jan Joseph Kratky is the lead developer for the Compound XML Document Editor and XML Forms Generator. Currently a software engineer with IBM Emerging Software Standards in Research Triangle Park, North Carolina, he holds a B.A. from Cornell University and an M.S. from Rensselaer Polytechnic Institute. A Sun Certified Java Programmer and Sun Certified Web Component Developer, Jan has worked with Java technologies since 1997, and with Eclipse technologies since 2001. |
 | |  | Steve Speicher, the lead developer of the Compound XML Document Toolkit, is an IBM senior software engineer working on emerging standards. Mr. Speicher is a member of the W3C Compound Document Formats Working Group; he uses Model-Driven Development (MDD) to improve the development of standards. He has previously worked on "build" and SCM tools in the Rational division and in IBM internal tools. |
Rate this page
|