XForms/Suggesting Items

Motivation
You have a text field that the users types text into. You want to show a list of possible suggested items as the user types. This feature is also known as autocomplete.

Method
Use a standard input control and set the incremental="true" attribute. Use the xforms-value-changed event to trigger a submission that puts possible values in a list. When the user select a value from the suggestion list it will replace the text value in the field.

We will also add a bind rule so that the selection view will only be visible if there is at least one suggested item.

This program depends on a server-side REST web service that you pass in a single parameter "prefix" such as the following that returns recipe ingredients that begin with ba:

This program returns an XML file that is restricted to the suggested ingredients that begin with the prefix passed to the XQuery web service:

Screen Image
The following example shows that as the user types in the letter "c", a list of possible ingredients instantly appears in the right column. The user can continue typing and the list will be restriced to the suggestions that start with the letters typed. When the user selects an item it will fill in the prior selected field.



Link to XForms Application
NOTE: To get this program to run you will first have to add our server to the XForms "Trusted Sites" list.

To do this in FireFox, go to your Tools/Options/Security menu and add www.cems.uwe.ac.uk and googlecode.com to your allowed sites list. This allows the form (loaded from the googlecode domain) to pull data from the server at www.cems.uwe.ac.uk.

After you do this click the link below:

Load XForms Application

Sample Program
  



 

 

 

     

<xf:instance id="log"> </xf:instance>

<xf:action ev:event="xforms-ready"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'xforms-ready'"/> <xf:setfocus control="field-1"/> </xf:action> </xf:model> Suggest Event Test <xf:input ref="instance('my-form')/element[1]" incremental="true" id="field-1"> <xf:label>Term 1:</xf:label> <xf:action ev:event="DOMFocusIn"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'Focus into input 1'"/> </xf:action> <xf:action ev:event="xforms-value-changed"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'Value changed in input 1'"/> <xf:setvalue ref="instance('suggest-query')/prefix" value="instance('my-form')/element[1]"/> <xf:send submission="get-suggestions"/> </xf:action> <xf:action ev:event="DOMFocusOut"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'Out of input 1'"/> <xf:setvalue ref="instance('selected-word')/calling-element" value="'1'"/> </xf:action> </xf:input>

<xf:input ref="instance('my-form')/element[2]" incremental="true" id="field-2"> <xf:label>Term 2:</xf:label> <xf:action ev:event="DOMFocusIn"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'FocusIn input 2'"/> </xf:action> <xf:action ev:event="xforms-value-changed"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'Value changed in input 2'"/> <xf:setvalue ref="instance('suggest-query')/prefix" value="instance('my-form')/element[2]"/> <xf:send submission="get-suggestions"/> </xf:action> <xf:action ev:event="DOMFocusOut"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'FocusOut input 2'"/> <xf:setvalue ref="instance('selected-word')/calling-element" value="2"/> </xf:action> </xf:input>

<xf:input ref="instance('my-form')/element[3]" incremental="true" id="field-3"> <xf:label>Term 3:</xf:label> <xf:action ev:event="DOMFocusIn"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'FocusIn input 3'"/> </xf:action> <xf:action ev:event="xforms-value-changed"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="'Value changed in input 3'"/> <xf:setvalue ref="instance('suggest-query')/prefix" value="instance('my-form')/element[3]"/> <xf:send submission="get-suggestions"/> </xf:action> <xf:action ev:event="DOMFocusOut"> <xf:setvalue ref="instance('selected-word')/calling-element" value="'3'"/> </xf:action> </xf:input>

<xf:group ref="instance('conditional-views')/suggest-view"> suggestions: <xf:repeat id="results-repeat" nodeset="instance('suggest-results')/ingredient"> <xf:trigger> <xf:label> <xf:output ref="."/> </xf:label> <xf:action ev:event="DOMActivate"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="concat('Clicked on a suggestion word:',                               instance('suggest-results')/word[index('results-repeat')])"/> <xf:setvalue ref="instance('my-form')/element[number(instance('selected-word')/calling-element)]" value="instance('suggest-results')/ingredient[index('results-repeat')]"/> </xf:action> </xf:trigger> </xf:repeat> </xf:group> <xf:output ref="instance('selected-word')/calling-element"><xf:label>Prior-field: </xf:label></xf:output>

Event Log <xf:repeat id="log-repeat" nodeset="instance('log')/event"> <xf:output ref="."/> </xf:repeat>

Discussion
The above example also has several lines of event logging that can be removed in a production application.

As the user is typing and they see a suggestion they like in the suggestion view they will click on the suggested item trigger. When the focus moves out of the field we must keep track of the element that they just left. This is done by the following:

This example attempts to reuse the selection functions for many elements. To do this we have to get the items selected into the right field. In this example we just set an integer value for the element.

Although this works if all your elements have the same element name you will have to replace the last setvalue with an equivalent string expression if you have many complex items in a form that need suggested values.

Setting the Field Value
The following action is used when the use clicks on suggestion:

The first two values are only for logging. The last setvalue sets the field of the element that has the calling-element value. So for the second element the value would be instance('my-form')/element[2].

Sample XQuery
As an example XQuery, we have a collection of "Term" elements with "TermName" subelements. The following XQuery is use for our server side script. If you use eXist just place it in the same collection as the form and call it "suggest-item.xq"

Link to Non-logging XForms Application
Load Non-logging XForms Application

Version with no logging
</xf:instance> <xf:instance xmlns="" id="conditional-views"> </xf:instance>

<xf:bind nodeset="instance('conditional-views')/suggest-view" relevant="count(instance('suggest-results')//ingredient) &gt; 1"/>

<xf:instance id="suggest-query"> </xf:instance>

<xf:instance xmlns="" id="selected-word"> </xf:instance>

<xf:instance id="suggest-results" xmlns=""> </xf:instance>

<xf:submission id="get-suggestions" action="http://www.cems.uwe.ac.uk/xmlwiki/XForms/suggest-ingredient.xq" method="get" separator="&" ref="instance('suggest-query')" replace="instance" instance="suggest-results"> </xf:submission>

<xf:instance id="log"> </xf:instance>

<xf:action ev:event="xforms-ready"> <xf:setfocus control="field-1"/> </xf:action> </xf:model> Suggest Event Test <xf:input ref="instance('my-form')/element[1]" incremental="true" id="field-1"> <xf:label>Term 1:</xf:label> <xf:action ev:event="xforms-value-changed"> <xf:setvalue ref="instance('suggest-query')/prefix" value="instance('my-form')/element[1]"/> <xf:send submission="get-suggestions"/> </xf:action> <xf:action ev:event="DOMFocusOut"> <xf:setvalue ref="instance('selected-word')/calling-element" value="'1'"/> </xf:action> </xf:input>

<xf:input ref="instance('my-form')/element[2]" incremental="true" id="field-2"> <xf:label>Term 2:</xf:label> <xf:action ev:event="xforms-value-changed"> <xf:setvalue ref="instance('suggest-query')/prefix" value="instance('my-form')/element[2]"/> <xf:send submission="get-suggestions"/> </xf:action> <xf:action ev:event="DOMFocusOut"> <xf:setvalue ref="instance('selected-word')/calling-element" value="2"/> </xf:action> </xf:input>

<xf:input ref="instance('my-form')/element[3]" incremental="true" id="field-3"> <xf:label>Term 3:</xf:label> <xf:action ev:event="xforms-value-changed"> <xf:setvalue ref="instance('suggest-query')/prefix" value="instance('my-form')/element[3]"/> <xf:send submission="get-suggestions"/> </xf:action> <xf:action ev:event="DOMFocusOut"> <xf:setvalue ref="instance('selected-word')/calling-element" value="'3'"/> </xf:action> </xf:input>

<xf:group ref="instance('conditional-views')/suggest-view"> suggestions: <xf:repeat id="results-repeat" nodeset="instance('suggest-results')/ingredient"> <xf:trigger> <xf:label> <xf:output ref="."/> </xf:label> <xf:action ev:event="DOMActivate"> <xf:insert nodeset="instance('log')/event" at="last" position="after"/> <xf:setvalue ref="instance('log')/event[last]" value="concat('Clicked on a suggestion word:', instance('suggest-results')/word[index('results-repeat')])"/> <xf:setvalue ref="instance('my-form')/element[number(instance('selected-word')/calling-element)]" value="instance('suggest-results')/ingredient[index('results-repeat')]"/> </xf:action> </xf:trigger> </xf:repeat> </xf:group>

Using Minimal Trigger Appearance
If you do not like the appearance of the button in the trigger you can add the appearance="minimal" attribute to the trigger element.

The following CSS will reverse the background and font colors when the user hovers over the trigger:

Preventing Null Searches
XForms 1.1 added the conditional event attribute. You can use this to only request an suggestion from the server if the element is at least one character long.

Discussion
The functionality of suggesting items is contained in the Orbeon Autocomplete component Orbon XBL Autocomplete

XSLTForms has an example that uses the JSON results from Wikipedia search to suggest pages: XSLT Autocomplete example