A List Manager with XForms

Steven Pemberton, CWI Amsterdam

Version: 2020-02-11.

Introduction

Lists are everywhere:

We're going to develop a list manager app in XForms.

Data

We'll start very simply, the data will just be a list of items:

<list name="Shopping">
   <item>Bananas</item>
   <item>Apples</item>
   <item>Milk</item>
   <item>Yoghurt</item>
</list>

Instances

We can either put the data directly in the application:

<instance>
   <list name="Shopping" xmlns="">
      <item>Bananas</item>
      <item>Apples</item>
      <item>Milk</item>
      <item>Yoghurt</item>
   </list></instance>

or in a file, which we include:

<instance resource="list.xml"/>

Display

We can display this easily:

<group>
   <label><output ref="@name"/></label>
   <repeat ref="item">
      <output ref="."/>
   </repeat>
</group>

Which, with suitable CSS, looks like this:

Source

Changing entries

We want to be able to edit the list, so we'll change the outputs into inputs:

<group>
   <label><output ref="@name"/></label>
   <repeat ref="item">
      <input ref="."/>
   </repeat>
</group>

which gives:

Source

Adding new entries

We also want to be able to add new items.

There are several ways to do this: have a button after each entry that adds a new item after the current one; have a single button that adds a new item at the end of the list...

but the simplest is to hit [return] when on an item to add a new item underneath.

When you hit return in an input, it receives the event DOMActivate. We just need to respond to that. Here's a first version:

<group>
   <label><output ref="@name"/></label>
   <repeat ref="item">
      <input ref=".">
         <action ev:event="DOMActivate">
            <insert ref="."/>
         </action>
      </input>
   </repeat>
</group>

The response to the event is to insert a new element.

Insert

An insert takes a list of values, and with no other attributes, duplicates the last element of the list, appending it after the list. In this case the list we have selected consists of just the single item we are looking at, so duplicates it and appends it after it. Like this (try it):

Source

Blank entries

However, we don't want to duplicate the current element, but insert a blank element.

So we create an instance containing a blank element:

<instance id="blank">
   <list xmlns="">
      <item/>
   </list>
</instance>

and alter the insert to this:

<insert ref="." origin="instance('blank')/item"/>

which still inserts after the current element, but inserts the item from origin instead.

Giving this (try it):

Source

Styling

By the way, the box around the items is only the default styling for an input element. We can change that if we want with a bit of CSS, and while we're doing it, we'll change the output for the name of the list into an input as well:

Source

Positioning

When you add a new item, you want of course to be positioned on it in order to type it in. We do that with a setfocus action, which moves the focus in this case to the newly-created element:

<group>
   <label><output ref="@name"/></label>
   <repeat ref="item">
      <input ref="." id="I">
         <action ev:event="DOMActivate">
            <insert ref="." origin="instance('blank')"/>
            <setfocus control="I"/>
         </action>
      </input>
   </repeat>
</group>

(as of this writing, there is a bug in the implementation being used for these examples so that this only works if you hit return more than once).

Source

Deleting entries

Next thing we need to be able to do us delete items. Again, there are several ways to do it. What we'll do here is add a trigger before every item, that if activated deletes the current element:

<repeat ref="item">
   <trigger label="X">
      <action ev:event="DOMActivate">
         <delete ref="."/>
      </action>
      <hint>Delete</hint>
   </trigger>
   <input ref="." id="I">
      <action ev:event="DOMActivate">
         <insert ref="." origin="instance('blank')"/>
         <setfocus control="I"/>
      </action>
   </input>
</repeat>

Source

Styling

We can add appearance="minimal" to the trigger so that it doesn't look like a button, but still acts the same way:

<trigger appearance="minimal" label="X">
   <action ev:event="DOMActivate">
      <delete ref="."/>
   </action>
   <hint>Delete</hint>
</trigger>

Source

Last delete

One last thing we should do is prevent the very last item being deleted, because if that happens, you can't add any more. A simple approach is to only let the delete work if there is more than one item:

<delete ref="." if="count(../item) > 1"/>

Source

Blank instead of delete the last

What we can do though, is instead of deleting the last entry, blank it out:

<setvalue ref="." if="count(../item)=1"/>

Source

Saving the result

We define a submission for the data, which says where to put it and how:

<submission resource="http://lists.example.com/saves/list.xml" replace="none"/>

The replace="none" has the effect of ignoring anything returned from the server.

Now we add a control to the application to save the data:

<submit label="save"/>

Source

Keeping track of changes

It would be good to let the user know if the data needs saving or not.

We'll keep track of that by recording whether any data has been changed. First a value to record it:

<instance id="changed">
   <changed xmlns="">no</changed>
<instance>

There are three ways the data can change:

These three cases generate different events.

Insert and delete events

In the insertion and deletion cases the events are dispatched to the instance containing the relevant item. So we'll add an id to that instance:

<instance id="list" resource="list.xml"/>

and then listen for events being sent to it. If either is received, we set changed to yes:

<action ev:event="xforms-insert" ev:listener="list">
   <setvalue ref="instance('changed')">yes</setvalue>
</action>
<action ev:event="xforms-delete" ev:listener="list">
   <setvalue ref="instance('changed')">yes</setvalue>
</action>

Shorter listeners

Actually, an action element with only one action under it this can be shortened if you want, by putting the ev: attributes on the contained action:

<setvalue ev:event="xforms-insert" ev:listener="list"
          ref="instance('changed')">yes</setvalue>
<setvalue ev:event="xforms-delete" ev:listener="list"
          ref="instance('changed')">yes</setvalue>

Changed event

For the case of an item being edited, the event xforms-value-changed is sent to any control using the value. In our case, that is the input that already has an id:

<setvalue ev:event="xforms-value-changed" ev:listener="I"
          ref="instance('changed')">yes</setvalue>

but you can also put the listener within the input element, just as we did for DOMActivate, to give the same effect:

<input ref="." id="I">
   <setvalue ref="instance('changed')" ev:event="xforms-value-changed">yes</setvalue>
   <action ev:event="DOMActivate">
      <insert ref="." origin="instance('blank')"/>
      <setfocus control="I"/>
   </action>
</input>

Responding to changes

So now we have captured the fact that the data has been changed. Now to use that information.

First we say that the information is only relevant if the value is yes:

<bind ref="instance('changed')" relevant=". = 'yes'"/>

If we now make the submit button refer to it, the button will only appear when the data is changed:

<submit ref="instance('changed')" label="save"/>

Try it, make a change:

Source

Incremental

(Actually I did one other thing, I added an attribute to the input elements:

<input ref="." id="I" incremental="true">

This makes changes to the value as you type them, rather than waiting until you move away from the input)

After saving

One other thing we need to do is mark the data as unchanged once the data has been saved.

After the submission of the data has been successful, the event xforms-submit-done is sent to the submission element. So we listen for that, and reset the changed value accordingly:

<submission resource="http://lists.example.com/saves/list.xml" replace="none">
  <setvalue ref="instance('changed')" ev:event="xforms-submit-done">no</setvalue>
</submission>

Saving automatically

Now that we know when the data needs to be saved, we can even arrange for it to be automatically saved at regular intervals.

At every point where we want to set changed to yes, if its value is no then we set a timer before setting changed to yes.

When the timer goes off, if changed is still yes (that is, if the user hasn't saved the data in the meantime), we save the data ourselves.

Creating a change event

Rather than repeat this code everywhere we need it, we will gather it in one place, and dispatch an event to make it happen.

So in the three places where we have something like this:

<setvalue ref="instance('changed')" ev:event="some event">yes</setvalue>

we will now use

<dispatch name="CHANGED" ev:event="some event" targetid="M"/>

and put the id of M on the model element:

<model id="M" xmlns="http://www.w3.org/2002/xforms">

Responding to the event

Now we have to catch and respond to the new event, by setting the timer (delay="5000" means 5 seconds), and then setting changed to yes:

<action ev:event="CHANGED">
   <dispatch name="TIMER" delay="5000" if="instance('changed')='no'"/>
   <setvalue ref="instance('changed')">yes</setvalue>
</action>

Responding to the timer

We then catch and respond to the timer when it goes off, by saving the data if necessary:

<action ev:event="TIMER">
   <send if="instance('changed')='yes'"/>
</action>

(send just initiates the default submission).

See it in action here - try changing a value, and waiting the 5 seconds for it to be saved - you'll see the save button appear on the change, and disappear after the save:

Source

Restoring the data

Having saved the data, then it is really a good idea to restore it when the application restarts.

Ideally we would initialise the instance data with:

<instance resource="http://lists.example.com/saves/list.xml"/>

which would be fine, except for the first time we run the application, when we won't have saved anything yet.

Fallback

So what we do is initialise the data for the first time the application gets run:

<instance id="list">
   <list name="list" xmlns="">
      <item>your data here</item>
   </list>
</instance>

(we could put the default in a file as well if preferred and load it from there).

Then on start up, we try to load the saved data if it exists.

Catching submit-error

This submission element says where to get the data, and use it to replace the instance called list.

If the data doesn't exist, an xforms-submit-error will be dispatched, which we can catch, and ignore (and so the instance stays with the initial data):

<submission id="init" resource="http://lists.example.com/saves/list.xml"             replace="instance" instance="list">
   <action ev:event="xforms-submit-error" 
           ev:propagate="stop" ev:defaultAction="cancel"/>
</submission>

On start-up we cause the submission to be processed:

<action ev:event="xforms-ready">
   <send submission="init"/>
</action>

Future version

The next version of XForms makes this initialisation process easier by having a fallback in case of failure:

<instance src="saveddata.xml">
   <list name="list" xmlns="">
      <item>your data here</item>
   </list>
</instance>

Conclusion

So we have a fairly comprehensive list application, that allows you to edit, add, and delete items, which get saved automatically at intervals, and automatically reloaded on start-up.

In a later example, we will add to this.