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