XForms/Suggesting Items

Motivation

edit

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

edit

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.

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

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:

http://www.cems.uwe.ac.uk/xmlwiki/XForms/suggest-ingredient.xq?prefix=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:

<suggestions>
<tc>Ba</tc>
   <ingredient>Baking powder</ingredient>
   <ingredient>Bananas</ingredient>
   <ingredient>Barbecue Sauce</ingredient>
   <ingredient>Barley</ingredient>
   <ingredient>Barley, malt</ingredient>
   <ingredient>Basil</ingredient>
   <ingredient>Bass</ingredient>
   <ingredient>Bay leaves</ingredient>
</suggestions>

Screen Image

edit

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.

 
Suggestions displayed in the right margin of a form
edit

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

edit
<html xmlns:xf="http://www.w3.org/2002/xforms"
   xmlns:ev="http://www.w3.org/2001/xml-events"
   xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Suggest Event Test</title>
        <style type="text/css">
           @namespace xf url("http://www.w3.org/2002/xforms");
           body {font-family: Helvetica, sans-serif}
	#suggest {
            	position: absolute;
            	top: 0;
            	margin: 0 0 0 300px;
            	width: auto;
            	border: 1px solid blue;
	}
	#showlog {
		position: absolute;
		width: auto;
		font-size: 8pt;
		color: SlateGray;
		background-color: lavender;
		border: 1px solid SlateGray;
	}
	</style>
        <xf:model>
            <xf:instance xmlns="" id="my-form">
                <data>
                    <element/>
                    <element/>
                    <element/>
                </data>
            </xf:instance>
            <xf:instance xmlns="" id="conditional-views">
                <data>
                    <suggest-view/>
                </data>
            </xf:instance>

            <!-- only show the suggested values if we have at least one suggestion -->
            <xf:bind nodeset="instance('conditional-views')/suggest-view"
                     relevant="count(instance('suggest-results')//ingredient) &gt; 1"/>

        	<!-- this is the place that we store the parameters that are going out to the remote suggest service -->
            <xf:instance id="suggest-query">
                <query xmlns="">
                    <prefix/>
                </query>
            </xf:instance>

           <!-- this is where we put an ID to the calling element  -->
            <xf:instance xmlns="" id="selected-word">
                <data>
                    <calling-element/>
                </data>
            </xf:instance>

           <!-- this is where we put the suggested ingredients that are returned from the server -->
           <xf:instance id="suggest-results" xmlns="">
                <suggestions/>
            </xf:instance>

            <!-- This sends the request to the ingredient suggestion service at the U of West England.  -->
            <!-- Please make a local copy if you are doing more than a few examples for learning XForms and XQuery -->
            <xf:submission id="get-suggestions" action="http://www.cems.uwe.ac.uk/xmlwiki/XForms/suggest-ingredient.xq"
                method="get" separator="&amp;"
                ref="instance('suggest-query')"
                replace="instance" instance="suggest-results">
                <xf:action ev:event="xforms-submit">
                    <xf:insert nodeset="instance('log')/event" at="last()" position="after"/>
                    <xf:setvalue ref="instance('log')/event[last()]"
                        value="concat('getting suggestions for: ', instance('suggest-query')/prefix)"/>
                 </xf:action>
            </xf:submission>

           <!-- this is where we put the logging events -->
            <xf:instance id="log">
                <data xmlns="">
                    <event/>
                </data>
            </xf:instance>

            <!-- put the cursor in the first field when the form becomes ready -->
            <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>
    </head>
    <body>
        <h3>Suggest Event Test</h3>
        <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>
        <br/>
        <br/>

       <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>
        <br/>
        <br/>

       <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">
            <div id="suggest">
                <span>suggestions:</span>
                <xf:repeat id="results-repeat" nodeset="instance('suggest-results')/ingredient">
                    <xf:trigger>
                        <xf:label>
                            <xf:output ref="."/>
                        </xf:label>
                        <!-- When the use clicks on  suggestion -->
                        <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>
            </div>
        </xf:group>
        <br/>
       <xf:output ref="instance('selected-word')/calling-element"><xf:label>Prior-field: </xf:label></xf:output>
       <br/>

        <div id="showlog">
            <span>
                <b>Event Log</b>
            </span>
            <xf:repeat id="log-repeat" nodeset="instance('log')/event">
                <xf:output ref="."/>
            </xf:repeat>
        </div>
    </body>
</html>

Discussion

edit

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:

<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>

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

edit

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

<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')/word[index('results-repeat')]"/>
</xf:action>

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

edit

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"

xquery version "1.0";
declare namespace exist="http://exist.sourceforge.net/NS/exist";
declare option exist:serialize "method=xml media-type=text/xml indent=yes";
let $collection-path := '/db/mdr/glossaries/data'
let $search-str := request:get-parameter('prefix', '')
return
<suggestions>{
       for $term in collection($collection-path)/Term[starts-with(TermName/text(),$search-str)]
       return <item>{$term/TermName/text()}</item>
}</suggestions>
edit

Load Non-logging XForms Application

Version with no logging

edit
<html xmlns:xf="http://www.w3.org/2002/xforms"
   xmlns:ev="http://www.w3.org/2001/xml-events"
   xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Suggest Event Test</title>
        <style type="text/css">
           @namespace xf url("http://www.w3.org/2002/xforms");
           body {font-family: Helvetica, sans-serif}
	       #suggest {
            	position: absolute;
            	top: 0;
            	margin: 0 0 0 300px;
            	width: auto;
            	border: 1px solid blue;
	       }
	   </style>
       <xf:model>
          <xf:instance xmlns="" id="my-form">
              <data>
                  <element/>
                  <element/>
                  <element/>
              </data>
          </xf:instance>
          <xf:instance xmlns="" id="conditional-views">
              <data>
                  <suggest-view/>
              </data>
           </xf:instance>

           <!-- only show the suggested values if we have more than one suggestion -->
           <xf:bind nodeset="instance('conditional-views')/suggest-view"
                     relevant="count(instance('suggest-results')//ingredient) &gt; 1"/>

        	<!-- this is the place that we store the parameters that are going out to the remote suggest service -->
            <xf:instance id="suggest-query">
                <query xmlns="">
                    <prefix/>
                </query>
            </xf:instance>

           <!-- this is where we put an ID to the calling element  -->
            <xf:instance xmlns="" id="selected-word">
                <data>
                    <calling-element/>
                </data>
            </xf:instance>

           <!-- this is where we put the suggested ingredients that are returned from the server -->
           <xf:instance id="suggest-results" xmlns="">
                <suggestions/>
            </xf:instance>

            <!-- This sends the request to the ingredient suggestion service at the U of West England.  -->
            <!-- Please make a local copy if you are doing more than a few examples for learning XForms and XQuery -->
            <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>

           <!-- this is where we put the logging events -->
            <xf:instance id="log">
                <data xmlns="">
                    <event/>
                </data>
            </xf:instance>

            <!-- put the cursor in the first field when the form becomes ready -->
            <xf:action ev:event="xforms-ready">
                <xf:setfocus control="field-1"/>
            </xf:action>
        </xf:model>
    </head>
    <body>
       <h3>Suggest Event Test</h3>
       <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>
       <br/>
       <br/>

       <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>
       <br/>
       <br/>

       <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">
            <div id="suggest">
               <span>suggestions:</span>
               <xf:repeat id="results-repeat" nodeset="instance('suggest-results')/ingredient">
                   <xf:trigger>
                       <xf:label>
                           <xf:output ref="."/>
                       </xf:label>
                       <!-- When the use clicks on  suggestion -->
                       <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>
            </div>
        </xf:group>
    </body>
</html>

Using Minimal Trigger Appearance

edit

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:

.suggestion:hover {
   background-color: blue;
   color: white;
   padding: 3px;
   font-size: 10pt;
}

Preventing Null Searches

edit

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.

<xf:action ev:event="xforms-value-changed" if="string-length(instance('save-data')/element[1]) gt 0">
      <xf:setvalue ref="instance('suggest-query')/prefix" value="instance('save-data')/element[1]"/>
      <xf:send submission="get-suggestions"/>
</xf:action>

Discussion

edit

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

Next Page: Slideshow | Previous Page: Dynamic Labels
Home: XForms