XForms Techniques Hands-On

Steven Pemberton, CWI, Amsterdam

Please go to

         cwi.nl/~steven/xforms/techniques/

to set up the server for the exercises

Contents

Abstract

XForms is an XML-based, Turing-complete declarative programming language for applications both on and off the web.

Experience with both small and large applications has shown that programming in XForms can reduce the time and resources needed for programming to a tenth or less of what is needed using traditional procedural programming.

This tutorial introduces techniques and idioms used in XForms for putting its facilities to best use. Each section of the presentation is followed by an exercise where attendees apply the technique themselves by editing an existing example.

This is an updated and revised version of an earlier tutorial, based on experience teaching it.

While it would be beneficial to have followed the earlier "Introduction to XForms" tutorial, available online at http://cwi.nl/~steven/xforms/tutorial/, it should still be possible to follow this tutorial without it.

The tutorial is a hands-on, bring your own device tutorial; attendees will be required to install a small piece of software (a server that accepts the PUT method). The materials will be available online for self-study after the conference.

Software

Go to cwi.nl/~steven/xforms/techniques/

Set up the server for the exercises

Introduction

I'm assuming you know the what and why of XForms

News: Ukraine

What you will learn

This is a new version of an earlier tutorial: I am always happy to get feedback.

There may well be more material than we can cover in the time, but the tutorial is online, so you can continue studying at home.

Answers to Exercises

At the bottom of each page, and also collected at the back

Recap: model

Recap: simple controls

All take ref=".." or bind=".."
all have children <label>, <hint>, <help> and <alert>.

Recap: Combining controls

Recap:events, actions, functions

Attribute Value Templates

Many attributes in XForms are expressions, that let you access instance values.

<output ref="amount"/>

If you need to insert values into other elements, that is easy:

<label><output ref="heading"/></label>

But some attributes are only strings. Then you need Attribute value templates (AVTs):

<output class="{if(amount < 0, 'negative', 'positive'}" ref="amount">
   <label>Result</label>
</output>

Modifying CSS presentation through XForms values

If a simple class name isn't enough, then use a style attribute:

<bind ref="style"
      calculate="concat('margin-left: ', ../left, 'ex;', 
                        'margin-top: ',  ../top, 'ex;')" />

and then:

<group style="{style}">

or whatever.

Exercise

Just to get started, add a select1 to the example to allow you to select units.

Relevance

You can assign properties to values: type, constraint, relevance, readonly, ...

<bind ref="some value" relevant="some condition"/>

If a value is not (currently) relevant, then any controls attached to it are not visible.

Exercise

Alter the data from that example

Error messages

Another use of relevance is for displaying error states:

<instance id="error">
   <error xmlns="">
      <message/>
   </error>
</instance>
<bind ref="instance('error')/message" relevant=". != ''"/>
 ...
<output class="error" ref="instance('error')/message"/>

So whenever it contains a value, it becomes relevant and the message is displayed, and whenever it is empty, nothing is displayed. Here it is in use:

Exercise

Change the error message colours to black on yellow.

Collapsible sections

version 1: switch

<switch>
   <case id="closed">
      <trigger appearance="minimal">
         <label>▶Address</label>
         <toggle case="open" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="open">
      <trigger appearance="minimal">
         <label>▼Address</label>
         <toggle case="closed" ev:event="DOMActivate"/>
      </trigger>
      ... collapsible stuff ...
   </case>
</switch>

Version 2: relevance

<instance id="admin">
   <admin xmlns="">
      <show/>
   </admin>
</instance>
<bind ref="instance('admin')/show" 
      relevant=". = true()"/>

Using relevance: if show is relevant, the content will be displayed:

<group ref="instance('admin')/show">
   ... collapsible stuff ...
</group>

Setting the value

<trigger appearance="minimal">
   <label>
      <output value="if(instance('admin')/show=true(),
            '▼Address', 
            '▶Address')"/>
   </label>
   <setvalue ev:event="DOMActivate"
             ref="instance('admin')/show"
             value="not(boolean-from-string(.))" />
</trigger>

Exercise

Change the second example to use a boolean value

Triggers: Disabling

Just like other controls, triggers can be bound to a node:

<trigger ref="something">...

The trigger doesn't use the value of the node, but it does use its other properties, in particular relevant and readonly (in the future more will be usable).

If the value is non-relevant, then the trigger, just like other controls, is not shown. If it is readonly then the trigger is greyed out, and non-clickable.

Exercise

Make the trigger activated once you have clicked on "Agree".

Triggers: Activating

You can make a [return] in an input field activate a trigger by transferring the activation.

<input ref="instance('new')/value">
   <dispatch name="DOMActivate" targetid="ok" ev:event="DOMActivate"/>
</input>
 ...
<trigger id="ok">
 ...

Conditional actions

Any action can be made conditional by adding an if="..." attribute to it, with the condition. For instance: don't insert an empty value:

<trigger id="ok">
   <label>OK</label>
   <action ev:event="DOMActivate">
      <insert ref="value"
              origin="instance('new')/value"
              if="instance('new')/value != ''"/>
      <setvalue ref="instance('new')/value"/>
   </action>
</trigger>

Exercise

Don't insert a duplicate value

Keeping lists sorted

Technique

<values>
   <value date="2025-05-17">100</value>
   <value date="2025-05-19">98</value>
   <value date="2025-05-21">102</value>
   <value date="2025-05-23">101</value>
</values>

Insert after dates that are less:

<insert origin="instance('new')/value"
        position="after"
        ref="value[@date &lt; instance('new')/value/@date]"
/>

Or before dates that are greater:

<insert origin="instance('new')/value"
        position="before"
        ref="value[@date > instance('new')/value/@date]"
/>

Exercise

Take the last example, and make it so that the most recent comes first.

Selection techniques

Selection techniques

Hardwired values:

<select ref="shopping">
   <item>
      <label>Bread</label>
      <value>Bread</value>
   </item>
   <item>
      <label>Butter</label>
      <value>Butter</value>
   </item>
   ...etc...
</select>

Output resulting string:

<output ref="shopping"/>

Initialisation

Initialised values also initialises the control

<instance>
   <data xmlns="">
      <shopping>Butter Bananas</shopping>
   </data>
</instance>

This is because there is a two-way relationship between the control and the value being filled. Each affects the other.

Not hard-wired

The values can come from an instance, internal

<instance id="items">
   <items xmlns="">
      <produce>Bread</produce>
      <produce>Butter</produce>
      <produce>Milk</produce>
      <produce>Cheese</produce>
      <produce>Bananas</produce>
   </items>
</instance>

or external:

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

Control

and then:

<select ref="shopping" appearance="full">
   <itemset ref="instance('items')/produce">
      <label ref="."/>
      <value ref="."/>
   </itemset>
</select>

Exercise

Take the last example, and add an <input> referencing the shopping value, and see how it changes as you select values, and how the select control changes as you add values.

Structure

Instead of

<shopping>Bread Bananas</shopping>

is is better to have:

<shopping>
   <produce>Bread</produce>
   <produce>Bananas</produce>
</shopping>

This needs one change:

<select ref="shopping" appearance="full">
   <itemset ref="instance('items')/produce">
      <label ref="."/>
      <copy ref="."/>
   </itemset>
</select>

Output

Change to:

<repeat ref="shopping/produce">
   <output ref="."/>
</repeat>

Exercise

Add items with more than one word to the produce.

Going shopping

<repeat ref="shopping/produce">
   <output ref="."/>
</repeat>

Instead of output, we look at the structured result as another select:

<select ref="bought" appearance="full">
    <itemset ref="../shopping/produce">
       <label ref="."/>
       <copy ref="."/>
    </itemset>
 </select>

What we have

<select ref="shopping" appearance="full">
   <itemset ref="instance('items')/produce">
      <label ref="."/>
      <copy ref="."/>
   </itemset>
</select>
<select ref="bought" appearance="full">
   <label>To buy:</label>
   <itemset ref="../shopping/produce">
      <label ref="."/>
      <copy ref="."/>
   </itemset>
</select>

Exercise

Alter the second select so that it refers to shopping, instead of bought (you have to change the ref on the itemset as well).

Sneaky trick

Your result should look like this.

<select ref="shopping" appearance="full">
   <itemset ref="instance('items')/produce">
      <label ref="."/>
      <copy ref="."/>
   </itemset>
</select>
<select ref="shopping" appearance="full">
   <label>To buy:</label>
   <itemset ref="produce">
      <label ref="."/>
      <copy ref="."/>
   </itemset>
</select>

Two-way

What this illustrates is an essential property of XForms.

You may think of a select control being used to populate a list, but as already pointed out, it works both ways: the control populates the list, but the list also populates the control.

Getting your head around this two-way nature of XForms is central to getting the most from the language.

Traditional view

Traditional view of selections

Actual two-way situation

Two way view

Full picture

Full view

Final version

Our final version

Exercise

The last example combines this exercise with an earlier one.

Add hints to the triggers.

Open selections

We want to be able to add items to the list, like this:

Open Selections

We're going to build an example where the new values get added to the data, and saved as well. Starting from this:

<instance id="items" src="items.xml"/>
 ...
<select ref="shopping" appearance="full">
   <itemset ref="instance('items')/produce">
      <label ref="."/>
      <copy ref="."/>
   </itemset>
</select>

Use collapsing technique

Technique from earlier:

<switch>
   <case id="closed">
      <trigger>
         <label>+</label>
         <toggle case="open" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="open">
      <trigger>
         <label>×</label>
         <toggle case="closed" ev:event="DOMActivate"/>
      </trigger>
      ... The input control goes here ...
   </case>
</switch>

Add a new value

<input ref="instance('new')/produce"/>

and use an OK button to add it to the data

<trigger>
   <label>OK</label>
   <action ev:event="DOMActivate">
      <insert ref="instance('items')/produce"
              origin="instance('new')/produce"/>
      <setvalue ref="instance('new')/produce"/>
      <toggle case="closed"/>
   </action>
</trigger>

Result

<switch>
   <case id="closed">
      <trigger>
         <label>+</label>
         <toggle case="open" ev:event="DOMActivate"/>
      </trigger>
   </case>
   <case id="open">
      <trigger>
         <label>×</label>
         <toggle case="closed" ev:event="DOMActivate"/>
      </trigger>
      <input ref="instance('new')/produce"/>
      <trigger>
         <label>OK</label>
         <action ev:event="DOMActivate">
            <insert ref="instance('items')/produce"
                    origin="instance('new')/produce"/>
            <setvalue ref="instance('new'')/produce"/>
            <toggle case="closed"/>
         </action>
      </trigger>
   </case>
</switch>

Exercise

Use relevance instead of the switch.

Improvements

  1. Hints should be added to the triggers where necessary to explain what they do.
  2. When cancelled, the new value should be set back to the empty string.
  3. If return is hit when typing in a new item, it should work the same as clicking on OK.
  4. An empty string shouldn't be inserted into the list.
  5. An existing value shouldn't be duplicated.

Hints

Easy

<trigger>
   <label>+</label>
   <hint>Add an item</hint>
   <toggle case="open" ev:event="DOMActivate"/>
</trigger>

Cancel

<trigger>
   <label>×</label>
   <hint>Cancel</hint>
   <action ev:event="DOMActivate">
      <setvalue ref="instance('new')/produce"/>
      <toggle case="closed"/>
   </action>
</trigger>

Return

<input ref="instance('new')/produce">
   <dispatch name="DOMActivate" targetid="ok"
             ev:event="DOMActivate"/>
</input>

Add an id to the OK button

<trigger id="ok">
   <label>OK</label>
   ...

Prevent empty string

<insert ref="instance('items')/produce"
        origin="instance('new')/produce"
        if="instance('new')/produce != ''"/>

Exercise

Change the example so that when you add a new element, it is already selected. (Hint, it's a single element)

Answer

Change the example so that when you add a new element, it is already selected.

As well as adding it to the list of produce items, you add it to the list of shopping items:

 <insert context="instance('data')/shopping"
         ref="produce"
         origin="instance('new')/produce"/>

Persistence

An event is sent whenever something is inserted in a structure. So we listen for it:

<instance id="items" src="items.xml"/>
<action ev:event="xforms-insert" ev:observer="items">
   ... do something here ...
</action>

That 'something' is save the data:

<send submission="save"/>

Referencing a suitable submission:

<submission id="save" ref="instance('items')"
            resource="items.xml" method="put"/>

You need a server that accepts put!

Catching errors

Similarly, there is an event if a submission fails:

<submission id="save" ref="instance('items')" resource="items.xml" method="put">
   <setvalue ev:event="xforms-submit-error"
       ref="message"
       value="concat('Data not saved: ',
              event('response-reason-phrase'))"/>
   <setvalue ev:event="xforms-submit-done" 
       ref="message"/>
</submission>

If it succeeds there is also an event. In this case we clear any message.

Events Bubble

You can catch errors from a particular submission, or several. These catch all submissions at this level:

<submission id="save" ref="instance('items')"
            resource="items.xml" method="put"/>
<setvalue ev:event="xforms-submit-error"
       ref="message"
       value="concat('Data not saved: ',
           event('response-reason-phrase'))"/>
<setvalue ev:event="xforms-submit-done" 
       ref="message"/>

Exercise

Make the check for duplicates case independent. (Hint: needs two changes)

Restoring data

<instance resource="items.xml"/>

But this only works if the file already exists.

What we do is initialise the instance with default values,

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

and then attempt to read a file:

Read

If the read fails, we cancel the error event.

<submission id="init"
resource="items.xml" method="get" replace="instance" instance="list"> <action ev:event="xforms-submit-error" ev:propagate="stop" ev:defaultAction="cancel"/> </submission>

On initialisation, set it in motion:

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

Exercise

At start-up after a successful restore of the saved data, save the instance to another file, and add a button to reset the data back to its initial state by reloading that new file.

Suggestions

We're going to create an input where you have control over the suggestions:

Suggestions

Traditional method of entering your country:

<select1 ref="instance('data')/country"
         appearance="minimal">
   <label>Country</label>
   <itemset ref="instance('countries')/country">
      <label ref="."/>
      <value ref="."/>
   </itemset>
</select1/>
<output ref="instance('data')/country">
   <label>Your choice</label>
</output>

But it's a long list:

<countries>
    <country>Afghanistan</country>
    <country>Albania</country>
    <country>Algeria</country>
    <country>Andorra</country>
    <country>Anguilla</country>
    ...

Method

Our method is plain (incremental) input:

<input ref="country" incremental="true">
    <label>Country</label>
</input>

and below it a select1 for suggestions that match that input:

<select1 ref="country" appearance="full">
   <label>Suggestions</label>
   <itemset ref="instance('countries')/country[
                starts-with(., instance('data')/country)]">
      <label ref="."/>
      <value ref="."/>
   </itemset>
</select1>

Note that they both reference the same value (country). This is fine.

Remarks

  1. Initially you get the whole list.
  2. As you type the list diminishes. This is thanks to the incremental="true" on the input, and the filter on the select1.
  3. If you select one of the suggestions, all the others disappear, and its value is filled in for you.
  4. But also, because of the two-way nature of XForms, if you just type the name of the country, only one suggestion will remain, selected.
  5. If you mistype a name, no suggestions are left.
  6. As mentioned, it is case-sensitive.

Improvements

Make typing easier, by making it case-insensitive.

country[starts-with(lower-case(.), lower-case(instance('data')/country))]

Use relevance to only display suggestions when there is something to suggest:

<bind ref="instance('suggestions')/suggest"
      relevant="..."

Suggestion relevance

  1. When something has been typed in.
    instance('data')/country!=''
  2. Some values match the input.
    count(instance('countries')/country[
          starts-with(lower-case(.),
          lower-case(instance('data')/country))])
    != 0
  3. When the value doesn't exactly match a country
    count(instance('countries')/country[
                   .=instance('data')/country
    ])!=1

Only accept valid countries

Give feedback if a value typed in isn't in the list.

<bind ref="country"
    constraint="instance('suggestions')/exact = true()"/>

Result

Exercise

Take the last example, and instead of matching suggestions on the start of what is typed in, instead match on whether a country contains what has been typed in. (Hint: use the function contains)

Dealing with unknown data structures

Basic technique

<repeat ref="*">
   <output value="local-name(.)"/>: <output ref="."/>
</repeat>

Example

<data xmlns="">
   <error-type/> 
   <resource-uri>test.xml</resource-uri>
   <response-status-code>200</response-status-code>
   <header>
      <name>content-length</name>
      <value>177</value>
   </header>
   <header>
      <name>content-type</name>
      <value>text/xml</value>
   </header>
</data>

Gives

error-type: 
resource-uri: test.xml
response-status-code: 200
header: content-length177
header: content-typetext/xml

Improvement

<repeat ref="*">
   <output value="local-name(.)"/>: 
   <output ref=".[count(*) = 0]"/>
   <repeat ref="*">
      <output class="child" value="local-name(.)"/>: 
      <output ref="."/>
   </repeat>
</repeat>

gives

error-type: 
resource-uri: test.xml
response-status-code: 200
header:
     name: content-length
     value: 177
header:
     name: content-type
     value: text/xml

Root and attributes

The root:

<output value="local-name(/*)"/>

Attributes:

<repeat ref="@*">
   <output class="child" 
           value="concat('@', local-name(.), ': ', .)"/>
</repeat>

Method 2

You can nest and nest similar repeats as above, but to make it general, you need another technique

<repeat ref="descendant::*">

See the tutorial for how to get the right indentation

Last modified

Last modified

If a file has been modified since you last saved, you might want to check before overwriting.

Servers return a number of useful values when you do a submission; these are returned with the xforms-submit-done and xforms-submit-error events.

return values

return values

Additionally for xforms-submit-error:

response headers

You access the returned values with the event function:

<setvalue ref="resource-uri"
          value="event('resource-uri')"/>

but the one we are interested in is:

<setvalue ref="last-modified"
   value="event('response-headers')//[
                  name='last-modified']/value"/>

technique

Every time the file is saved, we do a head on the file, to access and save the last-modified time:

<submission id="save" resource="data.xml" method="put">
    <action ev:event="xforms-submit-done">
       <send submission="head"/>
    </action>
</submission>

and

<submission id="head" resource="data.xml" method="head">
   <action ev:event="xforms-submit-done">
      <setvalue ref="instance('data')/@lm"
        value="event('response-headers')//[
                    name='last-modified']/value"/>
   </action>
</submission>

checking

Whenever we want to overwrite the file, we can do a check, using exactly the same technique:

<submission id="check" resource="data.xml" method="head">
   <action ev:event="xforms-submit-done">
     <setvalue ref="@check" 
          value="event('response-headers')//[
                   name='last-modified']/value"/>
   </action>
</submission>

and have a value that records if there is a problem:

<bind ref="@mismatch" relevant=". != ''" 
      calculate="if(../@lm != ../@check, 'mismatch', '')"/>

About this tutorial

The tutorial is written in XForms (of course)

What you have learned

What Next?