This is a new tutorial, so I am happy to get feedback.
It assumes you know XForms to a certain level already.
There may be more material than we can cover in the time available, but the tutorial is online, so you can continue studying at home.
While we're going to build a generic application, we're going to do that by first making a specific application that we will steadily make more generic.
So to start, we will make a simple quick reference application for XForms.
The data we are using is a series of entries for aspects of XForms:
elements, attributes, actions, functions. This is stored in a file called
data.xml.
Here is a taste:
<xforms> ... <entry class="element"> <name>input</name> <common>Control Common</common> <common>inputmode</common> <common>incremental</common> <content><element>UICommon</element>*</content> </entry> <entry class="element"> <name>output</name> <common>Control Common</common> <common>value</common> <common>mediatype</common> <content>PCDATA | (<element class="deprecated">mediatype¹</element>, <element>UICommon</element>*) </content> </entry> ... <entry class="element"> <name>submit</name> <common>Control Common</common> <attribute> <name>submission</name> <type>IDREF</type> </attribute> <content><element>UICommon</element>*</content> </entry> ...
Our first task is to display the data, which we will do in fairly generic manner. We have a top-level series of elements, each of which contains another series of elements. As a first try, we will do this:
<instance id="data" resource="data.xml"/> ... <group> <label><output value="local-name(/*)"/><label> <repeat ref="entry"> <repeat ref="@*"> @<output value="local-name(.)"/>: <output ref="."/> </repeat> <repeat ref="*"> <output value="local-name(.)"/>: <output ref="."/> </repeat> </repeat>
What you can see is that we repeat over the top-level entry
elements, and then within each entry, we repeat over the attributes and then
the sub-elements, whatever they are. To see what they are, we display the name
of the sub-element (with the local-name
function) along with its
value.
A repeat
by default displays its content on a new line
(actually CSS controls this and each repeat
item has
display: block)
. We can chnage that by adding CSS to make repeat
items have display: inline
. The repeat
s themselves
still have display: block
:
<style type="text/css"> .xforms-repeat-item {display: inline} </style>
We add a group round the inner repeats with a class
attribute,
and some extra CSS, such as a border:
<style type="text/css"> .xforms-repeat-item {display: inline} .entry {border-bottom: thin black solid} </style> ... <repeat ref="entry"> <group class="entry"> <repeat ref="@*"> @<output value="local-name(.)"/>: <output ref="."/> </repeat> <repeat ref="*"> <output value="local-name(.)"/>: <output ref="."/> </repeat> </group> </repeat>
Now we have a better idea of the structure of the data.
Instead of displaying the name of the sub-element, we will use that name as
the CSS class
for displaying the element's content, with CSS rules
for each type of element:
<repeat ref="entry"> <group class="entry"> <repeat ref="*"> <output class="{local-name(.)}" ref="."/> </repeat> </group> </repeat>
We can now add CSS rules using the names of the sub-elements, like:
.name {font-weight: bold; display: inline-block; width: 14ex} .common {color: green} .common::after {content: ", "} .attribute {color: blue} .attribute::after {content: ", "} .parameter {color: blue; padding-right: 1ex} .content {color: red}
Move the CSS to a separate file.
Hint: In the head use <link href="..." rel="stylesheet"
type="text/css"/>
Hint 2: You need to add at the top of the file after the
xml-stylesheet
line, the line
<?css-conversion no?>
To get a copy of the example XForm, right click on the 'Source' link above, and choose "Save link as...".
Save it to the directory called xforms
where you unpacked the
server.
Run it, use the link http://localhost:8082/xforms/display.xhtml
(If you download examples from later chapters, you have to download both the xform xyz.xhtml and its matching css file xyz.css.)
We're now going to add a search field, and only display the entries that match.
For that we need a new instance for the search string:
<instance id="q"> <q xmlns=""> <q/> </q> </instance>
and an input control for the search string:
<input incremental="true" ref="instance('q')/q"> <label>Search</label> </input>
and change the display so that it only displays entries that match:
<repeat ref="entry[contains(lower-case(.), lower-case(instance('q')/q))]">
After the search box add a trigger to clear the search box.
Now we're going to add the ability to look at a single entry in detail.
Uses a switch
element, with one case for viewing all entries,
and one for viewing just one (more possibilities later):
<switch> <case id="viewall"> ... the stuff we already have ... </case> <case id="viewone"> ... the new stuff ... </case> </switch>
To switch between the two we add a trigger in front of each displayed entry in the View All case, that will cause just that entry to be viewed, and a trigger in the View One case that will switch back to the View All. The second of these is the easier:
<trigger> <label>View all</label> <toggle case="viewall" ev:event="DOMActivate"/> </trigger>
For the other case, we need to record which entry we want to view before toggling:
<repeat ref="entry[contains(lower-case(.), lower-case(instance('q')/q))]"> <group class="entry {@class}"> <trigger> <label>Show</label> <action ev:event="DOMActivate"> <setvalue ref="instance('q')/selected" value="count(context()/preceding-sibling::*)+1"/> <toggle case="viewone"/> </action> </trigger> ...
Clearly this has required the addition of a new value selected
in the q
instance.
The case
for viewing one entry can then display the selected
entry:
<group ref="instance('data')/*[position()=instance('q')/selected]"> <output ref="@class"/> <repeat ref="*"> <group> <output value="local-name(.)"/>: <output ref="."/> </group> </repeat> </group>
Now to add the ability to edit an entry.
Add a new case
to the switch
:
<switch> <case id="viewall"> ... </case> <case id="viewone"> ... </case> <case id="edit"> ... </case> </switch>
Instead of displaying the selected entry, we copy it somewhere, allow the copy to be edited, and then the edited version can either be accepted, and thus copied back, or cancelled, in which case nothing will have changed.
We'll start this action from the view-one case. Remember, that the value of
selected
has already been set:
<trigger> <label>edit</label> <action ev:event="DOMActivate"> <insert ref="instance('copy')" origin="instance('data')/*[ position()=instance('q')/selected]"/> <toggle case="edit"/> </action> </trigger>
For this we need a new instance to store the copy in:
<instance id="copy"> <entry xmlns=""/> </instance>
and now we can write the case for editing.
This is similar to the view-one case, except using input
controls instead of output
:
<case id="edit"> <label>Edit</label> <group ref="instance('copy')"> <input ref="@class"><label>Class</label></input> <repeat ref="*"> <group> <input ref="."> <label><output value="local-name(.)"/></label> </input> </group> </repeat>
Of course, just changing the content of elements is not enough: you might want to delete or insert new ones. This is fairly easy. After the input control add:
<trigger> <label>+</label> <hint>Add another</hint> <insert ref="." ev:event="DOMActivate"/> </trigger> <trigger> <label>🗑</label> <hint>Delete</hint> <delete ref="." ev:event="DOMActivate"/> </trigger>
We then follow with the cancel
and save
triggers.
Cancel just returns us to the view-one case, leaving the data untouched:
<trigger> <label>Cancel</label> <toggle case="viewone" ev:event="DOMActivate"/> </trigger>
The save
trigger inserts the edited entry after the original
entry, and deletes the original:
<trigger> <label>Save</label> <action ev:event="DOMActivate"> <insert position="after" ref="instance('data')/*[position()=instance('q')/selected]" origin="instance('copy')/entry" /> <delete ref="instance('data')/*[position()=instance('q')/selected]"/> <toggle case="viewone"/> </action> </trigger>
An entry has one structured sub-element: <attribute>
that
has children <name>
and <type>
. That's
not a problem when viewing, since outputting a structured element just
concatenates the text nodes of the children. But for editing, we need to access
the individual fields. This exercise is to fix that.
While an easy solution would be to treat attribute
specially,
we want a generic solution. For output, that would look like this. Instead of
the current:
<repeat ref="*"> <group> <output value="local-name(.)"/>: <output ref="."/> </group> </repeat>
we use:
<repeat ref="*"> <group> <output value="local-name(.)"/>: <output ref=".[count(*) = 0]"/> <repeat ref="*"> <output class="child" value="local-name(.)"/>: <output ref="."/> </repeat> </group> </repeat>
The second output
line only outputs the current element's
content if it has no children. The repeat
after that only does
anything if there are children.
Your task is to change the edit case similarly.
(Note that [not(*)]
is an equivalent option for [count(*)
= 0]
.)
Having edited the data, we should ensure that it gets saved.
For this we need a submission
that saves the data back to the
file:
<submission id="save" ref="instance('data')" resource="data.xml" method="put" validate="false" replace="none"/>
and activate it when we change the data, by adding a
<send/>
to the save
trigger:
<trigger> <label>Save</label> <action ev:event="DOMActivate"> <insert position="after" ref="instance('data')/*[position()=instance('q')/selected]" origin="instance('copy')/entry" /> <delete ref="instance('data')/*[position()=instance('q')/selected]"/> <send submission="save"/> <toggle case="viewone"/> </action> </trigger>
You should always check that a submission gets successfully done:
<submission id="save" ref="instance('data')" resource="data.xml" method="put" validate="false" replace="none"> <action ev:event="xforms-submit-error"> <setvalue ref="instance('q')/message" value="concat('Not saved: ', event('response-reason-phrase'))"/> <message>Submission error on save error-type: <output value="event('error-type')"/> error-message: <output value="event('error-message')"/> response-status-code: <output value="event('response-status-code')"/> response-reason-phrase: <output value="event('response-reason-phrase')"/> resource-uri: <output value="event('resource-uri')"/> </message> </action> <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/message"/> </action> </submission>
This requires a new value to be added to the q
instance:
<instance id="q"> <q xmlns=""> <q/> <selected/> <message/> </q> </instance> <bind ref="instance('q')/message" relevant=". != ''"/>
which we should also display somewhere:
<output ref="instance('q')/message"><label>Error</label></output>
Since it is only relevant when it is non-empty, it will only be displayed when it has a value.
Give the error message a red background.
Hint: put a value in the message
element during testing.
Since we're now overwriting the data file, it would be good practice to create a backup at startup in case of disaster:
<send submission="backup" ev:event="xforms-ready"/>
with a similar submission
to the earlier one, though without a
message
since the user can't do anything about it if the backup
fails:
<submission id="backup" ref="instance('data')" resource="backup.xml" method="put" validate="false" replace="none"> <action ev:event="xforms-submit-error"> <setvalue ref="instance('q')/message" value="concat('Backup failed: ', event('response-reason-phrase'))"/> </action> <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/message"/> </action> </submission>
You only really need a backup if the data gets changed: if you are only browsing the data, a backup is not needed.
Make it so that the backup only gets done the first time a change is done, just before the insert and delete. Record that the backup has been done and don't backup on subsequent saves.
Since we are backing up the data, we can easily add a trigger to restore the data to its state at the start:
<trigger> <label>Restore</label> <hint>Restore the data to its state at the beginning of the run</hint> <send submission="restore" ev:event="DOMActivate/> </trigger>
which needs a new submission similar to the others:
<submission id="restore" resource="backup.xml" serialization="none" method="get" replace="instance" instance="data"> <action ev:event="xforms-submit-error"> <setvalue ref="instance('q')/message" value="concat('Restore failed: ', event('response-reason-phrase'))"/> <message>Submission error on restore...</message> </action> <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/message"/> </action> </submission>
We shouldn't offer the restore trigger until a backup has been successful.
So we record the state of the backup. We add a new value to the
q
instance:
<backup/>
If it is empty, there is no backup to restore, otherwise there is.
We should only display the restore trigger when there is a backup:
<bind ref="instance('q')/backup" relevant=". != ''"/>
with:
<trigger ref="instance('q')/backup"> <label>Restore</label> <hint>Restore the data to its state at the beginning of the run</hint> <send submission="restore" ev:event="DOMActivate/> </trigger>
and then set the value at suitable places.
Firstly, when we do a backup:
<submission id="backup" ref="instance('data')" resource="backup.xml" method="put" validate="false" replace="none"> <action ev:event="xforms-submit-error"> <setvalue ref="instance('q')/message" value="concat('Backup failed: ', event('response-reason-phrase'))"/> <setvalue ref="instance('q')/backup"/> </action> <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/message"/> <setvalue ref="instance('q')/backup">done</setvalue> </action> </submission>
Secondly when we do a restore, since now the data and backup are identical, so a restore has no purpose until the next change:
<submission id="restore" resource="backup.xml" serialization="none" method="get" replace="instance" instance="data"> ... <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/message"/> <setvalue ref="instance('q')/backup"/> </action> </submission>
Every time we make a change to the data, it gets saved so that the internal data and the file are always the same.
That means that after a successful restore we must also do a save, otherwise the file still has the old changed data.
Add this.
Adding an entry is similar to editing an existing entry, so we merge the two cases. The main differences are that the source of the value being edited comes from somewhere else, and there will be no existing entry selected.
We'll need a blank template to provide the source of the value to be edited, and we can use a trigger to add it:
<trigger> <label>+</label> <hint>Add an entry</hint> <action ev:event="DOMActivate"> <setvalue ref="instance('q')/selected"/> <insert ref="instance('copy')" context="instance('copy')" origin="instance('template')/*"/> <toggle case="edit"/> </action> </trigger>
(Note that instance('template')/*
is just the root element of
the template instance, whatever that root is called.)
To create a template of what an empty entry looks like, we now have to examine the data in a little more detail.
Each entry
has a class
attribute which
represented what sort of item is being described. The class can be:
model
, subelement
, control
,
common
, collection
, element
,
function
, action
, or deprecated
.
Each entry has a name
sub-element.
Depending on the value of class
, the entry can have other
different subelements.
model
, subelement
, control
,
element
, action
, deprecated
:
(common
| attribute
)*, content
.common
: (common
| attribute
)+.collection
: content
.function
: type
, parameter
*,
description
.Here is an instance for this, containing a template entry with the possible elements listed above:
<instance id="template"> <template xmlns=""> <entry class=""> <name/> <common/> <attribute> <name/> <type/> </attribute> <content/> <type/> <parameter/> <description/> </entry> </template> </instance>
We can now specify when these different elements are relevant:
<bind ref="@class" required="true()"/> <bind ref="common" relevant="../@class!='function' and ../@class!='collection'"/> <bind ref="attribute" relevant="../@class!='function' and ../@class!='collection'"/> <bind ref="content" relevant="../@class!='function' and ../@class!='common'"/> <bind ref="type" relevant="../@class='function'"/> <bind ref="parameter" relevant="../@class='function'"/> <bind ref="description" relevant="../@class='function'"/>
These apply to the copy
instance that we edit.
Here are the possible class
values:
<instance id="classes"> <classes xmlns=""> <class>model</class> <class>subelement</class> <class>control</class> <class>common</class> <class>collection</class> <class>element</class> <class>function</class> <class>action</class> <class>deprecated</class> </classes> </instance>
There are two changes we have to make to the edit
case. One is
that we want to be able to change the class
attribute properly.
Change
<input ref="@class"><label>Class</label></input>
to
<select1 ref="@class"> <label>Class</label> <itemset ref="instance('classes')/class"> <label ref="."/> <value ref="."/> </itemset> </select1>
When we save the changes, we need to distinguish the two cases of editing and adding:
<action ev:event="DOMActivate"> <send if="instance('q')/backup = ''" submission="backup"/> <action if="instance('q')/selected != ''"> <insert position="after" ref="instance('data')/*[position()=instance('q')/selected]" origin="instance('copy')" /> <delete ref="instance('data')/*[position()=instance('q')/selected]"/> </action> <action if="instance('q')/selected = ''"> <insert position="after" ref="instance('data')/*" origin="instance('copy')" /> </action> <send submission="save"/> <toggle case="viewall"/> </action>
After doing the save, we return to the 'view all' view.
However, we started the edit case from 'view one', and the add case from 'view all', so really we should return to where we came from.
Change it so that saving an edit returns to the view one case, and saving an add returns to the view all case.
Deleting an entry is just a trigger that deletes the selected entry:
<trigger> <label>Delete</label> <action ev:event="DOMActivate"> <send if="instance('q')/backup = ''" submission="backup"/> <delete ref="instance('data')/*[position()=instance('q')/selected]"/> <send submission="save"/> <toggle case="viewall"/> </action> </trigger>
To protect the user from accidently deleting, make it a two step process. Provide a trigger that causes a second trigger to appear that really does the delete:
<trigger appearance="minimal"> <label>Delete> </label> <action ev:event="DOMActivate"> <setvalue ref="instance('q')/really" value="if(.='', 'yes', '')"/> </action> </trigger> <trigger ref="instance('q')/really"> <label>Really delete</label> <action ev:event="DOMActivate"> <send if="instance('q')/backup = ''" submission="backup"/> <delete ref="instance('data')/*[position()=instance('q')/selected]"/> <send submission="save"/> <setvalue ref="instance('q')/really"/> <toggle case="viewall"/> </action> </trigger>
This uses a new value really
, that is only relevant if it has
non-empty:
<bind ref="instance('q')/really" relevant=". != ''"/>
You may have noticed that when "Really delete" pops up, that clicking on the Delete trigger cancels it.
Modify it so that the label on the Delete trigger changes to "Cancel" when the "Really delete" trigger is visible.
Hint: Use an output in the label that depends on the value of the
really
location.
Sometimes I have an application open on my desktop at home, and then when I'm out, I use it on my phone. When I return home, I start using it on my desktop again, but the data is now out-of-date with the changes I made on my phone.
So a check is needed whenever the data is about to change, to ensure the data and what has been saved in the file are the same.
We do this by recording the last-modified
time of the file
every time it is saved. Then whenever the data is about to be changed, we check
that the last-modified
time hasn't changed.
Whenever you submit to a server, along with any data returned, there is also
some metadata. We've seen that already with the error messages for the
xforms-submit-done
or the xforms-submit-error
event,
where we use the the event()
function:
<message>Submission error on save error-type: <output value="event('error-type')"/> error-message: <output value="event('error-message')"/> response-status-code: <output value="event('response-status-code')"/> response-reason-phrase: <output value="event('response-reason-phrase')"/> resource-uri: <output value="event('resource-uri')"/> </message>
To summarise the values. For both events:
resource-uri
: The URI used for the submission.response-status-code
: The return code from the serverresponse-reason-phrase
: A brief explanation of the status
code.response-headers
: Zero or more 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.Additionally for xforms-submit-error
:
error-type
: An indication from XForms (rather than from the
server) of the type of error involved, some of which indicate that the
submission didn't even happen, because of problems with the submission. The
possible values are: submission-in-progress, no-data, validation-error,
parse-error, resource-error, target-error.response-body
: since the response body is not saved when an
error occurs, it is available here.To see it in action, here is an XForm that gets a non-existent file, or does a HEAD on the data.xml file:
If you click on the HEAD button, you will see one of the headers is for
last-modified
, with a date and time value. That's the one we are
interested in.
The above XForm uses the fact that many events bubble: in this case
the two events get dispatched to the submission
elements, but then
travel up the tree of elements.
This means that you can catch them higher up (as in this case) if you don't
care which submission
element they were dispatched to.
<action ev:event="xforms-submission-error"> ... </action> <submission .../> <submission .../>
This will catch submission errors from both submissions.
What we are going to do is add a submission
to do a HEAD on the
data file, and store the value of last-modified
for later use.
<submission id="head" resource="data.xml" serialization="none" replace="none" method="head"> <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/last-modified" value="event('response-headers')//[name='last-modified']/value"/> </action> <action ev:event="xforms-submit-error"> <message>...</message> </action> </submission>
When we initially load data.xml
, and every time we save to it,
we will activate that submission:
<action ev:event="xforms-ready"> <send submission="head"/> </action>
Just before changing the data we determine if the value is still the same, and warn if it isn't:
<submission id="check" resource="data.xml" serialization="none" replace="none" method="head"> <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/check" value="event('response-headers')//[name='last-modified']/value"/> <message if="instance('q')/check != instance('q')/last-modified"> The data has been modified outside of this app. You should reload the data first. </message> </action> <action ev:event="xforms-submit-error"> <message>...</message> </action> </submission>
Add a submission to reload the data, like restore
, but from a
different file:
<submission id="reload" resource="data.xml" serialization="none" replace="instance" instance="data" method="get"> <action ev:event="xforms-submit-done"> <setvalue ref="instance('q')/last-modified" value="event('response-headers')//[name='last-modified']/value"/> </action> <action ev:event="xforms-submit-error"> <message>Submission error on RELOAD...</message> </action> </submission>
and a trigger to activate it, using the same method of making it visible when needed:
<bind ref="instance('q')/check" relevant=". != '' and . != ../last-modified"/> ... <trigger ref="instance('q')/check"> <label>Reload<label> ...
All the submission
elements have a similar structure in the
xforms-submit-error
sections.
We can use the fact that xforms-submit-error
bubbles to handle
it twice: once for the specific part in the body of the
submission
, and once above, to issue the error message.
Do this.
Our aim is that as little of the actual code needs to be changed when using it to create a different application.
First: move all application-specific data to external files:
<instance id="template" resource="template.xml"/> <instance id="classes" resource="classes.xml"/>
The code in two places assumes that the repeating element in the data is
called entry
.
One of these is when we copy the template into the copy instance:
<insert ref="instance('copy')" context="instance('copy')" origin="instance('template')/entry"/>
Since this is just the root element of the template, we can replace it without any harm with:
<insert ref="instance('copy')" context="instance('copy')" origin="instance('template')/*"/>
The other place is in the repeat
when viewing all:
<repeat ref="entry[contains(lower-case(.), lower-case(instance('q')/q))]">
While we could use the same trick here, it would prevent the data containing other elements than the repeated one. Instead we will create a new instance (in a file) that contains meta-information about the application.:
<meta> <title>XForms Quick Reference</title> <entry>entry</entry> </meta>
Then we can display the title at the top:
<group xmlns="http://www.w3.org/2002/xforms"> <label><output ref="instance('meta')/title"/></label>
And repeat over whatever the repeating entry is called:
<repeat ref="*[local-name()=instance('meta')/entry and contains(lower-case(.), lower-case(instance('q')/q))]">
We can also remove the dependency on the name of the data file. We can add
that to the meta
file:
<meta> <title>XForms Quick Reference (generic)</title> <data>data.xml</data> <entry>entry</entry> </meta>
and then on all submission
s that use it, replace
resource="data.xml"
with
resource="{instance('meta')/data}"
. (You could do that with
backup.xml
as well if you wanted)
There is one place where you can't do that replacement, namely on the instance:
<instance id="data" resource="data.xml"/>
To replace that we provide an empty instance:
<instance id="data"> <dummy xmlns=""/> </instance>
and ensure that the data file gets loaded at startup (luckily easy, because we already have the mechanism for doing that):
<action ev:event="xforms-ready"> <send submission="reload"/> </action>
Originally there was a <send submission="head"/>
done at
startup, but we don't need that any more, because reload
does that
anyway.
Do this.
In the file claims.xml
there is a set of entries about travel
costs of three types: travel that has been done, but not yet claimed; entries
that have been claimed, and not yet received; claims that have been made and
received.
Adapt the application to instead deal with this data.
Hint: First modify the meta
, classes
,
template
, and style.css
files.
The data is in date order. Ideally you would like the display to be ordered on the classes: first the unclaimed, then the claimed, then the received, which you could do like this:
<repeat ref="instance('classes')/class"> <repeat ref="instance('data')/*[name()=instance('meta')/entry and @class=context() and contains(lower-case(.), instance('q')/lcsearch)]">
Do it