The problem
XForms has some powerful constructs to repeat over collections of items and show them to the user:
- The
<xforms:repeat>
control allows you to repeat over items (XML elements or attributes) in a collection.
- The
<xforms:insert>
action allows you (among other things) to add items to a collection.
It is not always easy to figure out how to add items to such a collection, and especially to add them precisely where you would like it! This how-to shows exactly this.
The solution
Initial idea
As an example, let's start with a simple instance repeating over elements called
<fruit>
:
<xforms:instanceid="fruits">
<fruits xmlns="">
<fruit>Apple</fruit>
<fruit>Orange</fruit>
<fruit>Pear</fruit>
</fruits>
</xforms:instance>
In the UI, you repeat over those and present an input field for each fruit, to allow editing the fruit name:
<xforms:repeat nodeset="fruit" id="fruit-repeat">
<xforms:input ref="."/>
<br/>
</xforms:repeat>
Now add a button (
<xform:trigger>
) which, when pressed, inserts a new fruit:
<xforms:trigger>
<xforms:label>Add fruit</xforms:label>
<xforms:insert ev:event="DOMActivate" ...
/>
</xforms:trigger>
Now the trick is to fill-out the "..." with the proper attributes! The
<xforms:insert>
action is very flexible and this makes it sometimes a little hard to use. But in fact, it is not so complicated.
The <xforms:insert> action
The idea of this action is that you specify:
- which containing element you are inserting into
- which elements you are inserting next to
- and whether would like to insert before or after the element specified
- what you insert
Now you might wonder why we need both an
into and a
next to: the answer is that you actually don't always need this distinction. But suppose all the
<fruit>
elements are now removed: now you can no longer insert
next to, and have to insert
into the containing
<fruits>
element. So by using both these concepts, you cover all the cases.
Here, we would like to insert:
- into the element called
<fruits>
- next to one of the
<fruit>
elements - before or after said
<fruit>
element - a new empty
<fruit>
element
The corresponding attributes are:
- The
context
attribute specifies the "into" part. - The
nodeset
and at
attributes specify the "next to" part. - The
position
attribute specifies the "before or after" part. - The
origin
attribute specifies the "what" part
First, let's create an instance containing a template for "what" will be inserted:
<xforms:instance id="new-fruit">
<fruit xmlns=""/>
</xforms:instance>
NOTE: Instead of creating a separate instance, you can also use the xxforms:element() function to dynamically create a new element.The repeat index
Each
<xforms:repeat>
control has an associated index, representing the idea of a currently selected iteration or "row" in the user interface.
This information can be used:
- In CSS, for example to highlight the current iteration.
- Through the XPath
index()
function, for example to specify an insertion position.
This index must not be confused with the
position of a given iteration, accessible through the
position()
function. In the following example, each row's position is output: 1, 2, and 3:
<xforms:repeat nodeset="fruit" id="fruit-repeat">
<xforms:output value="position()"/>
<xforms:input ref="."/>
<br/>
</xforms:repeat>
On the other hand, the
current index is 2 because the second row was selected by the user. This means that the
index()
function returns 2 at this particular time, while in each row the
position()
function returns a difference value: 1, 2, or 3.
Inserting at the end
Button and action:
<xforms:trigger>
<xforms:label>At the end</xforms:label>
<xforms:insert ev:event="DOMActivate"
context="." nodeset="fruit"
origin="instance('new-fruit')"/>
</xforms:trigger>
Explanation:
context
points to ".", which is the <fruits>
elementnodeset
points to all the nested <fruit>
elements- by default, without specifying
at
or position
, the action inserts after the last element, so after the last <fruit>
element
You could also write:
<xforms:trigger>
<xforms:label>At the end</xforms:label>
<xforms:insert ev:event="DOMActivate"
context="." nodeset="fruit" at="last()" position="after"
origin="instance('new-fruit')"/>
</xforms:trigger>
Or even:
<xforms:trigger>
<xforms:label>At the end</xforms:label>
<xforms:insert ev:event="DOMActivate"
context="." nodeset="fruit[last()]"
origin="instance('new-fruit')"/>
</xforms:trigger>
Result:
<xforms:instanceid="fruits">
<fruits xmlns="">
<fruit>Apple</fruit>
<fruit>Orange</fruit>
<fruit>Pear</fruit>
<fruit/>
</fruits>
</xforms:instance>
Inserting at the beginning
Button and action:
<xforms:trigger>
<xforms:label>At the beginning</xforms:label>
<xforms:insert ev:event="DOMActivate"
context="." nodeset="fruit" at="1" position="before"
origin="instance('new-fruit')"/>
</xforms:trigger>
Explanation:
context
points to ".", which is the <fruits>
elementnodeset
points to all the nested <fruit>
elements- among all the
<fruit>
elements, the action picks the first one with the at
attribute - it inserts before it with the
position
attribute
Result:
<xforms:instanceid="fruits">
<fruits xmlns="">
<fruit/>
<fruit>Apple</fruit>
<fruit>Orange</fruit>
<fruit>Pear</fruit>
</fruits>
</xforms:instance>
Inserting after the current index position
Button and action:
<xforms:trigger>
<xforms:label>After position <xforms:output value="index('fruit-repeat')"/></xforms:label>
<xforms:insert ev:event="DOMActivate"
context="." nodeset="fruit" at="index('fruit-repeat')"
origin="instance('new-fruit')"/>
</xforms:trigger>
Explanation:
context
points to ".", which is the <fruits>
elementnodeset
points to all the nested <fruit>
elements- among all the
<fruit>
elements, the action picks the one corresponding to the current repeat index with the at
attribute - it inserts after it since it is lacking a
position
attribute
Result, if the current index was 2:
<xforms:instanceid="fruits">
<fruits xmlns="">
<fruit>Apple</fruit>
<fruit>Orange</fruit>
<fruit/>
<fruit>Pear</fruit>
</fruits>
</xforms:instance>
Inserting before the current index position
Button and action:
<xforms:trigger>
<xforms:label>Before position <xforms:output value="index('fruit-repeat')"/></xforms:label>
<xforms:insert ev:event="DOMActivate"
context="." nodeset="fruit" at="index('fruit-repeat')" position="before"
origin="instance('new-fruit')"/>
</xforms:trigger>
Explanation:
context
points to ".", which is the <fruits>
elementnodeset
points to all the nested <fruit>
elements- among all the
<fruit>
elements, the action picks the one corresponding to the current repeat index with the at
attribute - it inserts before it with the
position
attribute
Result, if the current index was 2:
<xforms:instanceid="fruits">
<fruits xmlns="">
<fruit>Apple</fruit>
<fruit/>
<fruit>Orange</fruit>
<fruit>Pear</fruit>
</fruits>
</xforms:instance>
Run it and get the source