The public Forms working group mail list has been getting some traffic recently about a correction we made to XForms 1.0 in the second edition. Under the original recommendation, XForms processors had to take a snapshot of initial instance data before any data is filled in so that the empty version of the data can be used to insert new rows of data into a repeat. This is an idea that looks elegant at first, but it breaks down really quickly.
In language design, it is important to have a balance between the appearance of elegance and elegance in fact. Keep things as simple as possible, but no simpler! The initial data snapshot idea is too simple because it works in some cases, in others it only works with a highly sophisticated implementation, and in some reasonable use cases, the insert simply could not act as specified if the initial data snapshot method is used. For example, if the initial instance data did not contain the prototype because the repeating data is obtained by a submission after the form starts, then the insert would fail.
Due to these difficulties, many processors did not even implement the idea. Instead, they were implementing the insert action by making a copy of the last node in the currently running instance data. To correct the design and reflect the will of the XForms community, the recommendation was amended to say that insert creates a new node by copying the last node of its nodeset.
At the same time, XForms was also corrected to say that insert and delete perform no operation when their nodesets resolve to empty nodeset. It seems obvious to say this because if there are no nodes in the delete nodeset, then there is nothing to delete, and if there are no nodes in the insert nodeset, then you wouldn't know where to put a newly duplicated node even if you had one. However, we decided to explicitly say this so that all implementors would know to check for this condition rather than, say, assume they should fail with a binding exception. The reason is that the no-op behavior actually comes in quite handy in solving the full spectrum of use cases for repeats.
Here is an example. It shows how to manipulate a repeat that starts empty and is allowed to become empty and non-empty during run-time. Like a shopping cart. It starts empty, you add stuff to it, then you change your mind and delete the shopping cart items, and then you choose more stuff to buy.
<!-- Initial instance data contains the prototypical node as the last element -->
<xf:instance xmlns=""> <cart> <item> <name/> ... </cart></xf:instance>...
<!-- repeat omits the prototypical node -->
<xf:repeat nodeset="item[position()!=last()]" id="repeat-cart"> ...</xf:repeat>
<!-- Add new row after any current row, but do it in a way that can also handle zero rows. -->
<xf:trigger> <xf:label>Insert <xf:insert ev:event="DOMActivate" nodeset="item" at="index('repeat-cart')+1" position="before"/></xf:trigger>
<!-- Delete a row from the repeat. If only the prototypical data remains then the nodeset becomes empty and the action has no effect. -->
<xf:trigger> <xf:label>Delete <xf:delete ev:event="DOMActivate" nodeset="item[position()!=last()]" at="index('repeat-cart')"/></xf:trigger>
The above example showed that having the delete action resolve to no-operation when its nodeset is empty is very useful. Now, let's see an example in which it is useful for insert to have no effect on empty nodeset. Suppose that instead of a zero row repeat, you want a repeat that stays at one row. If the user hits delete for that row, then the row stays, but the data is cleared from it.
<xf:trigger> <xf:label>Delete <xf:action ev:event="DOMActivate"> <xf:delete nodeset="item[position()!=last()]" at="index('repeat-cart')"/> <xf:insert nodeset="item[last()=1]" at="1" position="before"/> </xf:action></xf:trigger>
Here the delete trigger could be activated a number of times, but it will only deleted down to the last row of data that the repeat is actually showing. When the number of elements drops to just the original prototypical data, further pressing of the Delete trigger produces an empty nodeset, so the prototypical data is not removed. The insert follows that up with a nodeset that resolves to empty unless the only data left is the prototypical data. If there is anything more, the insert has no effect. But when only the prototype is left over, we make a new copy of it, which presents a single empty row of data to the user.
The neat thing here is that the above is a general method in which a constant number of XForms actions achieve the desired result regardless of the schema for the data. The data may have inner sequences that correspond to inner repeats in the UI, and you can still use this method on the nodes bound to the outer repeat because the last row of the outer repeat is deleted no matter how many rows are on its inner repeats, and then the insert puts in place a copy of the prototypical data in which node sequence for the inner repeat is in the initial state.
So, you can create repeats that can start and return to the empty state, create repeats that enforce a one row minimum, you can create repeat/insert/delete constructs that operate correctly regardless of when the data is obtained in the lifecycle of the form, and especially, you can create repeat/insert/delete constructs that operate correctly in the save/reload case (e.g. in the presence of prepopulation data). And you can do it without using special attributes being added to XForms 1.1. You can do it today, right now, in XForms 1.0.[Read More]