A List Manager with XForms (Draft)

Steven Pemberton, CWI Amsterdam

Version: 2022-01-28.

Contents

Introduction

Lists are everywhere:

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

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>

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

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, 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.

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

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

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. While we're doing it, we'll change the output for the name of the list into an input as well:

Source

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

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

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

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

There's no point in creating a new list if we can't save it, so we'll do just that.

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

<submission method="put" resource="/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.

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>

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

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

)

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

but you can also put it 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>

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 attach a reference to it to the submit button, the button will only appear when the data is changed:

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

Try it, make a change:

Source

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

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 method="put" resource="/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 set changed to yes, if its value is no then we set a timer first. 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.

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">

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>

and 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="/saves/list.xml"/>

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

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), and then on start up, try to load the saved data: 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="/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>

Source

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 part 2, we will add to this.