DevExtreme Data Grid - An Advanced Custom Popup Editor - Custom Search Functionality

12 December 2016

This post is part of a series describing the process of creating an advanced popup editor for the DevExtreme Data Grid. You can find the introduction to the post series, as well as an overview of all posts, by following this link.


Step 3: The custom search functionality

I decided to use a dxForm as a container element for the controls in the popup. There may not be many of them, but the dxForm has useful functionality for the purpose of defining a layout and I don't feel like fiddling with divs and CSS myself for this purpose. Instead of creating the dxList directly as a child of the popup content container, I wrap it in a dxForm and include the editor for the search string as well. Here's an abbreviated code snippet:


const $form = $("<div>").dxForm({
  items: [
    {
      name: "search",
      itemType: "simple",
      editorType: "dxTextBox",
      ...
    },
    {
      name: "list",
      itemType: "simple",
      template: () => {
        const $list = $("<div>").dxList({
          ...
        });
        return $list;
      }
    }
  ]
});


So far, the list has been bound directly to the suppliers array. On the basis of the search string entered by the user, I want to apply a filter to the list. For the demo implementation, this filter will simply work with the source array, but in reality I would expect to run one or more queries through my data layer and retrieve results. I decided to implement a custom data store, which means that for a real-world implementation changes would only need to be made there, leaving the rest of the logic intact.

Here's my custom data store. In the full sample, I also add a few other helpers (customFilter and getFilteredData, for instance), but you can see the basics of what the custom store does in this snippet:


const supplierPopupDataStore = new DevExpress.data.CustomStore({
  key: "_id",
  load: function() { 
    const that = this;
    return $.Deferred(d => {
      const result = that.customFilter ? 
        that.getFilteredData(that.customFilter) : suppliers;
      that._totalCount = result.length;
      d.resolve(result, { totalCount: that._totalCount });
    }).promise();
  },
  totalCount: function() {
    return this._totalCount ? this._totalCount : 0;
  }
});


To use the custom store with the list, I change its data binding to this:


dataSource: {
  store: supplierPopupDataStore
}


The dataSource properties on our various widgets support a number of syntax variations. In this case, the configuration object is automatically used to create a new DataSource that wraps the store I'm passing in.

To apply the filter when a search string is entered in the search text box, I add an event handler to it:


onValueChanged: ({value}) => {
  supplierPopupDataStore.customFilter = value;
  supplierPopupList.reload();
  supplierPopupList.selectItem(0);
}


The reload() call on the list widget has the effect of re-evaluating the load function on the custom data store. Once that has happened, I select the first item in the list (that's index zero). Due to the prioritization of my custom search, this should be the one the user is looking for - more often than not.

I apply some usability optimizations. First, I add an event handler to the dxPopover to focus the search field directly when the popup is shown. Since the editor is auto-generated by the dxForm, I need to retrieve it from the form. I also add logic to reset the custom filter in case the popup is being shown for a second time without leaving the editor.


onShown: () => {
  if (supplierPopupDataStore.customFilter) {
    supplierPopupDataStore.customFilter = "";
    supplierPopupList.reload();
  }
  form.getEditor("search").focus();
}


The list initially shows all suppliers when the popup shows. I add a line of code to select the item in the list that represents the currently assigned supplier. Since the selectItem method requires a list index, I created a helper on my custom store that finds the correct index given an id value.


supplierPopupList.selectItem(supplierPopupDataStore.indexByKey(cellInfo.value));


Note that in version 16.2 the dxList has some more advanced functionality that allows it to understand values which have keys. On that basis, the selection logic could be simplified a bit, but I decided to show code in this demo that is compatible with version 16.1.

I'm trying to optimize the entire implementation for quick keyboard use, so I want to make it easy for the user to select the correct item from the list in cases where there's more than one item. Unfortunately there are no existing API methods that move the selection up and down, so I write my own. The logic is straight-forward, you can find these functions in the full sample below. I use the registerKeyHandler method both on the search text editor and the dxList, to bind the up and down cursor keys to my handlers:


form.getEditor("search").registerKeyHandler("upArrow", upArrow);
form.getEditor("search").registerKeyHandler("downArrow", downArrow);
         
supplierPopupList.registerKeyHandler("upArrow", upArrow);
supplierPopupList.registerKeyHandler("downArrow", downArrow);


The remaining task for this step is to enable the user to select an item from the list. So far, the cell editor value changes each time the list selection changes. I remove the existing onSelectionChanged handler from the dxList and I introduce two new functions to handle a cell editor value change and a "cancel" operation:


function confirmAssignment() {
  const selectedItems = supplierPopupList.option("selectedItems");

  if (selectedItems.length == 1) {
    $editor.dxTextBox("instance").option("value",
    cellInfo.column.lookup.displayExpr(selectedItems[0]));
    cellInfo.setValue(selectedItems[0]._id);
    popup.hide();
  }
}

function escapeOut() {
  popup.hide();
}


For keyboard support, I bind these handlers to the the form and the dxList:


form.registerKeyHandler("enter", confirmAssignment);
supplierPopupList.registerKeyHandler("enter", confirmAssignment); 
form.registerKeyHandler("escape", escapeOut);
supplierPopupList.registerKeyHandler("escape", escapeOut);


Note that I want the Enter key handler to be widely available, so I bind it on the form level. This conveys intention more than anything else at this point, since the dxList is not covered by this approach - it is included in the form by means of a template, so it needs its own handler.

For mouse users, I also want to support a double click on a list item as an assignment. The dxList doesn't have its own event handler for double clicks, so I use the standard jQuery helper instead:


$list.dblclick(confirmAssignment);


Finally, I also consider users who work on touch-enabled devices (and I hope to cover unforeseen circumstances at the same time) by adding OK and Cancel buttons to the popup. In the dxPopover configuration, I add the following toolbarItems configuration. As you can see, it uses the same handlers one more time.


toolbarItems: [
  { 
    toolbar: "bottom",
    widget: "dxButton",
    location: "after",
    options: {
      text: "OK",
      type: "default",
      onClick: confirmAssignment
    }
  },
  { 
    toolbar: "bottom",
    widget: "dxButton",
    location: "after",
    options: {
      text: "Cancel",
      onClick: escapeOut
    }
  }
]


That concludes this step. Here is the complete example up to this point:

no comments
No Comments

Please login or register to post comments.