Steven Pemberton, CWI Amsterdam
Version: 2022-01-28.
In part 1 we saw how to edit a list, add items, delete items, and save and restore the data.
In this part we are going to make the lists a little more sophisticated: list items will be selectable, and then you will be able to view only those selected.
For a shopping list, for instance, you would be able to keep a list of all the things you might be liable to buy, and then when you notice you need to buy an item, you only have to select it. Then when you go to the shop, you view just those selected, and unselect them as you buy.
Or a to-do list: you can select the things you plan to do today, and then deselect them as you do them.
So the data now has an attribute that marks items as selected:
<list name="Shopping"> <item selected="true">Bananas</item> <item selected="false">Apples</item> <item selected="true">Milk</item> <item selected="false">Yoghurt</item> </list>
We define the type of the new attribute:
<bind ref="item/@selected" type="boolean"/>
and display the result:
<group> <label><input ref="@name"/></label> <repeat ref="item"> <input ref="@selected"/> <input ref="."/> </repeat> </group>
Which, with suitable CSS, looks like this:
Notice how an input
for a boolean value takes the form of a
check box.
The next step is to switch between displaying all, and displaying only the selected elements. Firstly we add an attribute to record whether we want to display all elements or just the selected ones:
<list name="Shopping" showselected="false"> <item selected="true">Bananas</item>
and mark it as a boolean:
<bind ref="@showselected" type="boolean"/>
Then we use the relevance
property: if a value is relevant it
is displayed, and if not, it isn't.
Selected items will always be displayed, but non-selected items will only be displayed if the value for only displaying selected values is false:
<bind ref="item" relevant="@selected=true() or ../@showselected=false()"/>
We'll also add an input for the show selected value:
<label> <input ref="@show"><hint>show only selected</hint></input> <input ref="@name"/> </label>
looking like this:
A problem with this style of list is that it quickly becomes very long and unmanagable: "I know there is an entry for what I want to select, but where is it?".
So we'll structure the list into groups of entries:
<list name="Shopping" showselected="false"> <group name="dairy"> <item selected="true">milk</item> <item selected="false">butter</item> <item selected="true">cream</item> <item selected="false">yoghurt</item> </group> <group name="fruit"> <item selected="true">bananas</item> <item selected="true">apples</item> <item selected="false">pears</item> <item selected="false">peaches</item> </group> </list>
Change the binds to match the new structure:
<bind ref="group/item/selected" type=boolean"/>
and display it:
<group> <label> <input ref="@showselected"><hint>show only selected</hint></input> <input ref="@name"/> </label> <repeat ref="group"> <group> <label><input ref="@name"/></label> <repeat ref="item"> <input ref="@selected"/> <input ref="."/> </repeat> </group> </repeat> </group>
looking like this:
We'll add the code necessary to add new items, just like in part 1, but just for the fun of it, we'll do deletions in a different way: if the item name is empty, and it's not the last item of the group, we delete it. To do this, we listen for the value-changed event, and if the value is empty, and it's not the last item in the group, we delete the item:
<input ref="."> <action ev:event="DOMActivate"> <insert ref="." origin="instance('blank')/group/item"/> </action> <action ev:event="xforms-value-changed"> <delete ref="." if=". = '' and count(../item) != 1"/> </action> </input>
While we're at it, we'll add similar code to add and delete groups. We delete a group if its name is empty, and if the first item's name is empty (because of how we delete items, it is only possible for an item's name to be empty if it is the only one in a group):
<input ref="@name"> <action ev:event="DOMActivate"> <insert ref=".." origin="instance('blank')/group"/> </action> <action ev:event="xforms-value-changed"> <delete ref=".." if="@name='' and item[1]=''"/> </action> </input>
Can you see why we need to use ".." instead of "."?
[Not working yet]
There's an issue of what to display when you only want to look at the selected items, and there are none in a group: currently the name of the group will be displayed, with no items under it. However, if we want to completely ignore groups that have no selected items, we'll apply relevance to groups as well. It is very similar to the relevance we use for items, a group is always relevant if it has any selected items, and otherwise if the value for showing only selected items is false:
<bind ref="group" relevant="count(item[@selected=true()] > 0 or ../@showselected=false()"/>
Next collapsible groups. Watch what happens when you click on the little black triangles:
We give each group an extra attribute that says whether it is open or closed:
<group name="dairy" open="true"> <item selected="true">milk</item> ...
Declare its type as before:
<bind ref="group/@open" type="boolean"/>
This time though, instead of using an input
to change its
value, we'll use a trigger:
<label> <trigger appearance="minimal"> <label><output value="if(@open=true(), '⏷', '⏵')"/></label> <action ev:event="DOMActivate"> <setvalue ref="@open" value="not(boolean-from-string(.))"/> </action> </trigger> <input ref="@name" id="G"> ...
The only other thing we need to do is change the relevance for
item
s. An item will be visible if the "show only selected" value
has been selected and the item in question has been selected, or otherwise if
the open
value is true:
<bind ref="group/item" relevant="(../../@showselected=true() and @selected=true()) or (../../@showselected=false() and ../@open=true())"/>
In part 1, we showed how to save data, and to record when it had been successfully saved. What we didn't do was show how to deal with saves that fail.
Why might they fail? The internet might be down, or on a mobile device you may (briefly) have no connectivity; the server might have crashed; maybe you don't have the authority to save to that server. And dozens of other possibilities.
The important thing is let the user see that the save was attempted and had failed, so that they don't think that the data is safe.
The submission
element that we used in part 1 looked like
this:
<submission method="put" resource="http://lists.example.com/saves/list.xml" replace="none"> <setvalue ref="instance('changed')" ev:event="xforms-submit-done">no</setvalue> </submission>
What we are going to do is catch the xforms-submit-error
event
as well, record it similarly, and then use that to alter the display in some
way.
To do this we are going to replace the changed
instance, to
state
:
<instance id="state"> <state xmlns="">ok</state> </instance>
It will hold one of three states:
ok
: the data is unchanged, either in its initial state, or
identical to its saved state;changed
: the data has been changed, and is not yet
saved;error
: the data has been changed, and saving it failed.The new state will be created when submitting:
<submission method="put" resource="http://lists.example.com/saves/list.xml" replace="none"> <setvalue ref="instance('state')" ev:event="xforms-submit-done">ok</setvalue> <setvalue ref="instance('state')" ev:event="xforms-submit-error">error</setvalue> </submission>
We have to change the bind for relevance:
<bind ref="instance('state')" relevant=". != 'ok'"/>
and the button for saving the data:
<submit submission="save" class="{instance('state')}" label="save"/>
This looks like this: