Question

As of now (Dojo 1.9.2) I haven't been able to find a Dojo autocomplete widget that would satisfy all of the following (typical) requirements:

  • Only executes a query to the server when a predefined number of characters have been entered (without this, big datasets should not be queried)
  • Does not require a full REST service on the server, only a URL which can be parametrized with a search term and simply returns JSON objects containing an ID and a label to display (so the data-query to the database can be limited just to the required data fields, not loading full data-entities and use only one field thereafter)
  • Has a configurable time-delay between the key-releases and the start of the server-query (without this excessive number of queries are fired against the server)
  • Capable of recognizing when there is no need for a new server-query (since the previously executed query is more generic than the current one would be).
  • Dropdown-stlye (has GUI elements indicating that this is a selector field)

I have created a draft solution (see below), please advise if you have a simpler, better solution to the above requirements with Dojo > 1.9.

No correct solution

OTHER TIPS

The AutoComplete widget as a Dojo AMD module (placed into /gefc/dijit/AutoComplete.js according to AMD rules):

//
// AutoComplete style widget which works together with an ItemFileReadStore
//
// It will re-query the server whenever necessary.
//
define([
  "dojo/_base/declare", 
  "dijit/form/FilteringSelect"
], 
function(declare, _FilteringSelect) {
  return declare(
    [_FilteringSelect], {

      // minimum number of input characters to trigger search
      minKeyCount: 2,

      // the term for which we have queried the server for the last time
      lastServerQueryTerm: null,

      // The query URL which will be set on the store when a server query
      // is needed
      queryURL: null,
      //------------------------------------------------------------------------
      postCreate: function() {
        this.inherited(arguments);
        // Setting defaults
        if (this.searchDelay == null)
          this.searchDelay = 500;
        if (this.searchAttr == null)
          this.searchAttr = "label";
        if (this.autoComplete == null)
          this.autoComplete = true;
        if (this.minKeyCount == null)
          this.minKeyCount = 2;
      },    
      escapeRegExp: function (str) {
        return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
      },
      replaceAll: function (find, replace, str) {
        return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
      },
      startsWith: function (longStr, shortStr) {
        return (longStr.match("^" + shortStr) == shortStr)
      },
      // override search method, count the input length
      _startSearch: function (/*String*/ key) {

        // If there is not enough text entered, we won't start querying
        if (!key || key.length < this.minKeyCount) {
          this.closeDropDown();
          return;
        }

        // Deciding if the server needs to be queried
        var serverQueryNeeded = false;

        if (this.lastServerQueryTerm == null)
          serverQueryNeeded = true;
        else if (!this.startsWith(key, this.lastServerQueryTerm)) {
          // the key does not start with the server queryterm
          serverQueryNeeded = true;
        }

        if (serverQueryNeeded) {
          // Creating a query url templated with the autocomplete term
          var url = this.replaceAll('${autoCompleteTerm}', key, this.queryURL);
          this.store.url = url
          // We need to close the store in order to allow the FilteringSelect 
          // to re-open it with the new query term
          this.store.close();
          this.lastServerQueryTerm = key;
        }

        // Calling the super start search
        this.inherited(arguments);
      }
    }
  );
});

Notes:

  • I included some string functions to make it standalone, these should go to their proper places in your JS library.

The JavaScript embedded into the page which uses teh AutoComplete widget:

  require([
    "dojo/ready", 
    "dojo/data/ItemFileReadStore", 
    "gefc/dijit/AutoComplete", 
    "dojo/parser"
  ], 
  function(ready, ItemFileReadStore, AutoComplete) {

    ready(function() {

      // The initially displayed data (current value, possibly null)
      // This makes it possible that the widget does not fire a query against
      // the server immediately after initialization for getting a label for
      // its current value
      var dt = null;
      <g:if test="${tenantInstance.technicalContact != null}">
        dt = {identifier:"id", items:[
          {id: "${tenantInstance.technicalContact.id}",
           label:"${tenantInstance.technicalContact.name}"
          }
        ]};
      </g:if>

      // If there is no current value, this will have no data
      var partnerStore = new ItemFileReadStore(
        { data: dt, 
          urlPreventCache: true, 
          clearOnClose: true
        }
      );

      var partnerSelect = new AutoComplete({
        id: "technicalContactAC",
        name: "technicalContact.id",
        value: "${tenantInstance?.technicalContact?.id}",
        displayValue: "${tenantInstance?.technicalContact?.name}",
        queryURL: '<g:createLink controller="partner" 
          action="listForAutoComplete" 
          absolute="true"/>?term=\$\{autoCompleteTerm\}',
        store: partnerStore,
        searchAttr: "label",
        autoComplete: true
      }, 
      "technicalContactAC"
      );

    })
  })

Notes:

  • This is not standalone JavaScript, but generated with Grails on the server side, thus you see <g:if... and other server-side markup in the code). Replace those sections with your own markup.
  • <g:createLink will result in something like this after server-side page generation: /Limes/partner/listForAutoComplete?term=${autoCompleteTerm}

As of dojo 1.9, I would start by recommending that you replace your ItemFileReadStore by a store from the dojo/store package.

Then, I think dijit/form/FilteringSelect already has the features you need.

Given your requirement to avoid a server round-trip at the initial page startup, I would setup 2 different stores :

Then, to avoid querying the server at each keystroke, set the FilteringSelect's intermediateChanges property to false, and implement your logic in the onChange extension point.

For the requirement of triggering the server call after a delay, implement that in the onChange as well. In the following example I did a simple setTimeout, but you should consider writing a better debounce method. See this blog post and the utility functions of dgrid.

I would do this in your GSP page :

require(["dojo/store/Memory", "dojo/store/JsonRest", "dijit/form/FilteringSelect", "dojo/_base/lang"],
function(Memory, JsonRest, FilteringSelect, lang) {
    var initialPartnerStore = undefined;

    <g:if test="${tenantInstance.technicalContact != null}">
        dt = {identifier:"id", items:[
          {id: "${tenantInstance.technicalContact.id}",
           label:"${tenantInstance.technicalContact.name}"
          }
        ]};
        initialPartnerStore = new Memory({
            data : dt
        });
    </g:if>

    var partnerStore = new JsonRest({
        target : '<g:createLink controller="partner" action="listForAutoComplete" absolute="true"/>',
    });

    var queryDelay = 500;

    var select = new FilteringSelect({
        id: "technicalContactAC",
        name: "technicalContact.id",
        value: "${tenantInstance?.technicalContact?.id}",
        displayValue: "${tenantInstance?.technicalContact?.name}",
        store: initialPartnerStore ? initialPartnerStore : partnerStore,
        query : { term : ${autoCompleteTerm} },
        searchAttr: "label",
        autoComplete: true,
        intermediateChanges : false,
        onChange : function(newValue) {
            // Change to the JsonRest store to query the server
            if (this.store !== partnerStore) {
                this.set("store", partnerStore);
            }

            // Only query after your desired delay
            setTimeout(lang.hitch(this, function(){
                this.set('query', { term : newValue }
            }), queryDelay);

        }
    }).startup();

});      

This code is untested, but you get the idea...

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top