Полнофункциональный виджет автозаполнения для Dojo

На данный момент (Dojo 1.9.2) мне не удалось найти виджет автозаполнения Dojo, который удовлетворял бы всем следующим (типичным) требованиям:

  • Выполняет запрос к серверу только после ввода предопределенного количества символов (без этого не следует запрашивать большие наборы данных)
  • Не требует ли полная служба REST на сервере, только URL-адрес, который может быть параметризован поисковым запросом и просто возвращает объекты JSON, содержащие идентификатор и метку для отображения ( поэтому запрос данных к базе данных может быть ограничен только необходимыми полями данных, без загрузки полных объектов данных и последующего использования только одного поля)
  • Имеет настраиваемую временную задержку между отпусканием клавиш и запуском серверного запроса (без этого чрезмерное количество запросов к серверу).
  • Способен распознавать, когда нет необходимости в новом запросе к серверу (поскольку ранее выполненный запрос является более общим, чем текущий).
  • Выпадающий стиль (имеет элементы графического интерфейса, указывающие, что это поле выбора)

Я создал проект решения (см. ниже), сообщите, если у вас есть более простое и лучшее решение для вышеуказанных требований с Dojo > 1.9.


person sola    schedule 02.03.2014    source источник


Ответы (2)


Виджет AutoComplete в виде модуля Dojo AMD (помещается в /gefc/dijit/AutoComplete.js в соответствии с правилами AMD):

//
// 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);
      }
    }
  );
});

Примечания:

  • Я включил несколько строковых функций, чтобы сделать его автономным, они должны занять свои места в вашей библиотеке JS.

JavaScript, встроенный в страницу, которая использует виджет автозаполнения:

  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"
      );

    })
  })

Примечания:

  • Это не отдельный JavaScript, а сгенерированный с помощью Grails на стороне сервера, поэтому в коде вы видите <g:if... и другую разметку на стороне сервера). Замените эти разделы своей собственной разметкой.
  • <g:createLink приведет к чему-то вроде этого после генерации страницы на стороне сервера: /Limes/partner/listForAutoComplete?term=${autoCompleteTerm}
person Community    schedule 02.03.2014

Что касается dojo 1.9, я бы начал с рекомендации заменить ItemFileReadStore хранилищем из пакета dojo/store.

Тогда я думаю, что в dijit/form/FilteringSelect уже есть нужные вам функции.

Учитывая ваше требование избежать обхода сервера при первом запуске страницы, я бы установил 2 разных хранилища:

  • dojo/store/Memory, который будет обрабатывать исходные данные.
  • dojo/store/JsonRest, который запрашивает ваш контроллер при последующих запросах .

Затем, чтобы не запрашивать сервер при каждом нажатии клавиши, задайте для свойства IntermediateChanges FilteringSelect значение false и реализуйте свою логику в точке расширения onChange.

Для требования запуска вызова сервера после задержки также реализуйте это в onChange. В следующем примере я сделал простой setTimeout, но вам следует подумать о написании лучшего метода устранения дребезга. См. этот сообщение в блоге и служебные функции dgrid.

Я бы сделал это на вашей странице GSP:

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();

});      

Этот код не проверен, но вы поняли...

person Philippe    schedule 03.03.2014