« Back to home

Repeated XPages input controls with number-indexed field names

A common problem in interactive forms is to want a sequence of similar form fields. For example, a CD collection database will generally have a form allowing the user to enter the track details, with each track having its own editable text field.

In Domino, you often want to store the values in numbered fields in a single document, rather than as separate documents; for example, Track01, Track02, Track03 and so on. (Or, you might prefer Track_1, Track_2, etc.)

So the problem is: How do you lay out your form, following the DRY principle as much as possible? Here’s how.

First, you need to construct a custom control for whatever it is you need to repeat, to encapsulate that bit of complexity. Let’s imagine we’re building the CD database, and make a control that prompts for a track title. Mine has three custom properties defined.

The first is the field name it should use to store the input data. That’s just a string. Simple enough.


The second is sneakier. It’s a data source, which we’ll use to pass in the document that the field should be stored in. If you’re not familiar with that technique, check my article about passing document data to custom controls for more detail.


The third parameter is the row number, which in this case will be the CD track number.


You might wonder why I’ve made it a String. Surely row numbers are always integers? The answer is that by making it a string, I can allow the calling code to determine how the number is formatted. So potentially, the same custom control could be used whether you want the numbers to be 01, 02, 03, or whether you prefer 1, 2, 3. You could even pass in hexadecimal numbers if you want!

With the properties defined, the actual code for the custom control is pretty simple:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    <xp:label for="inputText1" id="computedField1" 
      value="Track #{compositeData.row}: " />
    <xp:inputText id="inputText1"
      value="#{compositeData.dataSource[compositeData.fieldName]}" />

Notice what I did with the xp:label element. The value is a string with an EL expression in the middle. The Domino Designer never generates code like this; if you rely on the GUI and specify an expression for your label, you’ll end up with this:

<xp:label for="inputText1" id="computedField1">
    <![CDATA[#{javascript:"Track " + compositeData.row + ": "}]]/>

It’ll still work, but you’ll be firing up the JavaScript interpreter and performing two string concatenations; whereas if you use EL, it’ll be a single JSF string splice in Java. Plus the code’s neater! Another small example of how a GUI is no substitute for knowing what you’re doing and writing the code yourself. The fact that you can interpolate values into the middle of a string attribute using EL will also become relevant again in a bit.

The value for the text field is a bit sneaky too. Remember that dataSource is going to be a passed in (wrapped) Domino document, and fieldName is going to be a String. So it’s just the bracket form of a JavaScript or EL property defined by the caller. If dataSource is passed in as the object document1, and fieldName as Track1, the expression in the inputText value parameter is equivalent to document1.Track1.

So far, so good, but you might wonder why the row number isn’t involved. To answer that, let’s look at how the calling code uses the control.

Let’s imagine we want 10 rows of track entry fields. Conveniently, XPages has a repeat control we can use. Let’s try some test code:

<xp:repeat indexVar="rownum" first="1" rows="10" 
value="#{javascript:10;}" var="junk">
 <p>All work and no play makes Jack a dull boy x<xp:text 

Normally xp:repeat is used to iterate over a collection of some kind, but in this case we simply want it to loop from 1 to 10. Instead of a collection value, we use the spectacularly undocumented feature that a number object N will be treated as if it was the collection [first..N]. So we’ll just pass in the value 10, though we do need to make sure it gets turned into a JavaScript object, because reasons. We set the variable used to hold the value to junk, because we’re not going to use it; we just want the row counter. (In this case it gets the same value as the row counter, but using the row counter is clearer to someone who doesn’t know about the subtle details of xp:repeat.)

If you try this code you’ll discover one of those random things that gives Java and XPages the reputation they have: You get 9 rows of output. Your value object needs to be at least 1 bigger than the maximum number of rows. I’d get the pitchfork and torch out, but I suppose since it’s an undocumented feature‚Ķ

So, here’s the final code for the XPage:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" 
    <xp:dominoDocument var="document1" formName="Album" />

  <p><xp:label value="Title:" id="title_Label1" for="title1" />
    <xp:br />
    <xp:inputText value="#{document1.Title}" id="title1" /></p>

  <xp:repeat indexVar="rownum" first="1" rows="10" var="data"
    <xc:track_row row="#{rownum}" fieldName="Name#{rownum}"
      dataSource="#{document1}" />

  <xp:br />
  <xp:button value="Submit" id="button1">
    <xp:eventHandler event="onclick" submit="true" refreshMode="complete"
      immediate="false" save="true" id="eventHandler1" />

I added an album title field and a submit button. It’s all pretty standard, except for the call to the composite control:

<xc:track_row row="#{rownum}" fieldName="Name#{rownum}"
  dataSource="#{document1}" />

I’m computing the fieldname in the calling code for flexibility, as mentioned earlier. And once again, I’m using a mid-attribute interpolation. Finally, as described in the previous article about passing data sources, I’m coercing document1 into being passed as an object by putting it in EL brackets.

So that’s it. Try it, and you should get a form with 10 CD track title entry fields, helpfully (and accessibly) labeled “Track 1” thru “Track 10”.

Extending this to allow the user to click a control to add and remove rows is fairly obvious, yes?