Oliver's Blog

December 2016 - Posts

  • DevExtreme Data Grid - An Advanced Custom Popup Editor - Considerations For Mobile Platforms

    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 6: Considerations for Mobile platforms

    For the final part of this blog post series, I'll apply some optimizations to my sample solution to improve its behavior on mobile devices. Other environments with restricted screen sizes benefit as well, since low resolutions are actually the most important issue up to this point, while usability of individual widgets is covered automatically and out of the box.


    Viewport configuration

    If you don't do a lot of HTML-based development for mobile devices, you might be surprised about the standard handling of viewport sizes in those environments. (You may be surprised by the terminology even before that: the "viewport" is the visual area that an HTML page is shown in, which may be a browser window or frame, or a device display.) For historical reasons, mobile device browsers use seemingly arbitrary sizes for their viewports, that do not correspond to the actual physical resolution available on the devices. The original reason for this was that web sites were not optimized in any way for mobile devices, so the browsers were written not to "admit" that they were in fact running on devices with severly limited screen resolutions.

    For a well-behaved modern responsive application, the viewport size should normally be set to the actual size of the device screen. This is done by adding a meta tag to the web page. For my sample, I'm using this meta tag:


    <meta name="viewport"
      content="width=device-width,height=device-height,initial-scale=1.0,maximum-scale=1.0"/>


    The width and height elements of the tag configure the viewport size as I described above. I also use the (generally recommended) initial-scale, which is set to 1.0 to make sure the device initially zooms to 100%.

    My application of maximum-scale is a bit more contentious and I wouldn't generally recommend it — it's in the example for illustration purposes. The parameter is meant to limit how far a user can zoom in to the page, and by setting it to 1.0, the user is prevented from zooming in at all. For reasons of accessibility, this doesn't seem like a really good idea, and that's why most web designers recommend against it.

    However, some devices (notably the iPhone) have functionality that automatically applies a zoom every time the user edits an input field. This behavior is certainly very useful when editing text on a small mobile device, in a web page that is not itself responsive — in that case, the page content is small and hard to read, but when you edit something, the browser on the device zooms in automatically and makes things easier for you. At the same time, this behavior is very annoying when looking at a page (or app) that is already at the correct size because it implements its own responsive behavior. In that case, the automatic zoom to the editing field makes the UI "jump" and the user loses their focus and orientation in the page flow, rendering the page less usable instead of more.

    Optimally, there would be a way to say "I want users to be able to zoom manually, but please don't zoom automatically." There is currently no solution to this issue, but the maximum-scale parameter provides one approach (with the disadvantages I discussed). Apple doesn't like this idea at all, which is why Safari on iPhone actually ignores maximum-scale. It does work in Chrome on the iPhone, so feel free to play with it and see for yourself whether you like the approach.

    With the viewport configuration in place, my sample renders the grid and the editors in the popup in a much better readable size than before.


    Fixed list height

    I need to fix an issue that is not technically restricted to low resolutions: the height of the popup list box. So far, the list doesn't have any configuration pertaining to its size, and its default behavior is to accommodate its content and resize itself accordingly. If you've played with the samples from the previous posts, you may have noticed that when you added supplier items, the list grew vertically, and some of the detail form elements were hidden behind the bottom toolbar of the popup window. The popup window itself had a fixed size, so the combination was unfortunate.

    To improve this behavior, I apply two changes. The list itself gets a fixed size:


    height: "10ex"


    For the popup (the dxPopover instance) I assign only the width now:


    width: "30em"


    Note that I'm using em and ex units for the sizes, since these depend on the font size. This is not a perfect solution for all cases, but it means that if the user sets a larger font size on their device or machine, the UI elements grow accordingly.

    As a result of these changes, the list now has a fixed height (which should be sufficient to display two list elements at a time, and scrolling is active by default to show further list content), and the popup doesn't have a height assigned, which means it will resize vertically as needed. The resizing behavior is important because the embedded detail form may change its height if the built-in responsive mechanisms restructure the columns and grow the form vertically.


    Adaptive Data Grid behavior

    The grid shows six columns in my applications, and depending on the screen size it might not be possible to see useful content in all of them. Content gets cut off and extended with an ellipsis (...) when the column width is not sufficiently large. Fortunately, the grid has a simple and very usable built-in mechanism to hide columns if they don't fit the viewport width. I apply this by adding a simple option to the grid configuration:


    columnHidingEnabled: true


    The column hiding behavior can work all by itself, but it usually makes sense to also configure the order in which columns will be hidden. I do this by setting the property hidingPriority on each grid column. Note that the name of this property is a bit misleading: the column with the highest hidingPriority does not have the "highest priority for hiding", but rather the lowest — in other words, the column with the highest hidingPriority will remain visible longest if the grid is narrowed in width.

    My recommendation for setting up hiding priorities is to start with your most important column that you want visible at all times and give it a high value for its priority. Then you should ask yourself, step by step, which column you would like to see appearing next, and assign it a slightly lower priority. For my purposes I worked out this order:

    • Amount: 11
    • Date: 10
    • Currency: 9
    • Supplier: 8
    • Number: 7
    • Details: 6


    Handling the popup form

    The popup form is the main focus of this article series, it's the most complex piece of UI in my sample, and it's also the most difficult to handle when it comes to responsive behavior.

    First, let me mention that all our widgets have the ability to apply rule-based options using their defaultOption helper methods. Especially if you are writing apps you intend to run on mobile devices, this mechanism can be very useful because it allows you to change the default behavior of widgets if they are, for instance, running on a tablet vs a desktop machine, or an iOS vs an Android device. Note that the mechanism in our public API currently applies these options based on widget classes, i.e. not for individual instances. There is an internal API that does the same thing per instance and we are considering publishing this functionality. Please let us know if you have a strong opinion either way!

    For the purposes of my demo scenario, I don't use these rule-based options because I want my UI to depend on viewport size only. I add an event handler to the dxPopover configuration:


    onShowing: ({component}) => {
      sizeAndPosition(component, $editor);
    }


    The "showing" event is triggered at a point in time when the elements of the popup window have been created, which is important because otherwise the DOM won't give me the sizes of elements. I'm implementing my algorithm to take the actual rendered size of the popup window into account. On the other hand, during the "showing" event, the popup window is not yet visible on screen, which is equally important — I'm going to make changes to the position and size of the popup window, and if it were visible already at this stage, the user would observe the window jumping around on screen during the process.

    In the sizeAndPosition helper function, I determine the viewport size as well as that of the popup:


    const viewportWidth = $(window).width();
    const viewportHeight = $(window).height();
    
    const $sizeElement = popup._$wrapper.children().first();
    const popupWidth = $sizeElement.width();
    const popupHeight = $sizeElement.height();


    Getting hold of the popup size is a bit tricky. The reference popup points to the placeholder element for the popup, but this is different from the actual popup window container that is rendered to the DOM when the popup is shown. This is what I'm getting from the private variable _$wrapper. Due to the styling structure, this node can't give me valid width and height values, so I access its first child in order to retrieve the correct values.

    As an alternative to the code accessing the internal _$wrapper, I can also search the structure for one of the container elements using its CSS class. Both of these implementations work, but they both make assumptions about internal implementation details, so I hope we will provide better access methods for use cases like this in the future.


    const $container = popup.content().closest('.dx-overlay-content');
    const popupWidth =  $container.outerWidth();
    const popupHeight = $container.outerHeight();


    With these values in hand, I can now figure out whether the popup is subjectively too large for the viewport size:


    if (popupHeight * 1.3 > viewportHeight || popupWidth * 1.3 > viewportWidth) {
    ...


    Of course you can play with the calculation factors in this expression, or you could even decide to use static values instead of the dynamic size calculations. The example is meant to provide a starting point for similar mechanisms — for instance, you could implement dynamic sizing for the embedded list as well.

    If I find the viewport to be too small, I resize the popup and display it centered:


    popup.option("width", viewportWidth - 20 + "px");
    popup.option("height", viewportHeight - 20 + "px");
    popup.option("position", {
      my: "top",
      at: "top",
      of: window
    });


    Using a slightly ugly mechanism (hopefully an option for this purpose will be supported in the future), I also remove the "arrow" at the top of the popup that normally points towards the associated editor.


    popup.on("shown", ({component}) => 
      component._$wrapper.find(".dx-popover-arrow").first().hide());


    Note that there is a choice of popup widgets in the DevExtreme library. The dxPopover, which I'm using, can show an arrow pointing to a target element, but that arrow cannot be switched off using built-in methods, and the dxPopover doesn't support an automatic mechanism to display itself in full-screen mode. The dxPopup supports full-screen, but it can't display the target arrow. An alternative to my implementation logic would be to make the decision about viewport size before ever initializing the popup, and then instantiating either a dxPopover or a dxPopup based on that decision. Of course you would lose the ability to calculate accurate display sizes when using this approach, because those are only available after the required elements have been created.

    Finally, since I know the popup will probably have either a narrower width or a lower height than intended, I enable scrolling of the top-level form content. I switch on scrolling for the form — in case it's not needed because the form is large enough, this will not have any effect.


    scrollingEnabled: true


    In my sizeAndPosition helper, I add a few lines to get hold of the embedded form and explicitly set its height to the available space in the content area of the popup. Note that without this height setting, the form will not allow scrolling!


    const $form = popup.content().children().first();
    const form = $form.dxForm("instance");
    form.option("height", popup.content().height() + "px");


    As you can see in the complete example, the helper function finishes with an else branch that initializes the popup with the same settings I was using before, in the case that there is enough space in the viewport to display it near the grid editor.

    This concludes my steps for advanced mobile support. I tried this implementation on Android phones and iPhones as well as in various desktop browsers and it works very nicely. Here is the final version of my code:

    The end of this blog series has also arrived at this point. I hope this practical use example was interesting and I explained my approaches sufficiently. If you have any questions or comments, please don't hesitate!

  • DevExtreme Data Grid - An Advanced Custom Popup Editor - Refactoring

    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 5: Refactoring

    For the sample implementations up to this point, I showed most of the configuration logic in the nested control hierarchy that defines the grid with all its sub-elements. Due to the deep nesting, this structure is not the easiest to maintain and I decide to refactor it. Much of the functionality implemented by the nested elements is tied together quite closely, and the current implementation takes advantage of the availability of certain variables throughout the scope, to keep things concise. In a refactored structure, this isn't possible to the same extent - but on the other hand, such a structure shows more clearly what dependencies each individual element has, because dependencies need to be passed in as parameters in many cases. (Of course it would also be technically possible to keep everything in global variables, but I'm not even going to go there - I hope that's not something you would seriously consider!)

    I have created a refactored version of my sample, where I extracted the logic into individual functions, passing parameters through as needed. This is only one structural option, there are certainly others! I hope it helps you come up with some techniques if you have large control structures to maintain. Here is the refactored code, below it you can find a number of notes on individual decisions I made while applying structural changes.

    Data layer structure

    At the top of the code file, the first item I come across is my data layer implementation. So far, this consisted of several types, functions and of course the demo data definition. Even though the data layer implementation would probably look a bit different (and it likely wouldn't live in the same code file as the UI code) in a real application, I decided to change its structure anyway to highlight some approaches and to help with the rest of the refactoring work.

    In the refactored code, I create a dataLayer object to encapsulate the data, the types and the helper functions. I use an IIFE to create this object, and all references from the original code that referred to elements that now become part of the new object need to be changed. The purpose of this step is mainly to make the rest of my refactoring work easier: by removing the data layer variables and types from the global scope, I will receive errors when my UI code tries to access these elements directly anywhere.

    Functional structures

    For each distinct element of my setup, I introduce a separate createXXX function. I write these functions to work only with their input parameters (with few exceptions — see below), so that the parameter lists serve as documentation of the dependencies for each function. All the major event handlers also have their own functions that receive a number of parameters, so I can reuse them with varying wrappers in several places.

    Assigning event handlers

    The structure of my popup window with all its nested elements is rather complex, and there are several event handlers (for example gconfirmAssignment) that access several controls contained in the hierarchy to do their job. In my old implementation, I was assigning event handlers at various points in the setup hierarchy — sometimes there were specific reasons to assign an event handler in a certain place, sometimes it might just have been convenient at the time. With a clean structure where element creation is encapsulated, I can't assign event handlers in arbitrary places because some context information may not be available at the time. For example, I had a line of code in the old version that assigned the confirmAssignment handler to the list dblclick event, right after the list was created. This is no longer possible now because confirmAssignment requires lots of context information that is not, or not yet, available when the list is created in its own createPopupSupplierList function.

    I decided to solve this problem by assigning all relevant events in one central place, at a point in time when the complete control hierarchy has been created. This happens in the onContentReady handler on the dxPopover. I chose to leave the code in that place instead of extracting it again, since I judged the showPopup function not to be overly complex. Extracting the event handler assignment code is easy, in case I change my mind about this later.

    Exceptions to the functional rules

    To assign the event handlers, I need access to various elements of the hierarchy. Unfortunately, I found a few cases where these elements are not easily accessible as children of the popup. The top-level form is created by the logic assigned to contentTemplate, but in order to access it later, I need to use the call chain component.content().children().first(). This is not optimal because it assumes a certain structure.

    I used an alternative approach for the nested form and list elements, and this technique could also be applied to the top-level form. The nested form and list elements are created by item template functions on dxForm, and there doesn't seem to be an easy way of retrieving these generated elements from the dxForm later on. (Note that the dxForm has a getEditor helper, but that only works with auto-generated editors, not with items generated through a template.) So I decided to assign unique ids to the list and the nested form in their creation functions, and use these ids when I need to access the elements at a later point. I don't see this as a particularly elegant approach because it is technically access to a global variable, and because in more complex scenarios it might not be safe to assume that the ids I use are actually unique.

    Eliminating global state

    There is one item of global state in my old implementation: the creatingNew flag that is set to true when the user clicks the button in the popup to create a new supplier. My persistence logic in confirmAssignment depends on this, and it still does in my refactored code. The difference is that the flag is now being passed in as a parameter, and the state is technically kept in a closure created during the event handler assignment. The variable creatingNew exists only on this level now, and is accessed exclusively by the two functions created to wrap the event handlers createNewSupplier and confirmAssignment. This is a functional technique that allows, in this case, two disconnected pieces of code to share a piece of state information without publishing that information to be read or even changed from elsewhere in the code.

    Encapsulating parameters for createDataGrid

    I decided to encapsulate the set of parameters that refer to data layer elements, and the set that refers to event handlers, each in their own object for the signature of the createDataGrid function. This enables me to pass in my existing dataLayer as one parameter, which satisfies the contract expressed by the signature of createDataGrid. Some flexibility is gained this way, but I'll readily admit that the benefit in this code sample is not too impressive.

    Results

    Overall, my refactoring work has simplified the code structure considerably. There are around 30 lines more code than in the old sample, which seems very reasonable for this much cleaner implementation. Since I made a lot of changes, I'm not completely sure I covered all relevant decisions in this summary — let me know if you'd like me to elaborate on something!

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

    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:

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

    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:

  • DevExtreme Data Grid - An Advanced Custom Popup Editor

    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:

LIVE CHAT

Chat is one of the many ways you can contact members of the DevExpress Team.
We are available Monday-Friday between 7:30am and 4:30pm Pacific Time.

If you need additional product information, write to us at info@devexpress.com or call us at +1 (818) 844-3383

FOLLOW US

DevExpress engineers feature-complete Presentation Controls, IDE Productivity Tools, Business Application Frameworks, and Reporting Systems for Visual Studio, along with high-performance HTML JS Mobile Frameworks for developers targeting iOS, Android and Windows Phone. Whether using WPF, ASP.NET, WinForms, HTML5 or Windows 10, DevExpress tools help you build and deliver your best in the shortest time possible.

Copyright © 1998-2017 Developer Express Inc.
All trademarks or registered trademarks are property of their respective owners