(2) Setting up data assignment

Overview

The second chapter describes all necessary steps to activate and use the MashZone NEXTGEN data assignment for a custom widget.
This chapter is recommended for everyone planning to create a custom widget that uses MashZone NEXTGEN data sources.
The basis for chapter two is a simple custom widget as explained and created in chapter one - Creating a simple custom widget.

Widget Preview

The custom widget “Demo Widget 2” is supposed to display the data of columns and rows of an assigned MashZone NG data source.
The result will look like that:

Resulting files

"Demo Widget 2" consists of the following folders and files.
As compared to the file structure of "Demo Widget 1" these folders and files were added.
As compared to the file names of "Demo Widget 1" the prefix "demoWidget1" has been changed to "demoWidget2". 

\---customWidgets
  \---demoWidget2
          |     demoWidget2Config.js
          |     demoWidget2Module.js
          | 
          +---assets
          |     +---css
          |     |        demoWidget2Styles.css
          |     | 
          |     \---images
          |              demoWidget2MenuIcon_32x32.png
          | 
          +---js
          |        demoWidget2AssignDataCtrl.js
          |        demoWidget2Ctrl.js
          |        demoWidget2DataService.js
          | 
          \---partials
               |   demoWidget2.html
               | 
               \---assignDataDialog
                        assignData.html

The created files and source code accompanying this article can be downloaded from here.


Creation steps

Step 1: Enable the "Assign Data" dialog

Edit demoWidget2Config.js and add the action "assignData" in the action properties as shown in the example below:

demoWidget2Config.js
angular.module('demoWidget2Module')
    .config(['dashboardProviderProvider',
        function (dashboardProviderProvider) {
            dashboardProviderProvider.widget('demoWidget2', {
                title: 'Demo Widget 2',
...
                actions: ['assignData',
                    'editName',
                    'hLine',
                    'copy', 'paste', 'cut', 'delete',
                    'toTop', 'bringForward', 'sendBackward', 'toBack',
                    'hideHeader','hideBorder'],
...
                }
            });
        }]);

Explanation: The added action "assignData" in line #7 will activate the "Assign data" icon in the widget properties. Clicking the icon will open the "Assign data" dialog.

Step 2: Create the "Assign data" dialog view

After activating the "assignData" action the custom widget framework will automatically enable the "Assign data" dialog and provide everything necessary to select a MashZone NEXTGEN data source and list all data columns available for assignment to the custom widget.
Since the usage of the data columns is depending on the custom widget implementation, an appropriate view needs to be created individually.

Therefor please create a sub folder assignDataDialog in the partials folder of the custom widget. Open a new file in the sub folder, save it as assignData.html and enter the following code:

assignData.html
<div class="assign-data">
    <div>
        <div class="inline-edit" ng-controller="demoWidget2AssignDataCtrl">
            Drop a data column here:
            <ul ff-single-column-drop
                item="itemClone"
                column="itemClone.config.demoWidgetCfgModel.anyDataColumn"
                input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
                column-dropped="columnDropHandler(column)"
                accept-filter="date,text,numeric">
            </ul>
        </div>
    </div>
</div>

Explanation: This file represents an individual view template for the "Assign data" dialog and is used to assign data columns to the custom widget.
In line #3 the "Assign data" dialog controller was added. The controller will be explained in the next step.
Starting in line #5 a drop target control "ff-single-column-drop" was added and configured to assign one column of any data type to the custom widget. 
 
After creating the view template, it must be registered in the custom widget configuration. 
Therefore please edit the file demoWidget2Config.js and define the assignData view template.

demoWidget2Config.js
angular.module('demoWidget2Module')
    .config(['dashboardProviderProvider',
        function (dashboardProviderProvider) {
            dashboardProviderProvider.widget('demoWidget2', {
                title: 'Demo Widget 2',
...
                assignData: 'widgets/customWidgets/demoWidget2/partials/assignDataDialog/assignData.html'
                }
            });
        }]);

Explanation: Line #7 defines the URL to the file "assignData.html" and stores it in the configuration property "assignData". It can now be loaded by the "Assign data" dialog view.

Step 3: Add the "Assign data" dialog controller

The controller used in line #3 of the assignData view template assignData.html needs to be created as well.
Therefore please create the file demoWidget2AssignDataCtrl.js in the js folder of the custom widget and add the following content:

demoWidget2AssignDataCtrl.js
angular.module('demoWidget2Module')
    /**
     * Controller for the Assign data dialog
     */
    .controller('demoWidget2AssignDataCtrl',['$scope',
        function($scope){
            /**
             * Is function is called when a column was dropped in the assign data view template
             * @param column
             */
            $scope.columnDropHandler = function(column) {
                column.newName = column.name;
            };
        }
    ]);

Explanation: The "Assign data" dialog controller is providing the application logic to manage the assigned columns of a custom widget.

Step 4: Activate the custom widget and try to assign a first column

Now it is time to try things out. Therefore please follow the explanations on: How can custom widgets be activated?
After adding the "Assign data" dialog view and controller, the custom widget "Assign data" dialog should work and look like follows.
It should be possible to assign a data column to the "Assign data" view. After leaving the dialog with "Ok" the assigned column will be persisted in the dashboard definition. Opening the "Assign data" dialog again will show the assigned column.
The preview will remain empty. In order to receive and display the assigned data in the widget please continue with step #5.

Step 5: Create and register the data service to receive data from the server

In order to receive the data of an assigned data source column in the custom widget view, the MashZone NEXTGEN server must be informed about the structure of the data to deliver. This is called "data mapping" and is to be implemented in the data service.
Therefore please create the file demoWidget2DataService.js in the js folder of the custom widget and add the following content:

demoWidget2DataService.js
angular.module('demoWidget2Module')
    /**
     * Data service for the custom widget
     * The data service defines the data mapping - the structure of the data delivered by the server
     */
    .service('demoWidget2DataService',['widgetDataService',
        function(widgetDataService) {
       <span style="color:#0000FF;"> /**
         * Utility function that creates one data mapping column
         * @param name The column name in the source, use dataColumn.name
         * @param type The data type of the column ("TEXT", "DATE", "NUMERIC")
         * @param filter Not interesting for this use case, set to '' or undefined
         * @param copyFrom If the user wants to have a source column twice, e.g. to see different aggregations for a numeric column, here he has to define the original column name
         * @param newName If the user has changed the column name please provide dataColumn.newName
         * @param round For numeric columns the user can define if values should be rounded or not. Set to true or false
         * @param precision Specifies precision for numeric columns. There is a utility function in formatNumberService that is called getPrecisionFromFormat
         * that can be used to calculate the precision from a format pattern. Set to undefined for text or date columns.
         * @param keepInResult Not interesting for this use case, set to true
         * @param aggregation Aggregation, if not specified the aggregation in the datamapping column is undefined
         * @returns A datamapping column object
         */</span>
        <span style="color:#B22222;">var </span>createDataMappingColumn =
            <span style="color:#B22222;">function</span>(anyDataColumn){
                <span style="color:#B22222;">return</span> widgetDataService.createDataMappingColumn(
                        anyDataColumn.name,
                        anyDataColumn.type,
                        '',
                        '',
                        anyDataColumn.name,
                        <span style="color:#B22222;">false</span>,
                        undefined,
                       <span style="color:#B22222;"> true</span>,
                        undefined);
            };
        <span style="color:#B22222;">return </span>{

            <span style="color:#0000FF;">/**
              * This function is called by the custom widget framework.
              * It prepares the data handling of the custom widget (item) with the server
              * by creating the data mapping for each configured (assigned) data column
              */</span>
            getDataMapping : <span style="color:#B22222;">function</span>(item) {
                <span style="color:#B22222;">var</span> dataMapping = {
                        columns: []
                    }
               <span style="color:#B22222;"> if</span> (item.config &amp;&amp; item.config.demoWidgetCfgModel){
                    <span style="color:#B22222;">if</span> (item.config.demoWidgetCfgModel.anyDataColumn) {
                        dataMapping.columns.push(createDataMappingColumn(item.config.demoWidgetCfgModel.anyDataColumn));
                    }
                }
                <span style="color:#B22222;">return</span> dataMapping;
            }
        };
    }
]);</code></pre>

Explanation: The data service defines the structure of the data delivered by the server. This is done with the help of the function "getDataMapping(item) starting in line #43.
Hint: The data service is an AngularJS service. For further explanations on AngularJS Services see: What are AngularJS Services?

The data service needs to be registered in the custom widget configuration file.
Therefore please edit the file demoWidget2Config.js and add the code block starting with "resolve".

demoWidget2Config.js
angular.module('demoWidget2Module')
    /**
     * Configuration for the custom widget
     * The config contains all configurable properties of the widget
     */
    .config(['dashboardProviderProvider',
        function (dashboardProviderProvider) {
            dashboardProviderProvider.widget('demoWidget2', {
                title: 'Demo Widget 2',
...
                resolve: {
                    widgetInterface: function(config, demoWidget2DataService) {
                        config.widgetInterface = {
                            getDataMapping: demoWidget2DataService.getDataMapping
                        };
                    }
                }
            });
        }
    ]);

Explanation: The data service is now registered. As a result the custom widget is now set up to request data for assigned columns from the server by using the function "getDataMapping" of the data service "demoWidget2DataService".

Step 6: Extend the custom widget controller and view to display the data of an assigned column

Up until this point, the assignment of a data column is possible, but without any visible effect in the custom widget. Why is that?
Answer: The custom widget controller needs to be informed about the assigned column as well.
Therefore please edit the file demoWidget2Ctrl.js and add the following content:

demoWidget2Ctrl.js

angular.module('demoWidget2Module')

    /**
     * Controller for the custom widget
     * The controller provides the widget variables and manages the interactions between data-model and view
     */

    .controller('demoWidget2Ctrl',['$scope',
        function($scope){

            /**
             * The init function creates all necessary scope variables at widget start up time
             */

            var initWidget = function() {
               //Create a scope object "config" if not present already
                //$scope.config holds the data of the widget settings like the assigned column(s)

                if(!$scope.config){
                    $scope.config = {};
                }
               //Define a property "assignedColumns" in the widget settings
                //to manage the information about the assigned column(s) in the controller

                if (!$scope.config.assignedColumns) {
                    $scope.config.assignedColumns = { anyDataColumn : undefined };
                }
            };

           /**
             * This function transforms the column und row data coming from the server
             * and creates scope variables that can be easily used in the view
             * @param data The data for the assigned and server-requested data source columns
             */

           var createScopeVariables = function(data) {
                $scope.columns = data.columns;
                $scope.rows = data.rows;
                $scope.rowCount = data.rows.length;
            };

            /**
             * This function is called from the custom widget framework when the requested data from the server returns
             * @param data The data for the assigned and server-requested data source columns
             */

            $scope.setData = function(feedResult){
                createScopeVariables(feedResult);
            };

            /**
             * This function loads the custom widget settings at widget start up time and stores it in the controller
             * @param config The persisted custom widget settings from the dashboard definition
             */

            $scope.setConfig = function(config){
                $scope.config = config;
            };

            initWidget();
        }
    ]);

Explanation: Please have a look at the documentation comments in the code example describing the different functions of the controller.

The custom widget view also needs to be prepared to work with the assigned data. Therefore please edit the file demoWidget2.html, and add the following content.

Explanation: Using the $scope variables "columns" and "rowCount" that were defined in the controller demoWidget2Ctrl.js the view has access to the assigned columns.

This is realized using AngularJS expressions. See the double curly brace notation {{ }} in lines #4, #7, #8 and #9. For further explanations on AngularJS Expressions see: What are AngularJS Expressions?

As a result the custom widget (preview) will display some column information, depending on a given data assignment.
Widget without an assigned column:

Widget with an assigned column:

Step 7: Extend the custom widget view to display the data of multiple assigned columns

In order to assign more than just one column to the custom widget a few changes are necessary. At first the data service needs to be adapted to manage multiple columns.
Therefore please edit the file demoWidget2DataService.js and extend the function getDataMapping() as follows:

demoWidget2DataService.js
angular.module('demoWidget2Module')
    /**
     * Data service for the custom widget
     * The data service defines the data mapping - the structure of the data delivered by the server
     */
    .service('demoWidget2DataService',['widgetDataService',
        function(widgetDataService) {
 ...
            return {
                /**
                  * This function is called by the custom widget framework.
                  * It prepares the data handling of the custom widget (item) with the server
                  * by creating the data mapping for each configured (assigned) data column
                  */
                getDataMapping : function(item) {
                    var dataMapping = {
                            columns: []
                        }
                    if (item.config && item.config.assignedColumns){
                        if (item.config.assignedColumns.numDataColumn) {
                            dataMapping.columns.push(createDataMappingColumn(item.config.assignedColumns.numDataColumn));
                        }
                        if (item.config.assignedColumns.textDataColumn) {
                            dataMapping.columns.push(createDataMappingColumn(item.config.assignedColumns.textDataColumn));
                        }
                        if (item.config.assignedColumns.dateDataColumn) {
                            dataMapping.columns.push(createDataMappingColumn(item.config.assignedColumns.dateDataColumn));
                        }
                        var furtherColumns = item.config.assignedColumns.furtherColumns;
                        if (furtherColumns) {
                            angular.forEach(furtherColumns, function(anyColumn) {
                                dataMapping.columns.push(createDataMappingColumn(anyColumn));
                            });
                        }
                    }
                    return dataMapping;
                }
            };
        }
    ]);

Explanation: This change extends the custom widget data service to handle multiple columns. As compared to the previous version the function "getDataMapping" now checks four variables that might contain information about assigned columns ("numDataColumn", "textDataColumn", "dateDataColumn" and the array "furtherColumns[]"). Once an assigned column was found, the function "createDataMappingColumn(...)" will be called to enrich the dataMapping with the column information.
Hint: All of these variables are accessible via the custom widget settings object "itemClone.config.assignedColumns" that was defined in the custom widget controller.

In order to fill the four variables with information about assigned columns the assignData view template needs to be extended to accept multiple column assignments first.
Therefore please edit the file assignData.html and add the following content.

assignData.html
<div class="assign-data">
    <div>
        <div class="inline-edit" ng-controller="demoWidget2AssignDataCtrl">
            Drop a numeric column here:
            <ul ff-single-column-drop
                item="itemClone"
                column="itemClone.config.assignedColumns.numDataColumn"
                input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
                column-dropped="columnDropHandler(column)"
                accept-filter="numeric">
            </ul>
            Drop a text column here:
            <ul ff-single-column-drop
                item="itemClone"
                column="itemClone.config.assignedColumns.textDataColumn"
                input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
                column-dropped="columnDropHandler(column)"
                accept-filter="text">
            </ul>
            Drop a date column here:
            <ul ff-single-column-drop
                item="itemClone"
                column="itemClone.config.assignedColumns.dateDataColumn"
                input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
                column-dropped="columnDropHandler(column)"
                accept-filter="date">
            </ul>
            Drop any further column here:
            <ul class="columnDropTarget"
                ff-multiple-column-drop
                item="itemClone"
                columns="itemClone.config.assignedColumns.furtherColumns"
                input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
                column-dropped="columnDropHandler(column)"
                accept-filter="text,numeric,date">
            </ul>
        </div>
    </div>
</div>

Explanation: The custom widget "Assign data" dialog view can accept multiple column assignments now by offering multiple column drop target controls. The first three drop target controls are data type specific having the attribute "accept-filter" set to one specific data type. The last one is a multi-column drop target control accepting more than one column in count and data type.

After the extension the "Assign data" dialog view will look like that:After the extension the "Assign data" dialog view will look like that:

Now the custom widget view template must be extended to display the data of multiple columns.
Therefore please adapt the file demoWidget2.html as follows:

demoWidget2.html
<!--This is the view template for Demo Widget 2-->
<div class="demoWidget2">
    <h3>Data Assignment</h3>
    <div ng-show="columns.length == 0">No column was assigned.</div>
    <div ng-show="columns.length > 0">
        <table border="1">
            <tr>
                <th>Column name</th>
                <th>Data type</th>
            </tr>
            <tr ng-repeat="column in columns">
                <td>{{column.name}}</td>
                <td>{{column.type}}</td>
            </tr>
        </table>
        <div>Row count: {{rowCount}}</div>
    </div>
</div>

Hint: Using the AngularJS directive ng-repeat (line #11), html table structures can be easily created by iterating over an array. For further explanations on ng-repeat see: https://docs.angularjs.org/api/ng/directive/ngRepeat

Finally, let's add some CSS styling to prettify the view of the custom widget. 
Therefore please edit the file demoWidget2Styles.css and add the following style definitions:

demoWidget2Styles.css
.demoWidget2 {
    overflow: auto;
    padding: 10px;
    height: 100%;
}
.demoWidget2 h3{
    text-decoration: underline;
    text-align: center;
}
.demoWidget2 table {
    width: 100%;
}
.demoWidget2 th {
    border: 1px solid grey;
    padding: 5px;
}
.demoWidget2 td {
    border: 1px solid grey;
    padding: 5px;
}
.demoWidget2 tr:nth-child(odd) {
    background-color: #f1f1f1;
}
.demoWidget2 tr:nth-child(even) {
    background-color: #ffffff;
}
.columnDropTarget{
    width: 200px;
    max-width: 200px;
}

As a result of step #7, the custom widget view and preview will display a table with details on assigned columns.

Step 8: Adapt the custom widget view to display the content of assigned columns

Up until this point, the custom widget only displays structural data of the assigned data source, like name and data type of columns or the number of rows.
In order to access the actual data, meaning the content of the rows for each column, the custom widget view template must be slightly changed.
Therefore please edit the file demoWidget2.html again and adapt the code as shown below:

demoWidget2.html
<!--This is the view template for Demo Widget 2-->
<div class="demoWidget2">
    <h3>Data Assignment</h3>
    <div ng-show="columns.length == 0">No column was assigned.</div>
    <div ng-show="columns.length > 0">
    &lt;div&gt;Row count: {{rowCount}}&lt;/div&gt;
    &lt;table&gt;
        &lt;tr&gt;
            &lt;th ng-repeat=<span style="color:#008000;">"column in columns"</span>&gt;
                {{column.name}}
            &lt;/th&gt;
        &lt;/tr&gt;
        &lt;tr ng-repeat=<span style="color:#008000;">"row in rows"</span>&gt;
            &lt;td ng-repeat=<span style="color:#008000;">"column in columns"</span>&gt;
                {{row.values[column.idx]}}
            &lt;/td&gt;
        &lt;/tr&gt;
    &lt;/table&gt;

&lt;/div&gt;

</div>

Explanation: By iterating over rows and columns the actual feed result can be accessed and visualized in the HTML table cells.
Hint: For further information on reading out the feed result please have a look at "Apendix 1: Understanding and accessing the feed result"


Summary and sources

As the final result of this chapter the custom widget now shows a table with all rows of multiple assigned columns.

The created files and source code for "Demo Widget 2" can be downloaded from here.


Apendix 1: Understanding and accessing the feed result

When the client requests data for a custom widget from the server, a feed result is being returned, depending on the assigned columns. For the assigned columns Employees, Sector, Year End and Country the following feed result is being delivered to the client:

FeedResult:
{
 "status":"COMPLETE",
 "columns":[{"idx":0,"name":"Employees","type":"NUMERIC"},
            {"idx":1,"name":"Company","type":"TEXT"},
            {"idx":2,"name":"Year End","type":"DATE"},
            {"idx":3,"name":"Sector","type":"TEXT"}],
 "rows":[{"values":["2200000.0","Wal-Mart Stores","2015-01-31","General retailers"]},
         {"values":["338875.0","Toyota Motor","2014-03-31","Automobiles & parts"]},
         {"values":["99927.0","Samsung Electronics","2014-12-31","Leisure goods"]},
         {"values":["94000.0","Royal Dutch Shell","2014-12-31","Oil & gas producers"]},
         {"values":["180000.0","Walt Disney","2014-09-27","Media"]}],
 "messages":[],
 "warnings":[],
 "errors":[]
}

Explanation: The idx, name and type of every requested (assigned) column is returned in an array "columns".
All available rows are returned in an array "rows". This array holds an object for each row, which again hold an array containing the data of the row cells.
The row index (position) in each row object array matches the accompanying column index (idx). Thus addressing a table cell is possible by using the row index and the column index.

//addressing of a table cell in general: rows[rowIndex].values[colIndex]
//some examples for the feed result above

 
//addressing the value for the first row of column "Company"
var rowIndex = 0;                                   //first row
var colIndex = 1;                                   //second column "Company"
var firstCompany = rows[rowIndex].values[colIndex]; //returns "Wal-Mart Stores"
 
//addressing the value for the last row of column "Sector"
var rowIndex = rows.length;                         //last row programmatic approach
var rowIndex = 4;                                   //last row
var colIndex = 3;                                   //fourth column "Sector"
var lastSector = rows[rowIndex].values[colIndex];   //returns "Media"

Read in this series:

image.png

image.png