DevExtreme Data Grid - An Advanced Custom Popup Editor - Preview And Quick-Add

Oliver's Blog
13 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 4: Preview and quick add

The last feature I want to add to my popup editor is quick-add functionality. The user should be able to create a new supplier directly in the popup and assign it the booking. I begin by adding a nested form to the top-level form. I'm choosing to use a nested form, because this will only contain the fields required for the supplier object, and I can easily data-bind that form - if I was going to insert individual fields into my top-level form, this would be harder because the fields would mix with those I already have. These are the items I add to the top-level form:


{ itemType: "empty" },
{
  name: "detailform",
  itemType: "simple",
  template: () => {
    const $detailForm = $("<div>").dxForm({
      labelLocation: "top",
      disabled: true,
      items: [ ... ]
    });
    detailForm = $detailForm.dxForm("instance");
    return $detailForm;
  }
}


The detail form is disabled by default, and as long as no new supplier is being created, I'll use it to show a preview of the selected supplier from the list. This is convenient because it shows information from all fields of the supplier object, as opposed to the subset shown in the list.

To show the selected supplier in the form, I add a new onSelectionChanged event handler to the list:


onSelectionChanged: ({addedItems}) => {
  if (addedItems.length > 0) {
    detailForm.option("formData", addedItems[0]);
  }
}


Now I add an element to the toolbarItems on the dxPopover to represent the New Supplier button:


{
  toolbar: "top",
  widget: "dxButton",
  location: "before",
  options: {
    text: "New Supplier (Alt-N)",
    accessKey: "n",
    onClick: createNewSupplier
  }
}


This button appears at the top and is configured to support an access key, so the user can activate the functionality from the keyboard. I add the createNewSupplier handler:


function createNewSupplier() {
  // "this" is the button that triggered the event - 
  // there is no very easy way of getting hold of the button otherwise
  this.option("disabled", true);

  supplierPopupList.option("disabled", true);
  form.getEditor("search").option("disabled", true);
  detailForm.option("disabled", false);
  detailForm.option("formData", { });
  detailForm.getEditor("number").focus();
  creatingNew = true;
}


As you can see, I disable all the UI elements that won't be used once the form is switched into its "creating new" state. The way I implement this, the switch is not reversible - for the quick-add feature I have in mind, this is acceptable, since the user can just hit Escape and bring up the popup a second time in case the process is started accidentally.

I initialize the detail form with an empty object. In reality I would expect some additional logic here, for example to automatically suggest a new supplier number.

Once the form is in "creating new" mode, I need to persist the new supplier when the OK button is clicked, and make sure the grid is aware of its existence. So far, my supplier lookup column uses the array of suppliers as its data source directly, but now I introduce a new custom store for this purpose and bind this to the lookup column instead of the array:


const supplierDataStore = new DevExpress.data.CustomStore({
  key: "_id",
  load: function() {
    return suppliers;
  },
  insert: function(v) {
    return $.Deferred(d => {
      suppliers.push(v);
      d.resolve(v._id);
    }).promise();
  }
});


Like before, this custom store can be seen as a placeholder implementation, which could use any other mechanism in a real-world application to persist the new supplier.

I modify the confirmAssignment function to handle the additional case:


if (creatingNew) {
  const vr = detailForm.validate();
  if (vr.isValid) {
    const newSupplier = detailForm.option("formData");

    // useless way of generating a random id
    newSupplier._id = parseInt(Math.random() * 1000000);

    supplierDataStore.insert(newSupplier);
    $editor.dxTextBox("instance").option("value", 
cellInfo.column.lookup.displayExpr(newSupplier)); cellInfo.setValue(newSupplier._id); popup.hide(); } } else { // ... old logic here }


Note that I'm using a very unsafe way of assigning a random id to the new supplier for this demo. Even for an array store, you should use a different algorithm in reality to find the next valid id. If you use any kind of real persistence layer, the id generation should be handled there.


One final tricky bit is missing at this point. By default, the grid creates an internal cache of lookup data source key/display string pairs, for performance reasons. As a result, the insertion of the newly created supplier into the lookup data source is not enough, because the grid needs to be made aware of the change in order to refresh its cache. A refresh() call to the grid would solve this problem, but of course there may be considerable overhead involved in case the grid has several lookup columns. There is also a way of switching off the caching behavior I'm describing: configuring a calculateDisplayValue function on the lookup column. This tells the grid that the display values for lookup ids are retrieved separately, and it does not build its own cache.


Note that you shouldn't always fall back on calculateDisplayValue in cases like this. Depending on your deployment structure and your data layer (de)normalization level, it might be more or less expensive to retrieve the correct display string from your data layer.


For this demo, I'm using the expression I had previously assigned as the displayExpr of the lookup column to create two simple helper functions:


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

const bookingSupplierDisplayString =
    b => supplierDisplayString(supplierDataStore.supplierByKey(b.supplier_id));


I assign supplierDisplayString as the displayExpr and bookingSupplierDisplayString for calculateDisplayValue.

This completes the implementation of step 4. You can find the code for this step here:

no comments
No Comments

Please login or register to post comments.