Oliver's Blog

April 2017 - Posts

  • DevExtreme - Real World Patterns - Angular and Knockout front-ends

    This post is part of a series describing a demo project that employs various real-world patterns and tools to provide access to data in a MongoDB database for the DevExtreme grid widgets. You can find the introduction and overview to the post series by following this link.

    In the last post on the DevExtreme widgets I promised I would show alternative UI front-end implementations using our Knockout and Angular adapters. I have now added two branches to the demo repository for these two samples.

    This is the Knockout branch

    This is the Angular branch

    The Knockout front-end

    For Knockout, I have kept the existing structure of the webapp project. Knockout is an MVVM-style library that focuses mainly on data binding within single pages. There are modules for Knockout that implement navigation and external templating features, but I found that some of these projects have been deprecated now. It has always been my personal opinion that Knockout is best viewed as an unopinionated system for ad-hoc in-page use rather than a full-blown SPA framework, so I kept things simple by going with the old project structure.

    The difference between the Knockout front-end and the basic jQuery one is the widget instantiation mechanism. With jQuery, I had divs with ids in the HTML and I used jQuery instructions to instantiate DevExtreme widgets for these divs. With Knockout, I use data-bind attributes instead (from index.html):

    <div data-bind="dxToolbar: toolbarOptions"></div>
    

    The binding refers to toolbarOptions (and in other pages there are similar gridOptions), which I chose to make part of my model. In index.js you can find the model definition and the toolbarOptions as part of it. You will notice that the data structure used for the options is the same as in the jQuery project.

    The call ko.applyBindings(model) at the end of the file tells Knockout that it needs to evaluate all declared bindings using the model I pass in.

    There’s only one other trick I used in this setup. In dataGrid.js you can see an empty grid element declared as part of the model. This is used to store a reference to the grid in the onInitialized event handler, so that I can later refer to the grid from the Reload grid toolbar button’s onClick handler.

    If you are interested in some further information about the use of Knockout with DevExtreme, I recommend this documentation page as a starting point.

    The Angular front-end

    Note that this front-end targets Angular, not AngularJS.

    With Angular, everything is different. I wanted to create a project that would have a standard structure, so I created one using the Fountain Webapp Generator. I had to update lots of modules to the latest versions, but the general structure of the template application is still recognizable.

    Fountain uses Gulp as its build and automation tool and I didn’t change this. Of course it’s a contrast to the other Makefile-based projects, and I found myself thinking, again, what a cumbersome tool Gulp really is. There’s certainly a lot it can do with all its additional modules, but the overall setup ends up spread out across loads of different files, and standard “recipes” are hard to recognize because it’s all in code. Plus, everything the Gulp modules do can be achieved at least as easily using a few simple command lines. I appreciate that it’s an integrated environment that doesn’t require a separate tool (make!), but I can’t say I’m a fan…

    Back to Angular. I set up the project according to the instructions for the devextreme-angular package. Since I decided to go with webpack, I also followed the steps in the related readme for devextreme-angular. I imported the required modules and added them to @NgModule:

    import { DxButtonModule,
             DxToolbarModule,
             DxPivotGridModule,
             DxDataGridModule } from "devextreme-angular";
    
    @NgModule({
        imports: [
        BrowserModule,
        routing,
        DxButtonModule,
        DxToolbarModule,
        DxPivotGridModule,
        DxDataGridModule
        ],
    
        ...
    

    My individual views (component templates) can now instantiate the DevExtreme widgets using Angular syntax (from menu.html):

    <dx-toolbar>
      <dxi-item location="before" locateInMenu="auto">
        <dx-button type="danger" text="Create 1000 Test Objects"
               (onClick)="createTestObjects()"
               [disabled]="testObjectsDisabled"></dx-button>
      </dxi-item>
      <dxi-item location="before" locateInMenu="auto">
        <dx-button type="success" text="Show Data Grid" [routerLink]="['/dataGrid']"></dx-button>
      </dxi-item>
      <dxi-item location="before" locateInMenu="auto">
        <dx-button type="success" text="Show Pivot Grid" [routerLink]="['/pivotGrid']"></dx-button>
      </dxi-item>
    </dx-toolbar>
    

    You can see how our widgets use Angular binding syntax to bind events and properties, and even the routerLink binding works correctly with the button to navigate between the two views.

    To interface with the custom datastore for the data grid and pivot grid widgets, I made a two small changes to the dataStore.js. First, I imported the two elements $ and CustomStore explicitly:

    import $ from "jquery";
    import CustomStore from "devextreme/data/custom_store";
    

    The jQuery reference is needed because my implementation uses it for AJAX calls. Of course there would be another way of doing this with Angular, but I decided to leave it in place for consistency, and also because the DevExtreme widgets still use jQuery under the covers anyway.

    At the end of the file, I export the dataStore so it can be pulled in from other files:

    export default dataStore;
    

    With this in hand, I created two components to represent the data grid and pivot grid views. Here is pivotGrid.ts as an example:

    import { Component, ViewChild } from "@angular/core";
    
    import { DxPivotGridComponent } from "devextreme-angular";
    
    import dataStore from "./dataStore.js";
    
    @Component({
      template: require("./pivotGrid.html")
    })
    export class PivotGridComponent {
      @ViewChild(DxPivotGridComponent) grid: DxPivotGridComponent;
    
      dataStore = dataStore;
    
      refresh() {
        this.grid.instance.getDataSource().reload();
      }
    }
    

    I’m taking advantage of the ViewChild decorator to have access to the grid component created in the view. The template is defined in pivotGrid.html and it binds to the dataStore defined in the class:

    ...
    
    <dx-pivot-grid>
      <dxo-field-panel visible="true"></dxo-field-panel>
    
      <dxo-data-source [store]="dataStore"
         remoteOperations="true" retrieveFields="false">
        <dxi-field dataField="date1" dataType="date" format="shortDate"
               allowFiltering="false" allowSorting="true"
               allowSortingBySummary="true" area="filter"></dxi-field>
    
    ...
    

    The data grid view and component are implemented along the same lines and there were no surprises - everything worked just like it should!

    In addition to the devextreme-angular readme linked above, I recommend also reading the documentation on DevExtreme/Angular integration.

  • DevExtreme - Real World Patterns - DevExtreme Widgets

    This post is part of a series describing a demo project that employs various real-world patterns and tools to provide access to data in a MongoDB database for the DevExtreme grid widgets. You can find the introduction and overview to the post series by following this link.

    The front-end web application is the last “big” component of the demo application, and it is technically quite simple. The server-side index.js launches an Express web server to make a few static files available:

    const express = require("express");
    
    var app = express();
    
    app.use(require("morgan")("dev"));
    app.use(express.static("static"));
    
    app.listen(8080, function() {
        console.log("Web server running on port 8080");
    });
    

    The static files themselves contain what little application logic there is in this simple demo, and they configure the data grid and the pivot grid to access data through a custom data store implementation.

    The JavaScript API I’m using is jQuery, to keep things simple and universal. As you may know, DevExtreme also supports bindings to Angular and Knockout. Using either of these bindings doesn’t change the way the widgets work on a low level, it mainly changes the syntax of your code to integrate the widgets. I plan to show alternative UI implementations in the future.

    The implementation of the custom store adheres to the description in the DevExtreme documentation. The modification functions are very simple, since they trigger a single AJAX call. Here’s the update function as an example:

    update: function(key, value) {
      return $.ajax({
        url: BASEDATA + "/" + encodeURIComponent(key),
        method: "PUT",
        data: JSON.stringify(value),
        contentType: "application/json"
      });
    }
    

    The load function is a bit more complicated, since it has to transfer the loadOptions to the server. I started out with an implementation from our knowledge base, but I made a few modifications as I implemented the various parts of the data service.

    Since I’m using the options in the same format on the server, it would be possible to simplify this code by just transferring the options directly, but I decided to leave this code explicit, making it more likely for future changes to the structure to be noticed early. At the end of the function, an AJAX call is made to the server to retrieve the data and other details, with the latter being passed back with the resolve function:

    $.getJSON(BASEDATA, params).done(function(res) {
      var details = {};
      if (options.requireTotalCount) details.totalCount = res.totalCount;
      if (options.requireGroupCount) details.groupCount = res.groupCount;
      if (options.totalSummary) details.summary = res.summary;
    
      d.resolve(res.data, details);
    });
    

    Setting up the widgets

    While the configuration of the widgets is mostly straight-forward, there are a few things that need to be done correctly. The data grid uses this block to set up its data connection to the custom data store (from dataGrid.js):

    dataSource: {
      store: dataStore
    }
    

    Technically there is an additional class in the data access chain between the widget and the server: the DataSource. As you can see in the documentation, it is possible to instantiate a DataSource given a data store, and by assigning the object with the store property to the dataSource property of the grid configuration, this is done automatically. In addition, to take advantage of the server-side functionality implemented in the demo, it is important to activate remoteOperations correctly:

    remoteOperations: {
      filtering: true,
      grouping: true,
      groupPaging: true,
      paging: true,
      sorting: true,
      summary: true
    }
    

    As you can see, there are several individual flags for remoteOperations. I don’t normally recommend using those individually. There might be edge case scenarios where it is useful to activate one remote operation and not the other, but it is certainly very easy to end up with performance-killing behavior by setting these options selectively – so be careful if you do!

    Note that you can also set remoteOperations: true, which looks like it will activate all remote operations at once. However, as the docs explain, this does not activate groupPaging together with the other options. The option groupPaging was only introduced in version 16.2 of DevExtreme and our developers were afraid of breaking behavior, so they kept it separate. I’m using the longer explicit syntax in the demo to activate all remote operation features.

    For the pivot grid, the setup is simpler (from pivotGrid.js):

    dataSource: {
      remoteOperations: true,
      store: dataStore,
    
      // ... field configuration
    }
    

    There are no separate remote operation flags for the pivot grid, setting remoteOperations to true takes care of everything at once.

    The data service public endpoint

    One part of the demo project that hasn’t been mentioned yet is the data service that the custom store implementation talks to. This is implemented in the web-proxy service.

    The service start-up is the most complicated of all the services, because it uses an Express web server as well as an elaborate Seneca configuration to interface with other services. The web server activates CORS support (without any restrictions) to enable browsers from any source to access the public service.

    The purpose of web-proxy is to provide a REST-style interface to the other services. There are several Seneca modules that make this easier, but I decided implement the service manually for this demo since it allows me to carefully parse and check parameters passed to this public interface, before they are passed on to other services.

    The routing for the REST service is configured in routes.js. Routing utilizes functionality from the seneca-web module together with the seneca-web-adapter-express adapter, to translate incoming URL requests into Seneca messages. These messages are received by the service implemented in proxy.js, which works through three steps for each message:

    1. Validate the message parameters, i.e. the values that were sent with the REST service request.
    2. Send a message onwards to the responsible back-end service and receive the response.
    3. Return that response to the caller, adding details like the correct HTTP status code depending on the outcome of the operation, and additional HTTP headers required by the REST conventions.

    For the message list, which implements the complex parametrized query functionality, this process is the most complicated because almost all incoming parameters are checked before the options are passed on to the query-service. The basic philosophy I have implemented (here and in the query-service) is to ignore elements that are misformed or otherwise not understood, so that the service usually returns data even if parameters are invalid. Log instructions that output to the service console help debugging issues with invalid parameters during development.

    Since the web-proxy service simply performs some validated routing, there isn’t much more to say about what it does. Please feel free to ask if you have any questions!

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