Steven Pemberton, CWI Amsterdam
Version: 2020-02-16.
For selections that have a lot of options, it can be irritating to have to scroll through a long list of values to get to the one you want.
For instance, selecting a country from the list of all countries when filling in an address, all the more so if you are not sure which form of the country name is being used: do they want England, Great Britain, United Kingdom, or UK? Is it Holland, Netherlands, or The Netherlands?
You have a value you want to enter, say country
. Furthermore,
you have a document containing the list of possible countries.
<instance id="data"> <data xmlns=""> <country/> </data> </instance> <instance id="countries" src="countries.xml"/>
The document countries
looks like this:
<countries> <country>Afghanistan</country> <country>Albania</country> <country>Algeria</country> <country>Andorra</country> <country>Anguilla</country> ...
The traditional way of doing it is to use a select1
:
<select1 ref="country" label="Country" appearance="minimal"> <itemset ref="instance'countries')/country"> <label ref="."/> <value ref="."/> </itemset> </select1/> <output ref="country" label="Your choice"/>
That looks like this:
As you can see, a huge list.
What we will do instead is have a plain input
for the
country:
<input ref="country" incremental="true" label="Country"/>
and under it a select1
that displays the options that match
that input.
To do this we will have a support value called suggestion
:
<instance id="admin"> <admin xmlns=""> <suggestion/> </admin> </instance>
This value is only of relevance if something has been typed in for the country already:
<bind ref="instance('admin')/suggestion" relevant="instance('data')/country!='.'"/>
and we input it using a select1
that only contains the items
from the list of countries that match the input typed so far.
The countries that match are:
country[starts-with(., instance('data')/country)]
So here is the suggestion select1
:
<select1 ref="suggestion" appearance="full" label="Suggestions"> <itemset ref="instance('countries')/country[starts-with(. instance('v')/country)]"> <label ref="."/> <value ref="."/> </itemset> </select1>
Here it is in use:
Four remarks.
Firstly you'll see that the suggestions pop up when you start typing. This is thanks to the relevance bind.
Secondly, you'll see that as you type, the suggestions adapt. This is thanks
to the incremental="true"
on the input
, and the
filter on the select1
.
Thirdly, you might notice that it is a case-sensitive match. We can fix that easily enough by changing the filter to compare the lower case versions of both strings:
country[starts-with(lower-case(.), lower-case(instance('data')/country))]
Finally, and most importantly, when you select a suggestion, nothing happens. We fix that by copying the value across, when a selection is chosen:
<select1 ref="instance('admin')/suggestion" appearance="full" label="Suggestions"> <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>
giving:
We're nearly there.
You'll see that one suggestion remains after you have made a choice. We can
make that disappear by fixing the relevance condition for
suggestion
. Once the choice is made, country
and
suggestion
will be equal, and so, suggestion
is no
longer relevant:
<bind ref="instance('admin')/suggestion" relevant="instance('data')/country!='' and instance('data')/country!=."/>
Finally, we should add a validity constraint to the country
value: it is only valid if it matches one of the values of the
countries
list:
<bind ref="country" constraint="count(instance('countries')/country[.=instance('data')/country])=1"/>
Actually, we could make one more improvement: if you type in the country's
name, and completely ignore the suggestions, a single suggestion remains. We
can get rid of this by adjusting the relevance for suggestions
, so
that if you type in a name that exactly matches a country name, the suggestions
disappear:
<bind ref="instance('admin')/suggestion" relevant="instance('data')/country!='' and instance('data')/country!=. and count(instance('countries')/country[.=instance('data')/country])!=1" />
And just to compare, here is a version where instead of
starts-with
, we use contains
to generate the
suggestions:
country[contains(lower-case(.), lower-case(instance('data')/country))]