A List Manager with XForms – Part 2 (Draft)

Steven Pemberton, CWI Amsterdam

Version: 2022-01-28.

Introduction

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:

Source

Notice how an input for a boolean value takes the form of a check box.

Displaying selected only

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:

Source

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:

Source

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>

Source

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]

Source

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()"/>

Source

Next collapsible groups. Watch what happens when you click on the little black triangles:

Source

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 items. 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())"/>

Catching failed saves

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:

  1. ok: the data is unchanged, either in its initial state, or identical to its saved state;
  2. changed: the data has been changed, and is not yet saved;
  3. 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:

Source