Data-driven XForms

Steven Pemberton, CWI, Amsterdam

2017-06-30

Introduction

A request came in for a form that had a short introduction, gave the user a choice of three options, and then revealed a small number of questions, based on the choice.

Something like this (try it):

A First Version

In the most obvious and direct approach, the body could look like this:

Please help us discover problems and solutions that would improve our processes.

<select1 ref="choice" appearance="full">
   <label>How can you help?</label>
   <item><label>You know a problem that needs to be fixed</label><value>problem</value></item>
   <item><label>You know a 'solution' that doesn't work</label><value>failure</value></item>
   <item><label>You have a prediction about a future possible failure</label><value>prediction</value></item>
</select1>

<switch ref="choice">

   <case name=""/> <!-- Until a choice is made, nothing is displayed -->

   <case name="problem">
      <group ref="problem">
         <label>You know a problem that needs to be fixed</label>
         <textarea ref="problem"><label>What problem do you see?</label></textarea>
         <textarea ref="solution"><label>Can you propose a solution?</label></textarea>
      </group>
   </case>

   <case name="failure">
      <group ref="failure">
         <label>You know a 'solution' that doesn't work</label>
         <textarea ref="problem"><label>What 'solution' will fail or cause trouble?</label></textarea>
         <textarea ref="solution"><label>Can you propose a fix?</label></textarea>
      </group>
   </case>

   <case name="prediction">
      <group ref="prediction">
         <label>You have a prediction about a future possible failure</label>
         <textarea ref="problem"><label>What is your scenario?</label></textarea>
         <select1 ref="likely">
            <label>How likely is this scenario?</label>
            <item><label>High</label><value>1</value></item>
            <item><label>Medium</label><value>2</value></item>
            <item><label>Low</label><value>3</value></item>
         </select1>
         <textarea ref="who"><label>Who should address these issues?</label></textarea>
         <textarea ref="solution"><label>Can you propose a solution?</label></textarea>
      </group>
   </case>

</switch>

<submit><label>Submit</label></submit>

This uses the following instance for the data:

<instance id="data">
   <data xmlns="">
      <choice/>
      <problem>
         <problem/>
         <solution/>
      </problem>
      <failure>
         <problem/>
         <solution/>
      </failure>
      <prediction>
         <problem/>
         <likely/>
         <who/>
         <solution/>
      </prediction>
   </data>
</instance>

Making the form multi-lingual

One of the early decisions made was that since this form would be used in a multi-lingual environment, it would be made so that the user could select which language to use.

This involved creating an instance that contained all the labels and other texts:

<instance id="m">
  <messages xmlns="" lang="en">
    <intro>Please help us discover problems and solutions that would improve our processes.</intro>
    ...
  </messages>
</instance>

and in the body:

<output ref="instance('m')/intro"/>

For the select1, the messages:

<choice>
   <label>How can you help?</label>
   <item value="problem">You know a problem that needs to be fixed</item>
   <item value="failure">You know a 'solution' that doesn't work</item>
   <item value="prediction">You have a prediction about a future possible failure</item>
</choice>

with the body now reading:

<select1 ref="choice">
   <label ref="instance('m')/choice/label"/>
   <itemset ref="instance('m')/choice/item"><label ref="."/><value ref="@value"/></itemset>
</select1>

And for the groups within the cases, the messages:

<problem>
   <label>You know a problem that needs to be fixed</label>
   <problem>What problem do you see?</problem>
   <solution>Can you propose a solution?</solution>
</problem>

And in the body:

<case name="problem">
   <group ref="problem">
      <label ref="instance('m')/problem/label"/>
      <textarea ref="problem"><label ref="instance('m')/problem/problem"/></textarea>
      <textarea ref="solution"><label ref="instance('m')/problem/solution"/></textarea>
   </group>
</case>

Generalising

At was at this point the similarities between the different sets of answers was noticed, and how they could be generalised. The data instance was changed so that instead of:

<instance id="data">
   <data xmlns="">
      <choice/>
      <problem>
         <problem/>
         <solution/>
      </problem>
      <failure>
         <problem/>
         <solution/>
      </failure>
      <prediction>
         <problem/>
         <likely/>
         <who/>
         <solution/>
      </prediction>
   </data>
</instance>

the following was used:

<instance id="data">
   <data xmlns="">
      <choice/>
      <answer choice="problem">
         <problem/>
         <solution/>
      </answer>
      <answer choice="failure">
         <problem/>
         <solution/>
      </answer>
      <answer choice="prediction">
         <problem/>
         <likely/>
         <who/>
         <solution/>
      </answer>
   </data>
</instance>

This then allowed replacing the whole switch with a single group. The group selects only the answer whose choice attribute matches that of the value actually chosen:

<group ref="answer[@choice=../choice]">
   <label ref="instance('m')/answer/item[@choice=context()/@choice]/label"/>
   <textarea ref="problem">
      <label ref="instance('m')/answer/item[@choice=context()/../@choice]/problem"/>
   </textarea>
   <select1 ref="likely">
      <label ref="instance('m')/answer/item[@choice=context()/../@choice]/likely/label"/>
      <itemset ref="instance('m')/answer/item[@choice=context()/../@choice]/likely/item">
         <label ref="."/><value ref="@value"/>
      </itemset>
   </select1>
   <textarea ref="who">
      <label ref="instance('m')/answer/item[@choice=context()/../@choice]/who"/>
   </textarea>
   <textarea ref="solution">
      <label ref="instance('m')/answer/item[@choice=context()/../@choice]/solution"/>
   </textarea>
</group>

Note that:

The message for the group's label is selected using item[@choice=context()/@choice]. This selects the item element, whose choice attribute has the same value as the context element's choice attribute. In this case the context element is the answer element that has been selected.

For the first textarea element, the context item is now problem, which is a child of answer, so you have to go up one level to get to answer, in order to get its choice attribute: item[@choice=context()/../@choice].

The change to the instance data required an associated change to the structure of the messages instance:

<answer>
   <label>How can you help?</label>
   <item choice="problem">
      <label>You know a problem that needs to be fixed</label>
      <problem>What problem do you see?</problem>
      <solution>Can you propose a solution?</solution>
   </item>
   <item choice="failure">
      <label>You know a 'solution' that doesn't work</label>
      <problem>What 'solutions' will fail or cause trouble?</problem>
      <solution>Can you propose a solution?</solution>
   </item>
   <item choice="prediction">
      <label>You have a prediction about a future possible failure</label>
      <problem>What is your scenario?</problem>
      <likely>
         <label>How likely is this scenario?</label>
         <item value="1">High</item>
         <item value="2">Medium</item>
         <item value="3">Low</item>
      </likely>
      <who>Who should address these issues?</who>
      <solution>Can you propose a solution?</solution>
   </item>
</answer>

Extras

Since it is pointless to submit the data for answers that have not been chosen, a bind was added to the data instance that said that the only answer that was relevant was the one that matched the value of choice:

<bind ref="answer" relevant="@choice=../choice"/>

Also the submit button was not displayed until a set of answers had been chosen. To do this, an admin instance was created that contained an element that indicated whether the submit button should be displayed:

<instance id="admin">
   <admin xmlns="">
      <submit/>
   </admin>
</instance>
<bind ref="instance('admin')/submit" relevant="instance('data')/choice != ''"/>

changing the submit element to:

<submit ref="instance('admin')/submit"><label ref="instance('m')/submit"/></submit>

Finally, a message of thanks was added, once the data had been submitted, and the user was allowed to enter another set of answers.

<group ref="instance('admin')/thanks">
   <label ref="instance('m')/thanks"/>
   <trigger>
      <label ref="instance('m')/another"/>
      <action ev:event="DOMActivate">
         <reset/>
      </action>
   </trigger>
</group>

When the "another" button gets pressed, the form is just reset to its initial state. To support the "thanks" message, the admin instance was added to:

<instance id="admin">
   <admin xmlns="">
      <submit/>
      <submitted/>
      <thanks/>
   </admin>
</instance>
<bind ref="instance('admin')/submit" relevant="instance('data')/choice != '' and ../submitted!='yes'"/>
<bind ref="instance('admin')/thanks" relevant="../submitted='yes'"/>

and the submit button changed to:

<submit ref="instance('admin')/submit">
   <label ref="instance('m')/submit"/>
   <action ev:event="DOMActivate">
      <setvalue ref="instance('admin')/submitted" value="'yes'"/>
   </action>
</submit>

The Resulting Code

The data instance was put in an external file, and loaded from there:

<instance id="data" src="data.xml"/>

and likewise for the messages:

<instance id="m" src="messages.xml"/>

Giving the following result:

<model xmlns="http://www.w3.org/2002/xforms">

   <instance id="data" src="data.xml"/>
   <bind ref="answer" relevant="@choice=../choice"/>
   <submission resource="add-data" method="post"/>

   <instance id="m" src="messages.xml"/>

   <instance id="admin">
      <admin xmlns="">
         <submit/>
         <submitted/>
         <thanks/>
      </admin>
   </instance>
   <bind ref="instance('admin')/submit" 
         relevant="instance('data')/choice != '' and ../submitted!='yes'"/>
   <bind ref="instance('admin')/thanks" relevant="../submitted='yes'"/>

</model>

<group xmlns="http://www.w3.org/2002/xforms">
   <label ref="instance('m')/label"/>
   <output ref="instance('m')/intro"/>

   <select1 ref="choice" appearance="full">
      <label ref="instance('m')/answer/label"/>
      <itemset ref="instance('m')/answer/item"><label ref="label"/><value ref="@choice"/></itemset>
   </select1>

   <group ref="answer[@choice=../choice]">
      <label><output ref="instance('m')/answer/item[@choice=context()/@choice]/label"/></label>
      <textarea ref="problem">
         <label ref="instance('m')/answer/item[@choice=context()/../@choice]/problem"/>
      </textarea>
      <select1 ref="likely">
         <label ref="instance('m')/answer/item[@choice=context()/../@choice]/likely/label"/>
         <itemset ref="instance('m')/answer/item[@choice=context()/../@choice]/likely/item">
            <label ref="."/><value ref="@value"/>
         </itemset>
      </select1>
      <textarea ref="who">
         <label ref="instance('m')/answer/item[@choice=context()/../@choice]/who"/>
      </textarea>
      <textarea ref="solution">
         <label ref="instance('m')/answer/item[@choice=context()/../@choice]/solution"/>
      </textarea>
   </group>

   <submit ref="instance('admin')/submit">
      <label ref="instance('m')/submit"/>
      <action ev:event="DOMActivate">
         <setvalue ref="instance('admin')/submitted" value="'yes'"/>
      </action>
   </submit>

   <group ref="instance('admin')/thanks">
      <label ref="instance('m')/thanks"/>
      <trigger>
         <label ref="instance('m')/another"/>
         <action ev:event="DOMActivate">
            <reset/>
         </action>
      </trigger>
   </group>

</group>

Analysis

We now have a form that is driven from two data files: the template for the data, and the messages.

Suppose we wanted to add a fourth option to the form; all we have to do is add it to the data template, with a suitable new choice value:

<answer choice="solution"><problem/><who/><solution/></answer>

and matching messages in the message file:

<item choice="solution">
   <label>You know an existing solution that can be adopted</label>
   <problem>What solution do you know of?</problem>
   <who>Who should we approach?</who>
   <solution>How could we best adopt the solution?</solution>
</item>

and the form now works without change for the new entry.

This makes life much easier for the content providers: they can make textual changes to the form without having to ask the programmers to do it, they can add new cases themselves fairly easily. In fact, you could even make a form to simplify the process!

Making the form multi-lingual (slight return)

Although the form has been prepared to be multi-lingual, it isn't so yet.

The first thing that must be done, is a select1 added at the top of the page, to allow setting of language:

<select1 ref="lang">
  <label>🗨</label>
  <item><label>English</label><value>en</value/></item>
  <item><label>Nederlands</label><value>nl</value/></item>
  ...
</select1>

This adds a new value to the data instance, called lang, which should be initialised in the data.xml file to whatever the default language should be.

Of course, this is not how we are going to do the select1, but instead access the values from an instance:

<select1 ref="lang">
  <label>🗨</label>
  <itemset ref="instance('languages')/item"><label ref="."/><value ref="@code"/></itemset>
</select1>

where the languages instance is loaded from a file:

<instance id="languages" src="languages.xml"/>

and looks like this:

<languages>
   <item code="en">English</item>
   <item code="nl">Nederlands</item>
   ...
</languages>

What we want, is that each time a different language is selected, that the messages instance is replaced with values from a different file. So we add to the select1:

<select1 ref="lang">
  <label>🗨</label>
  <itemset ref="instance('languages')/item"><label ref="."/><value ref="@code"/></itemset>
  <action ev:event="xforms-value-changed">
     <send submission="change-language"/>
  </action>
</select1>

and add an extra submission element:

<submission id="change-language" method="get" replace="instance" instance="m">
   <resource value="concat('lang-', lang, '.xml')"/>
</submission>

which would, if Nederlands is chosen, replace the messages instance with the contents of the file lang-nl.xml.

This means that to add a new language, let's say for French, you just translate the lang-en.xml file and store the result in lang-fr.xml, and add the single line to the languages.xml file:

<item code="fr">Français</item>

Wrapping up

Our form is nearly complete. However, adding the language selection has changed one thing: if the user clicks the "Add another" button, the form is returned to its initial state using the <reset/> action. However, this also reverts to the default language, which isn't very polite. So, we have to change the thanks section from

   <group ref="instance('admin')/thanks">
      <label ref="instance('m')/thanks"/>
      <trigger>
         <label ref="instance('m')/another"/>
         <action ev:event="DOMActivate">
            <reset/>
         </action>
      </trigger>
   </group>

to

   <group ref="instance('admin')/thanks">
      <label ref="instance('m')/thanks"/>
      <trigger>
         <label ref="instance('m')/another"/>
         <action ev:event="DOMActivate">
            <send submission="restart"/>
            <setvalue ref="instance('admin')/submitted"/>
         </action>
      </trigger>
   </group>

where the reset submission just reloads the original data file:

<submission id="restart" method="get" resource="data.xml" 
            replace="instance" instance="data"/>

Now we really are done. Here's the result: