This is a brand new tutorial, so I am happy to get feedback.
There is way more material than we can cover in three hours! But the tutorial is online, so you can continue studying at home.
<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>
<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()"/>
<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
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>
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>
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>
<repeat ref="shopping/produce"> <output ref="."/> </repeat>
<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>
<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 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.
Add hints to the triggers.
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.)
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('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>
<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>
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('data')/produce"/> <toggle case="closed"/> </action> </trigger>
<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> ...
<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>
See the example for preventing adding values with spaces, and preventing duplicates.
Change the example so that when you add a new element, it is already selected.
<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
!
<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>
<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>
<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
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:
<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>
In the future this will be easier:
<instance src="items.xml" id="list"> <list xmlns=""> <item>your data here</item> </list> </instance>
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.
<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 < instance('new')/value/@date]" />
Or before dates that are greater:
<insert origin="instance('new')/value" position="before" ref="value[@date > instance('new')/value/@date]" />
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> ...
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>
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!='.'"/>
country[ starts-with(lower-case(.), lower-case(instance('data')/country)) ]
<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>
The suggestions should disappear:
<bind ref="instance('admin')/suggestion" relevant="instance('data')/country!='' and instance('data')/country!=."/>
<bind ref="country" constraint="count(instance('countries')/country [.=instance('data')/country])=1"/>
(There's one other improvement in the tutorial)
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>
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
<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
There isn't one
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.
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.
<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)
Four triggers:
<trigger label="Home"> <setvalue ev:event="DOMActivate" ref="instance('tab')/selected">home</setvalue> </trigger>
<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>
The rest is styling using CSS
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!
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.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', '')"/>
<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>
<bind ref="cursor" calculate="if(../state='up', 'pointer', 'move')"/>
and style the outer group suitably:
<group style="cursor: {cursor}">
The tutorial is written in XForms (of course)
At the back