Steven Pemberton, CWI Amsterdam
Version: 2020-08-23.
The XForms select
control allows you to select zero or more
items from a list.
For instance, you want to go shopping, and you can have a list of items you might want to buy, and you select the ones you actually want to buy:
<instance> <data xmlns=""> <shopping/> </data> </instance> ... <select ref="shopping"> <item><label>Bread</label><value>Bread</value><item> <item><label>Butter</label><value>Butter</value><item> <item><label>Milk</label><value>Milk</value><item> <item><label>Cheese</label><value>Cheese</value><item> <item><label>Bananas</label><value>Bananas</value><item> </select> <output ref="shopping" label="The result:"/>
The values of the items that you select get gathered together as a string, and stored in the location. Try it:
If the shopping
location initially contains values, the
matching items in the select are selected initially:
<instance> <data xmlns=""> <shopping>Butter Bananas</shopping> </data> </instance>
The list of items to be selected from doesn't need to be hard-wired into the
select
control like this, but can come from data. Since the
labels and values are the same in this case, we don't need to duplicate
them:
<instance id="items"> <items xmlns=""> <produce>Bread</produce> <produce>Butter</produce> <produce>Milk</produce> <produce>Cheese</produce> <produce>Bananas</produce> </items> </instance>
Alternatively, you can put the data in a file, and load it from there:
<instance id="items" src="items.xml"/>
Then the select
control uses this data in an
itemset
:
<select ref="shopping" appearance="full"> <itemset ref="instance('items')/produce"> <label ref="."/> <value ref="."/> </itemset> </select>
The result of course looks and acts exactly the same:
Now, to be perfectly honest with you, the reason that select
collects the result in a single string like this is because that is what HTML
did and does. But from a data-structuring point of view, instead of a result
like
<shopping>Bread Bananas</shopping>
it can be far more useful to have structured data, like
<shopping> <produce>Bread</produce> <produce>Bananas</produce> </shopping>
To achieve this we only have to change one thing: we change
<select ref="shopping" appearance="full"> <itemset ref="instance('items')/produce"> <label ref="."/> <value ref="."/> </itemset> </select>
to
<select ref="shopping" appearance="full"> <itemset ref="instance('items')/produce"> <label ref="."/> <copy ref="."/> </itemset> </select>
Then when an item is selected, rather than just the value of the element being saved, the whole element is saved.
This means that we also have to change how the result is output, from:
<output ref="shopping"/>
to:
<repeat ref="shopping/produce"> <output ref="."/> </repeat>
The list of things you potentially want to buy can get very long (we've kept it at 5 things here for brevity).
When you actually go shopping, what you really want is the much shorter list of just the things you have to buy, and even better, rather than just a list, a list that you can check off.
So what we do here is replace the output
of the selected
produce with another select
control that gets its itemset from the
list of things to buy. Instead of
<repeat ref="shopping/produce"> <output ref="."/> </repeat>
we use another select:
<select ref="bought" appearance="full"> <itemset ref="../shopping/produce"> <label ref="."/> <copy ref="."/> </itemset> </select>
(and add another location:)
<instance id="shopping"> <data xmlns=""> <shopping/> <bought/> </data> </instance>
Or to make it even more obvious what is happening: displaying the things you buy as you buy them:
<repeat ref="bought/produce"> <output ref="."/> </repeat>
But now a sneaky trick. Instead of the new select
control
referencing the bought
location, how about if we reference the
shopping list itself? Can you see what will happen?
<select ref="shopping" appearance="full"> <itemset ref="produce"> <label ref="."/> <copy ref="."/> </itemset> </select>
You may need a moment to understand what is happening here, but I hope you agree that it's pretty cool!
The final step is to make this into a shopping app.
We split the two cases, one selecting what you need to buy and the other for actually going to buy, into two cases of a switch:
<switch> <case id="add"> ... </case> <case id="buy"> ... </case> </switch>
each proceeded by a trigger that toggles the other case:
<trigger appearance="minimal"> <label>▸ buy</label> <toggle case="add" ev:event="DOMActivate"/> </trigger>
In full:
<switch> <case id="add"> <trigger class="line" appearance="minimal"> <label>▾ buy</label> <toggle case="buy" ev:event="DOMActivate"/> </trigger> <select ref="shopping" appearance="full"> <itemset ref="instance('items')/produce"> <label ref="."/> <copy ref="."/> </itemset> </select> </case> <case id="buy"> <trigger class="line" appearance="minimal"> <label>▸ buy</label> <toggle case="add" ev:event="DOMActivate"/> </trigger> <select ref="shopping" appearance="full"> <itemset ref="../shopping/produce"> <label ref="."/> <copy ref="."/> </itemset> </select> </case> </switch>
giving the final result: