Steven Pemberton, CWI, Amsterdam
Please go to
cwi.nl/~steven/xforms/techniques/
to set up the server for the exercises
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.
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.
At the bottom of each page, and also collected at the back
All take ref=".." or bind=".."
all have children <label>,
<hint>, <help> and
<alert>.
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>
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.
Just to get started, add a select1
to the
example to allow you to select units.
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.
Alter the data from that example
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:
Change the error message colours to black on yellow.
<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>
<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>
<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>
Change the second example to use a boolean value
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.
Make the trigger activated once you have clicked on "Agree".
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"> ...
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>
Don't insert a duplicate value
<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 < instance('new')/value/@date]" />
Or before dates that are greater:
<insert origin="instance('new')/value" position="before" ref="value[@date > instance('new')/value/@date]" />
Take the last example, and make it so that the most recent comes first.
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"/>
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.
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"/>
and then:
<select ref="shopping" appearance="full"> <itemset ref="instance('items')/produce"> <label ref="."/> <value ref="."/> </itemset> </select>
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.
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>
Change to:
<repeat ref="shopping/produce"> <output ref="."/> </repeat>
Add items with more than one word to the produce.
<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>
<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>
Alter the second select
so that it refers to
shopping
, instead of bought
(you have to change the
ref
on the itemset
as well).
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>
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.
The last example combines this exercise with an earlier one.
Add hints to the triggers.
We want to be able to add items to the list, like this:
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>
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>
<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>
<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>
Use relevance instead of the switch
.
Easy
<trigger> <label>+</label> <hint>Add an item</hint> <toggle case="open" ev:event="DOMActivate"/> </trigger>
<trigger> <label>×</label> <hint>Cancel</hint> <action ev:event="DOMActivate"> <setvalue ref="instance('new')/produce"/> <toggle case="closed"/> </action> </trigger>
<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> ...
<insert ref="instance('items')/produce" origin="instance('new')/produce" if="instance('new')/produce != ''"/>
Change the example so that when you add a new element, it is already selected. (Hint, it's a single element)
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"/>
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
!
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.
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"/>
Make the check for duplicates case independent. (Hint: needs two changes)
<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:
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>
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.
We're going to create an input where you have control over the 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> ...
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.
incremental="true"
on the input
, and the filter
on the select1
.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="..."
instance('data')/country!=''
count(instance('countries')/country[ starts-with(lower-case(.), lower-case(instance('data')/country))]) != 0
count(instance('countries')/country[ .=instance('data')/country ])!=1
Give feedback if a value typed in isn't in the list.
<bind ref="country" constraint="instance('suggestions')/exact = true()"/>
Result
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
)
Basic technique
<repeat ref="*"> <output value="local-name(.)"/>: <output ref="."/> </repeat>
<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
<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
The root:
<output value="local-name(/*)"/>
Attributes:
<repeat ref="@*"> <output class="child" value="concat('@', local-name(.), ': ', .)"/> </repeat>
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
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.
header
elements, each representing a header in the response from the server, with
two child elements, name
and value
, containing
the name and value of the header, for instance:
<header: <name>content-length</name> <value>33</value> </header>
Additionally for xforms-submit-error:
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"/>
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>
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', '')"/>
The tutorial is written in XForms (of course)