DevExtreme Data Grid - An Advanced Custom Popup Editor

09 December 2016

I recently tried to implement a real-world scenario with our DevExtreme Data Grid, which involved creating a complex custom popup editor. The grid easily accommodates extensions like this, but there were several decisions to be made and details to be figured out, and I decided to document the process. The implementation uses our direct JavaScript and jQuery APIs, not the support for Knockout or Angular. I'm structuring this walk-through in several steps, and at the end of each step there is a live example you can try out for yourself.

Here is an overview of the series of posts that will appear over the next few days. I will update the links as new posts are released.


Introduction and step 2: Creating a popup editor (this article)

Step 3: Custom search functionality

Step 4: Preview and quick-add

Step 5: Refactoring

Step 6: Considerations for Mobile platforms



Introduction

My scenario is this:

The main grid level shows bookings, like you would expect to see in a ledger.

The bookings have a reference to a supplier. A list of suppliers is available separately.

To assign a supplier to a booking, the user should have a lookup editor available. While a simple lookup is easy to implement using out-of-the-box features, I want several additions:

A popup should be shown that allows the user to search for suppliers

I want to prioritize the results of the search. Suppliers have a number, and if that number is entered as a search criterion, any full-match results should show at the top of the result list. Assuming that the supplier number will be a frequently used search criterion, I want to show partial matches on the number underneath the full matches. Additional results may be found by considering partial matches in the supplier's "name" and "details" fields. The implementation of the complete search logic varies with the underlying data layer, in other words the most efficient way of achieving what I want may be different when assuming local arrays vs a remote data service. In any case I'm assuming I'll have to implement the search logic myself instead of relying on a standard control.

For data entry purposes, I want very efficient keyboard handling. Optimally, the user should tab into the supplier field in the grid, enter a supplier number and immediately hit Return to confirm the top search result.

For new suppliers, I want a "quick-add" feature right there in the popup: creating a new supplier and selecting it for the booking should be quick and easy.


For the purposes of this description, I'm keeping the data structures as small as possible, and I'm not using any remote data connections.

As a starting point, I have created a basic grid setup that shows the booking data. I have configured the columns to some extent, and for the supplier column I have included a lookup configuration that works with the separate supplier list to show the supplier name. Feel free to play with this basic setup:



Step 2: Creating the popup

To improve the appearance of the supplier in the main grid, I change the definition of the lookup displayExpr to a lambda expression that renders the supplier number combined with the viewName:

displayExpr: s => s ? `${s.number} - ${s.viewName}` : "Not assigned"

Now it gets really interesting. I set up a function for the editCellTemplate property of the column. This function receives arguments for the cellElement and the cell configuration options (cellInfo), and it is expected to modify and extend the cellElement to accommodate the custom elements.

First, I add a basic editor to the cell. This is important because the cell uses this to show its content while editing is in progress. I can also use the editor to position the popup on screen relative to the cell location. Using the cellInfo, I can retrieve the text the cell is showing initially and set up the editor accordingly.

const $editor = $("<div>").appendTo(cellElement).dxTextBox({
  readOnly: true,
  value: cellInfo.text
});

As a second element, I add a dxPopover widget to the cell. I'm assigning a CSS class to the div, which allows the grid to recognize the popup as an extension of the in-place editor. Without this, the grid would end the editing operation as soon as the focus leaves the grid itself.


const popup = $('<div class="dx-dropdowneditor-overlay">').
    appendTo(cellElement).dxPopover({
  ...


Note that it is also possible to create a dxPopover on the basis of an existing div defined in HTML. However, I'm choosing to define my entire widget in JavaScript, which gives me the benefits of encapsulation - with a little refactoring, my complete custom editor would be usable in a different grid or on a different page, without requiring specific HTML to be included.

The contentTemplate property of the dxPopover defines what the popup will show. I'm assigning a function that can receive a container as an argument and you can extend this container in-place, or alternatively creating a template element and return it from the function. I prefer the latter approach because in my mind it fits in better with the idea of a "template" function. In this case I don't need the container argument, so I ignore it.

I set up a dxList to show the suppliers. The itemTemplate function on the list works similarly to the contentTemplate I just described, but in this case the itemElement from the argument list can be used to contain the text shown for each item in the list. Of course you can extend the elements as needed, but in this case that's not required.


contentTemplate: () => {
  const $list = $("<div>").dxList({
    ...
    itemTemplate: (itemData, itemIndex, itemElement) => {
      itemElement.text(`${itemData.number} - ${itemData.viewName}`);
    }
  });

  return $list;
}


I define an event handler for onSelectionChanged on the list. Since my list is defined to use single-item selection, I know that the only element included in the addedItems property of the event arguments is the selected item. I pass the display representation for that item to the cell editor, for display while the editing operation is still ongoing, and I use the setValue helper on the cellInfo to notify the grid of the change.


onSelectionChanged: ({ addedItems }) => {
  if (addedItems && addedItems.length == 1) {
    $editor.dxTextBox("instance").option("value",
    cellInfo.column.lookup.displayExpr(addedItems[0]));
    cellInfo.setValue(addedItems[0]._id);
  }
}


I add another event handler, this time for onHidden. A simple line of code notifies the grid that the editing operation should be concluded since the popup has been closed.


onHidden: () => grid.closeEditCell()


Finally, I need to show the popup on screen. Its position is set up relative to the cell editor, and I use setTimeout to call the show() method because otherwise it is possible that the popup hasn't completely initialized at this point and the display position is incorrect.


popup.option("position.of", $editor);
setTimeout(() => {
  popup.show();
});


You can find this complete step in the following code sample:

no comments
No Comments

Please login or register to post comments.