Advanced XForms Hands-On: Techniques and Examples

Steven Pemberton, CWI, Amsterdam

Contents

Abstract

Software

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

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 tutorial, so I am happy to get feedback.

There is way more material than we can cover in the time! But the tutorial is online, so you can continue studying at home.

Collapsible sections

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

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

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

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

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

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>

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>

Structure

Instead of

<shopping>Bread Bananas</shopping>

good to have:

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

One change:

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

Output

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

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>

Sneaky trick

<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 in fact 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.

The rest of this is turning the above into an application using techniques from the earlier section.

Traditional view

Traditional view of selections

Actual two-way situation

Two way view

Full picture

Full view

Final version

Our final version

Exercise

Add hints to the triggers.

Open selections

Open Selections

There are 'open' selects, that permits values outside the given range:

<select ref="..." selection="open">
   ...

But we're going to build an example where the new values get added to the data, and saved as well.

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

(Unstructured version of select because it is slightly harder.)

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('data')/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('data')/produce"/>
      <setvalue ref="instance('data')/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('data')/produce"/>
      <trigger>
         <label>OK</label>
         <action ev:event="DOMActivate">
            <insert ref="instance('items')/produce"
                    origin="instance('data')/produce"/>
            <setvalue ref="instance('data')/produce"/>
            <toggle case="closed"/>
         </action>
      </trigger>
   </case>
</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. A value containing spaces shouldn't be inserted into the list (because we're using the unstructured select).
  6. 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('data')/produce"/>
      <toggle case="closed"/>
   </action>
</trigger>

Return

<input ref="instance('data')/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('data')/produce"
        if="instance('data')/produce != ''"/>

Warn the user beforehand though:

<bind ref="produce"
      constraint="not(contains(., ' '))"/>

And add an alert:

<input ref="instance('data')/produce" incremental="true">
   <alert>May not contain spaces</alert>
   <dispatch name="DOMActivate" targetid="ok"
             ev:event="DOMActivate"/>
</input>

Etc

See the example for preventing adding values with spaces, and preventing duplicates.

Exercise

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

Persistence

<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

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

Better method

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

Relevance technique

<bind ref="message" relevant=". != ''"/>

so that it is only relevant when nonempty, and then display:

<output class="error" ref="message"/>

Only displayed when there is an error

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

<submission id="init" resource="items.xml"
   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>

Future

In the future this will be easier:

<instance src="items.xml" id="list">
   <list xmlns="">
      <item>your data here</item>
   </list>
</instance>

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.

Keeping lists sorted

Keeping lists sorted

<values>
   <value date="2018-08-17">100</value>
   <value date="2018-08-19">98</value>
   <value date="2018-08-21">102</value>
   <value date="2018-08-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]"
/>

Suggestions

Suggestions

Traditional method of entering your country:

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

But it's a long list:

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

Method

Plain input:

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

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

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

Method

The select1 is only relevant if the country has a value (if someone has started typing):

<bind ref="instance('admin')/suggestion"
      relevant="instance('data')/country!='.'"/>

Remarks

  1. Suggestions pop up when you start typing. This is thanks to the relevance bind.
  2. As you type, the suggestions adapt. This is thanks to the incremental="true" on the input, and the filter on the select1.
  3. It is a case-sensitive match. We can fix that:
    country[
       starts-with(lower-case(.),
                   lower-case(instance('data')/country))
    ]
  4. when you select a suggestion, nothing happens.

Choosing a suggestion

<select1 ref="instance('admin')/suggestion" 
         appearance="full">
   <label>Suggestions</label>
   <itemset ref="instance('countries')/country[
                           starts-with(lower-case(.),
                lower-case(instance('data')/country))]">
      <label ref="."/>
      <value ref="."/>
   </itemset>
   <action ev:event="xforms-value-changed">
      <setvalue ref="instance('data')/country"
                        value="context()"/>
   </action>
</select1>

Having made a selection

The suggestions should disappear:

<bind ref="instance('admin')/suggestion"
      relevant="instance('data')/country!='' and
                    instance('data')/country!=."/>

Only accept valid countries

<bind ref="country" 
      constraint="count(instance('countries')/country
               [.=instance('data')/country])=1"/>

(There's one other improvement in the tutorial)

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>

So with

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

You get

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

Exercise

There isn't one

Modifying CSS presentation through XForms values

The trick is to use AVTs

<output class="{...}" ref="..."/>

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

<bind nodeset="style"
      calculate="concat('margin-left: ', ../offx, 'px; 
                         margin-top: ',  ../offy, 'px;')" />

and then:

<group style="{style}">

or whatever.

Tabbed interfaces

Tabbed interfaces

The traditional approach (see introductory tutorial) is to use switch, and styling to create a tabbed interface.

Instead here we are going to use relevance-style techniques.

<group ref="instance('tab')/home">
   <label>Home</label>
   Welcome to our home page...
</group>
<group ref="instance('tab')/products">
   <label>Products</label>
   We produce many fine products ...
</group>
 ...

where only one of the cases is relevant at any time.

relevance

<instance id="tab">
   <tab xmlns="">
      <selected>home</selected>
      <home/>
      <products/>
      <support/>
      <contact/>
   </tab>
</instance>

Relevance:

<bind ref="instance('tab')">
   <bind ref="home" relevant="../selected='home'"/>
   <bind ref="products" relevant="../selected='products'"/>
   <bind ref="support" relevant="../selected='support'"/>
   <bind ref="contact" relevant="../selected='contact'"/>
</bind>

(Note nested binds)

Selecting a tab

Four triggers:

<trigger label="Home">
   <setvalue ev:event="DOMActivate"
         ref="instance('tab')/selected">home</setvalue>
</trigger>

Alternative

<select1 ref="instance('tab')/selected" appearance="full">
   <item>
      <label>Home</label>
      <value>home</value>
   </item>
   <item>
      <label>Products</label>
      <value>products</value>
   </item>
   <item>
      <label>Support</label>
      <value>support</value>
   </item>
   <item>
      <label>Contact</label>
      <value>contact</value>
   </item>
</select1>

Styling

The rest is styling using CSS

Exercise

Generalise:

<instance id="tab">
   <tab selected="Home" xmlns="">
      <home name="Home"/>
      <products name="Products"/>
      <support name="Support"/>
      <contact name="Contact/>
   </tab>
</instance>

Bind:

<bind ref="instance('tab')/*"
      relevant="../@selected=context()/@name"/>

and change the select1 so it gets its labels and values from there.

Now add a new tab to the instance!

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

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', '')"/>

Mouse

Using the mouse

<mouse>
   <x/><y/><state/>
</mouse>

Access the mouse values:

<group>
   <label>Move mouse here</label> 
   <action ev:event="mousemove">
      <setvalue ref="mouse/x" value="event('clientX')"/>
      <setvalue ref="mouse/y" value="event('clientY')"/>
   </action>
   <action ev:event="mousedown">
      <setvalue ref="mouse/state">down</setvalue>
   </action>
   <action ev:event="mouseup">
      <setvalue ref="mouse/state">up</setvalue>
   </action>
   x: <output ref="mouse/x"/>
   y: <output ref="mouse/y"/>
   state: <output ref="mouse/state"/>
</group>

style the mouse

<bind ref="cursor" 
      calculate="if(../state='up', 'pointer', 'move')"/>

and style the outer group suitably:

<group style="cursor: {cursor}">

The use of SVG

Graphs

Study examples

A Dial

A Clock

A clock

A Calendar

A calendar

Multilingual interfaces

Multilingual

Maps

Maps

Game: slide

A game

Game: minesweeper

Automatically updating displays

Time-based presentations

A Slide Displayer

Presentation

Troubleshooting

About this tutorial

The tutorial is written in XForms (of course)

What you have learned

Answers to Exercises

At the back

What Next?