(3) Configuration of assigned columns

Overview

The third chapter is a follow-up of chapter two and describes further steps to configure assigned columns.
This chapter is recommended for anyone planning to create a custom widget that uses MashZone NG data sources.
The basis for chapter three is a custom widget that uses the data assignment as it was explained and created in chapter two.

Widget Preview

The third custom widget “Demo widget 3” will be able to show aggregated and formatted values of a MashZone NG data source.
As compared to "Demo widget 2" the view of the "Assign data dialog" will be extended and provide more options for column configuration and pre-processing of the data.
As a result, the structure and format of the widget values will change. Jump to Summary at the end of the chapter for a preview.

Resulting files

Demo widget 3 consists of the following directories and files.
As compared to the structure of "Demo widget 2" the following folders and files were added or changed. 

\---customWidgets
     \---demoWidget3 
          |     demoWidget3Config.js
          |     demoWidget3Module.js
          | 
          +---assets 
          |     +---css
          |     |        demoWidget3Styles.css
          |     | 
          |     \---images
          |              demoWidget3MenuIcon_32x32.png
          | 
          +---js
          |        demoWidget3AssignDataCtrl.js 
          |        demoWidget3Ctrl.js
          |        demoWidget3DataService.js 
          | 
          \---partials
               |   demoWidget3.html
               | 
               \--- assignDataDialog
                     |   advancedProperties.html

                     |   assignColumns.html
                     |   assignData.html
                     |
                     \--- columnTypeConfigTemplates
                               date.html
                               numeric.html
                               text.html 

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

Recap and next steps


As described in chapter two and implemented in "Demo widget 2" it is now possible to assign data columns to a custom widget.
As a result "Demo widget 2" will display all rows of all assigned columns as shown in the example below.

In this example above the columns "Employees", "Country" and "Company" were assigned to the widget. One can easily see that there is a relationship between these columns. The column "Company" serves as a partition and groups all employees of one country into subsets, one subset for each company. In other words: all employees of one country are distributed among the companies of that country. Up until now the widget displays all rows of all assigned columns and having the columns "Employees", "Country" and "Company" assigned the widget is showing the correct data. 

But in case the partition column "Company" was not assigned, displaying all rows of all assigned columns might not make sense as the partition information is missing showing the mentioned relationship between the columns. In the example below only the columns "Employees" and "Country" were assigned to "Demo widget 2". This results in several rows displaying a different number of employees for the same country.

The solution to this problem is the aggregation of the subsets, for example, the summation of all employees for one country. As a result, there would be only one row per country displaying the sum of all employees of that country, regardless of the company.
Aggregation is just one example of a column configuration. Depending on the column data type there are different configurations possible. 

Configuration overview 

Configuration property Text Column Numeric Column Date Column
Display Name
Format  
Aggregation    
Rounding    

The data mapping provides all necessary properties to configure assigned columns, but the necessary controls must be added to the data assignment view first.
In this chapter the data assignment view will be extended to provide configuration controls for assigned columns. 

Creation Steps

Step 1: Extend the Assign data dialog view

As compared to the implementation in "Demo widget 2" the Assign data dialog view will become more complex. Besides the column assignment it will be extended with a column configuration part. The different views will be extracted in own html templates, one for the main view template, one for the column assignment view and one for the column configuration view. The main view template is implemented in the file assignData.html and combines the two view templates for column assignment assignColumns.html and column configuration advancedProperties.html
Adapt the file assignData.html and create the files assignColumns.html and advancedProperties.html in order to extend the Assign data dialog view.
Edit the file assignData.html in the folder partials and add the following code.

assignData.html
<div class="assign-data">
    <div class="inline-edit">
        <div class="inline-edit" ng-controller="demoWidget3AssignDataCtrl">
            <div class="master-panel" ng-include="widget.assignColumns">
            </div>
            <div class="details-panel" ng-include="widget.advancedProperties">
            </div>
        </div>
    </div>
</div>

Hint: Using the AngularJS directive ng-include (see lines #4 and #6) it is possible to load external HTML fragments into a view template. For further explanations on ng-include please visit the AngularJS API Docs.

Create the file assignColumns.html in the folder partials and add the drop target controls as shown in the source code below.

assignColumns.html
Drop a numeric column here:
<ul class="columnDropTarget"
    ff-single-column-drop
    item="itemClone"
    column="itemClone.config.assignedColumns.numDataColumn"
    input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
    column-clicked="columnSelectionHandler(column)"
    column-dropped="columnDropHandler(column)"
    column-removed="columnDeleteHandler(column)"
    accept-filter="numeric"
    selection-condition="selectColumn(column)">
</ul>
Drop a text column here:
<ul class="columnDropTarget"
    ff-single-column-drop
    item="itemClone"
    column="itemClone.config.assignedColumns.textDataColumn"
    input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
    column-clicked="columnSelectionHandler(column)"
    column-dropped="columnDropHandler(column)"
    column-removed="columnDeleteHandler(column)"
    accept-filter="text"
    selection-condition="selectColumn(column)">
</ul>
Drop a date column here:
<ul class="columnDropTarget"
    ff-single-column-drop
    item="itemClone"
    column="itemClone.config.assignedColumns.dateDataColumn"
    input-columns="itemClone.calculationDefinitionContainer.calculationResultColumns"
    column-clicked="columnSelectionHandler(column)"
    column-dropped="columnDropHandler(column)"
    column-removed="columnDeleteHandler(column)"
    accept-filter="date"
    selection-condition="selectColumn(column)">
</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-clicked="columnSelectionHandler(column)"
    column-dropped="columnDropHandler(column)"
    column-removed="columnDeleted(column)"
    accept-filter="date,text,numeric"
    selection-condition="selectColumn(column)">
</ul>

Hint: The functionality of the drop target controls was explained in chapter two.
Add the file advancedProperties.html in the folder partials to create the column configuration view template.

advancedProperties.html
<div ng-if="selectedColumn" class="advanced-properties">
    <div class="advanced-cofig-main-row">
        Display Name
        <div ff-column-name-input
             item="itemClone"
             column="selectedColumn"></div>
    </div>
     
    <!--Switches templates depending on the data type of the selected column-->
    <div ff-column-type-config
         widget-type-name='demoWidget3'
         column="selectedColumn"></div>
</div>

Explanation: The directive "ff-column-name-input" in line #4 provides the controls to configure the column display name. The directive "ff-column-type-config" in line #10 loads additional view templates to configure the properties for different column types.
Hint: The column properties for different column types are described in the Configuration overview.

Step 2: Create the column configuration view templates for different column types

As listed in the Configuration overview, each column type has different configuration properties and therefore needs different configuration controls. Besides the display name, that can be configured for all column types, there are additional configuration properties for numeric and date columns. Please follow the instructions to create the different view templates.
Create the folder columnTypeConfigTemplates underneath the folder assignDataDialog and add three empty files numeric.html, date.html and text.html.
Add the following source code to the file numeric.html

numeric.html
<div class="config-field">
    Aggregation
    <div ff-aggregation-select model="column.aggregation" allow-no-aggregation="true"></div>
</div>
<div class="config-field" >
    Format
    <div ff-number-format-select model="column.format" editable="true" class="config-field-entry"></div>
</div>
 
<div class="config-field advanced-cofig-main-row alignCenter" >
    <span ff-number-round-select model="column.round"></span>
    <label>Round Numerically</label>
</div>

Explanation: This will add the necessary controls for the configuration of numeric columns as shown in the image below.

Add the following source code to the file date.html

date.html
<div>
    <label>Format </label>
    <div ff-date-locale-format-select model="column.format" editable="true" class="config-field-entry"></div>
</div>

Explanation: This will add the necessary controls for the configuration of date columns as shown in the image below.

Since text columns don't have any additional configuration properties the file text.html will stay empty.

Step 3: Register view templates in the custom widget configuration

Finally all created view templates must be registered in the custom widget configuration.
Therefore please edit the file demoWidget3Config.js and add the following lines of code.

demoWidget3Config.js
angular.module('demoWidget3Module')
 
    /**
     * Configuration for the custom widget
     * The config contains all configurable properties of the widget
     */
    .config(['dashboardProviderProvider','columnTypeConfigUrlServiceProvider',
        function (dashboardProviderProvider, columnTypeConfigUrlServiceProvider) {
            dashboardProviderProvider.widget('demoWidget3', {
                title: 'Demo Widget 3',
...
                assignData: 'widgets/customWidgets/demoWidget3/partials/assignDataDialog/assignData.html',
                assignColumns: 'widgets/customWidgets/demoWidget3/partials/assignDataDialog/assignColumns.html',
                advancedProperties: 'widgets/customWidgets/demoWidget3/partials/assignDataDialog/advancedProperties.html',
...
            });
 
            columnTypeConfigUrlServiceProvider.addColumnTypeUrlMapByWidget('demoWidget3',
                {
                    NUMERIC : ['widgets/customWidgets/demoWidget3/partials/assignDataDialog/columnTypeConfigTemplates', 'numeric.html'].join('/'),
                    DATE : ['widgets/customWidgets/demoWidget3/partials/assignDataDialog/columnTypeConfigTemplates', 'date.html'].join('/'),
                    TEXT : ['widgets/customWidgets/demoWidget3/partials/assignDataDialog/columnTypeConfigTemplates', 'text.html'].join('/')
                });
        }
    ]);

Explanation:
In the lines #7 and #8 the Angular JS service "columnTypeConfigUrlService" is added to the configuration. For further explanations on AngularJS services please visit the AngularJS API Docs.

In the lines #12 to #14 the Assign data dialog view templates are registered.

In the lines #18 to #23 the "columnTypeConfigUrlService" is used to register the column configuration view templates for different data types. This information is used by the directive "ff-column-type-config" in advancedProperties.html to load the column type depending view templates.

Step 4: Extend the Assign data dialog controller

All Assign data dialog view templates are ready now, but in order to interact with each other some logic needs to be added to the Assign data dialog controller.
Therefore please edit the file demoWidget3AssignDataCtrl.js and adjust the source code as shown in the example below.

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

demoWidget3AssignDataCtrl.js
angular.module('demoWidget3Module')
 
    /**
     * Controller for the Assign data dialog
     */
    .controller('demoWidget3AssignDataCtrl',['$scope','aggregationConstants','typeconstants','numberFormatConstants',
        function($scope, aggregationConstants, typeconstants, numberFormatConstants){
 
            /**
             * This function changes the selection state of a column.
             * A column selection triggers the loading of the matching configuration view template.
             * @param column The column that is to be selected
             */
            $scope.columnSelectionHandler = function(column) {
                $scope.selectedColumn = column;
            };
 
            /**
             * This function is called when a column was dropped in the assign data view template.
             * According to the column data type the column configuration will be predefined.
             * @param column The column that was dropped
             */
            $scope.columnDropHandler = function(column) {
                column.newName = column.name;
                if(column.type === typeconstants.NUMBER){
                    if(!column.aggregation) {
                        column.aggregation = aggregationConstants.AVG;
                    }
                    if(!column.format) {
                        column.format = numberFormatConstants.NUMERIC_FORMAT_PATTERNS[3];
                    }
                    column.round = true;
                } else if(column.type === typeconstants.DATE){
                    column.format = "yyyy-MM-dd'T'HH:mm:ss";
                }
                $scope.columnSelectionHandler(column);
            };
 
            /**
             * This function is called when the user clicks on the trash icon of a column.
             * If no column is selected the configuration view will be hidden.
             * @param column The column that should be deleted
             */
            $scope.columnDeleteHandler = function(column) {
                if ($scope.selectedColumn === column) {
                    delete $scope.selectedColumn;
                }
            };
 
            /**
             * Indicates if a column should be marked as selected
             * @param column The column to check
             * @returns {boolean} true if the column should be selected, otherwise false
             */
            $scope.selectColumn = function(column) {
                return $scope.selectedColumn === column;
            };
 
            /**
             * This function searches for assigned columns and selects the first one found
             */
            $scope.selectFirstColumnFound = function(){
                var foundColumn = undefined;
                //check numeric column
                if($scope.itemClone.config.assignedColumns.numDataColumn != undefined){
                    foundColumn = $scope.itemClone.config.assignedColumns.numDataColumn;
                } else
                //check text column
                if($scope.itemClone.config.assignedColumns.textDataColumn != undefined){
                    foundColumn = $scope.itemClone.config.assignedColumns.textDataColumn;
                } else
                //check date column
                if($scope.itemClone.config.assignedColumns.dateDataColumn != undefined){
                    foundColumn = $scope.itemClone.config.assignedColumns.dateDataColumn;
                } else
                //check other columns
                if($scope.itemClone.config.assignedColumns.furtherColumns != undefined && $scope.itemClone.config.assignedColumns.furtherColumns.length > 0){
                    foundColumn = $scope.itemClone.config.assignedColumns.furtherColumns[0];
                }
                if(foundColumn != undefined){
                    $scope.columnSelectionHandler(foundColumn);
                }
            }
            //There must be one selected column, initially select the first column found
            $scope.selectFirstColumnFound();
        }
    ]);

Step 5: Extend the custom widget data service

Before any of the additional column configuration properties will show an effect they must be correctly added to the data mapping. Therefore the data service function "createDataMappingColumn()" must be adjusted as follows.

demoWidget3DataService.js
angular.module('demoWidget3Module')
    /**
     * Data service for the custom widget
     * The data service defines the data mapping - the structure of the data delivered by the server
     */
    .service('demoWidget3DataService',['widgetDataService','formatNumberService','typeconstants',
        function(widgetDataService,formatNumberService,typeconstants) {
            /**
             * Utility function createDataMappingColumn(...) 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
             */
            var createDataMappingColumn =
                function(dataColumn){
                    var isNumericColumn =  dataColumn.type === typeconstants.NUMBER;
                    var format = isNumericColumn ? formatNumberService.getFormatFromFormatKey(dataColumn.format) : undefined,
                        precision = isNumericColumn ? formatNumberService.getPrecisionFromFormat(format) : undefined,
                        aggregation = isNumericColumn && dataColumn.aggregation ? {type: dataColumn.aggregation} : undefined;
                    return widgetDataService.
                        createDataMappingColumn(
                            dataColumn.name,
                            dataColumn.type,
                            null,
                            '',
                            dataColumn.newName || '',
                            dataColumn.round,
                            precision,
                            true,
                            aggregation);
                };
            return {
  ...
            };
        }
    ]);

Explanation: As compared to the data service of "Demo widget 2" the properties "newName", "round", "precision" and "aggregation" are used now in the data mapping. Depending on the column data type different configuration properties are send to the server for data manipulation. 

Step 6: Adjust the custom widget controller and view

As a last step the actual widget view must be adjusted to make use of the additional settings. Therefore the custom widget controller must be extended as follows.

demoWidget3Ctrl.js
angular.module('demoWidget3Module')
 
    /**
     * Controller for the custom widget
     * The controller provides the widget variables and manages the interactions between data-model and view
     */
    .controller('demoWidget3Ctrl',['$scope','formatDateService','formatNumberService',
        function($scope, formatDateService, formatNumberService){
 
            //These services are providing value transformation functions that are used in the custom widget view
            $scope.formatDateService = formatDateService;
            $scope.formatNumberService = formatNumberService;
 
            /**
             * The init function creates all necessary scope variables at widget start up time
             */
            var initWidget = function() {
...
            };
 
            /**
             * This function searches for a configured column with a given name
             * @param columnName The name of the configured column
             * @retrun The configured column, or undefined if not found
             */
            var getConfiguredColumnByName = function(columnName) {
                //check numeric column
                if($scope.config.assignedColumns.numDataColumn != undefined && $scope.config.assignedColumns.numDataColumn.name == columnName){
                    return $scope.config.assignedColumns.numDataColumn;
                }
                //check text column
                if($scope.config.assignedColumns.textDataColumn != undefined && $scope.config.assignedColumns.textDataColumn.name == columnName){
                    return $scope.config.assignedColumns.textDataColumn;
                } else
                //check date column
                if($scope.config.assignedColumns.dateDataColumn != undefined && $scope.config.assignedColumns.dateDataColumn.name == columnName){
                    return $scope.config.assignedColumns.dateDataColumn;
                } else
                //check other columns
                if($scope.config.assignedColumns.furtherColumns != undefined){
                    for(var i = 0; i < $scope.config.assignedColumns.furtherColumns.length; i++){
                        var column = $scope.config.assignedColumns.furtherColumns[i];
                        if(column.name == columnName){
                            return $scope.config.assignedColumns.furtherColumns[i];
                        }
                    }
                }
                return undefined;
            };
 
            /**
             * This function adds all configured columns to a scope variable configuredColumns
             * that can be used by the custom widget view to access the column configuration
             * @param data The data for the assigned and server-requested data source columns
             */
            var createConfiguredColumns = function(data) {
                for (var i = 0; i < data.columns.length; i++) {
                    var configuredColumn = getConfiguredColumnByName(data.columns[i].name);
                    $scope.configuredColumns[data.columns[i].name] = configuredColumn;
                }
            }
 
            /**
             * 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.configuredColumns = [];
                createConfiguredColumns(data);
            };
 
...
 
            initWidget();
        }
    ]);

Explanation: 
The AngularJS services "formatDateService" and "formatNumberService" were added to the controller (see line #7) and made available as scope variables (see line #10 to #12). These services provide functionalities to format date and number values in the custom widget view. The column configuration was also made available as a scope variable "configuredColumns" (see line #70) and provides the view with the configuration data. The functions "createConfiguredColumns()" and "getConfiguredColumnByName()" are helper functions used to fill the array "configuredColumns" with data.

The column configuration can finally be used in the custom widget view. The column configuration can finally be used in the custom widget view. 

demoWidget3.html
<!--This is the view template for Demo Widget 3-->
<div class="demoWidget3">
    <h3>Data Configuration</h3>
...
            <tr ng-repeat="row in rows">
                <td ng-repeat="column in columns" >
                    <!--Date row-->
                    <span ng-if="column.type==='DATE'">
                        {{formatDateService.formatDate(row.values[column.idx], configuredColumns[column.name].format}}
                    </span>
                    <!--Numeric row-->
                    <span ng-if="column.type==='NUMERIC'">
                        {{formatNumberService.formatNumberWithPattern(row.values[column.idx], configuredColumns[column.name].format, configuredColumns[column.name].round)}}
                    </span>
                    <!--Text row-->
                    <span ng-if="column.type==='TEXT'">
                        {{row.values[column.idx]}}
                    </span>
                </td>
            </tr>
        </table>
    </div>
</div>

Explanation: The values are transformed now according to the configuration settings. Using the AngularJS directive "ng-if" it is possible to easily differentiate between the column data types. For each column data type the corresponding configuration settings are used to transform the value. The transformation of the values is done using the services "formatDateService" and "formatNumberService" in an AngularJS Expression. The column configuration settings are read from the array "configuredColumns".

Hint: For further explanations on AngularJS Expressions see: What are AngularJS Expressions?

Summary and sources 

As the final result of chapter three, the custom widget provides additional column configuration controls. The configuration settings are used to do value transformations in the view and to enrich the data mapping to ensure the right pre-processing of the data on the server.

Result for numeric column configuration

Result for text column configuration

Read in this series:


The source code for "Demo Widget 3" can be Downloaded here