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

Oliver's Blog
15 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 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!

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.
No Comments

Please login or register to post comments.