How-to guides‎ > ‎XForms Logic‎ > ‎

Insert a new item into a repeat

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> element
  • nodeset 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> element
  • nodeset 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> element
  • nodeset 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> element
  • nodeset 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

Comments