In the previous section we saw the power of the datagrid with lazy loaded flat data. That example demonstrated lazily loading flat data. In terms of hierarchical data, this means it demonstrated lazy loading data at the "top level". As you may have guessed, we support this same functionality at the inner (children) levels. Before we get too far ahead, you may be better served by understanding the concept of 'level"


Below is the documentation of the class FlexDataGridColumnLevel:


A class that contains information about a nest level of grid. This includes the columns at this level, information about whether or not to enable paging, footers, filters, the row sizes of each, the property of the dataprovider to be used as the key for selection, the property of the data provider to be used as the children field, the renderers for each of the cells, etc.The Grid always contains at least one level. This is the top level, and is accessible via the columnLevel property of the grid.


One of the most important concepts behind the Architecture of Flexicious Ultimate arose from the fundamental requirement that the product was created for - that is display of Heterogeneous Hierarchical Data.


The notion of nested levels is baked in to the grid via the "columnLevel" property. This is a property of type "FlexDataGridColumnLevel". The grid always has at least one column level. This is also referred to as the top level, or the root level. In flat grids (non hierarchical), this is the only level. But in nested grids, you could have any number of nested levels. This can be demonstrated in the following image.





The columns collection actually belongs to the columnLevel, and since there is one root level, the columns collection of the grid basically points to the columns collection of this root level. The FlexDataGridColumnLevel class has a "nextLevel" property, which is a pointer to another instance of the same class, or a "nextLevelRenderer" property, which is a reference to a ClassFactory the next level. Please note, currently, if you specify nextLevelRenderer, the nextLevel is ignored. This means, at the same level, you cannot have both a nested subgrid as well as a level renderer. Bottom line - use nextLevelRenderer only at the innermost level. Our examples demonstrate this.


Another thing to note is that there are two modes in which inner levels work with columns. Either same set of columns shared across each level, or each level has its own set of columns (We call them nested grids vs grouped grids). More information here :http://htmltreegrid.com/newdocs/html/Flexicious%20HTMLTreeGrid.html?AdvancedConfigurationOptionsHier.html


In markup, these would be defined as such:

='<grid id="grid"  ...>'+
'                        <level ...>'+
'                                <columns>'+
'                                        <column type="checkbox"  />'+
...

'                                        <column enableCellClickRowSelect="false" width="2000" excludeFromSettings="true" excludeFromExport="true" excludeFromPrint="true" />'+
'                                </columns>'+
'                                <nextLevel>'+
'                                        <level  ...>'+
'                                                <columns>'+
'                                                        <column type="checkbox" />'+
'                                                        ...
'                                                        <column itemEditor="flexiciousNmsp.DatePicker" editable="true" editorDataField="selectedDate"  dataField="dealDate" headerText="Deal Date" labelFunction="flexiciousNmsp.UIUtils.dataGridFormatDateLabelFunction"/>'+
'                                                </columns>'+
'                                                <nextLevel>'+
'                                                        <level  ...>'+
'                                                                <columns>'+
'                                                                        <column type="checkbox" />'+
'                                                                        ...
'                                                                        <column editable="true" dataField="lineItemAmount" headerText="Line Item Amount" textAlign="right" footerLabelFunction2="myCompanyNameSpace.fullyLazyLoaded_getFooterLabel" footerAlign="right" labelFunction="flexiciousNmsp.UIUtils.dataGridFormatCurrencyLabelFunction"/>'+
'                                                                                </columns>'+
'                                                                        </level>'+
'                                                                </nextLevel>'+
'                                                        </level>'+
'                                                </nextLevel>'+
'                                        </level>'+
'                                </nextLevel>'+
'                        </level>'+
'        </grid>';

 

(Full markup here : http://www.htmltreegrid.com/demo/examples/js/samples/Nested.js)


OR


'<grid id="grid" ...>'+
'                '+
'                        <level  ... >'+
'                                <columns>'+
'                                        <column type="checkbox"   />'

....

'                                        <column itemEditor="flexiciousNmsp.DatePicker"  '+
'                                                                                                                   dataField="dueDate" headerText="Due Date" filterControl="DateComboBox"'+
'                                                                                                                   labelFunction="flexiciousNmsp.UIUtils.dataGridFormatDateLabelFunction"/>'+
'                                </columns>'+
'                                <nextLevel>'+
'                                        <level ... reusePreviousLevelColumns="true" >'+
'                                                '+
'                                                <nextLevel>'+
'                                                        <level ... reusePreviousLevelColumns="true">'+
'                                                                '+
'                                                        </level>'+
'                                                </nextLevel>'+
'                                        </level>'+
'                                </nextLevel>'+
'                        </level>'+
'                '+
'        </grid>';


(Full markup here : http://www.htmltreegrid.com/demo/examples/js/samples/GroupedData.js)



Now, lets look at what a simple configuration with hierarchical data looks like:


Hierarchical data works very similar to flat data, with the exception that the JavaScript object at the top level have a property usually named children that is a pointer to an array of nested objects which represent the children of the top-level objects.  These nested objects in turn can have children of their own which in turn can have children of their own up to any number of nested levels.  The one thing to keep in mind here is that the name of the property that points to the next level children is configurable via the childrenField property.  This defaults to the string “children”  but can be anything as long as it's configured appropriately


Another thing to notice about this example is the use of the enableDynamicLevels flag:

For those of you who are not familiar with what a dynamic tree grid is, (Actually the term is dynamicLevels) - This means that the grid will introspect the data provider to automatically figure out how deep the tree will nest. This is in contrast to other configurations where you explicitly define how "deep" the tree will be, what columns will be at each level, etc. But in case the hierarchy is unknown at design time, the grid is capable of introspecting the data provider and automatically generating the levels at run time. You do this by setting enableDynamicLevels="true" on the grid. However, since the levels are not defined at design time, to be able to manipulate their properties at runtime, we have an event, DYNAMIC_LEVEL_CREATED and DYNAMIC_ALL_LEVELS_CREATED. Both these events are defined on the FlexDataGridEvent class.




<script type="text/javascript">
  
   $(document).ready(function () {
       var grid new flexiciousNmsp.FlexDataGrid(document.getElementById("gridContainer"),
               {

                   configuration:'<grid id="grid" enableDynamicLevels="true" variableRowHeight="true" horizontalScrollPolicy="on" recalculateSeedOnEachScroll="true" enableExport="true" forcePagerRow="true" pageSize="50" enableFilters="true" enableFooters="true" >' +
                           '                        <level  enableFooters="true" ' +
                           '   childrenField="children">' +
                           '                                <columns>' +
                           '                                        <column enableHierarchicalNestIndent="true" dataField="id" headerText="ID" width="100"/>' +
                           '                                        <column dataField="type" headerText="Type" width="100" wordWrap="true"/>' +
                           '                                        <column dataField="type" headerText="Type" />' +
                           '                                </columns>' +
                           '                        </level>' +
                           '                +
                           '        </grid>',
                   dataProvider:[
                       { "id":"5001""type":"None None None None None None None None None None None None None "  children:[
                           { "id":"5001""type":"None 1 None 1 None 1 None 1 None 1 None 1 None 1 None 1 None 1"  },
                           { "id":"5002""type":"Glazed 1 Glazed 1 Glazed 1 Glazed 1 Glazed 1 Glazed 1 Glazed 1 Glazed 1 Glazed 1 Glazed 1" },
                           { "id":"5005""type":"Sugar 1 Sugar 1 Sugar 1 Sugar 1 Sugar 1 Sugar 1 Sugar 1 Sugar 1" },
                           { "id":"5007""type":"Powdered Sugar 1 Powdered Sugar 1 Powdered Sugar 1 Powdered Sugar 1 Powdered Sugar 1" },
                           { "id":"5006""type":"Chocolate with Sprinkles 1 Chocolate with Sprinkles 1 Chocolate with Sprinkles 1 Chocolate with Sprinkles 1" },
                           { "id":"5003""type":"Chocolate 1 Chocolate 1  Chocolate 1  Chocolate 1  Chocolate 1  Chocolate 1  Chocolate 1  Chocolate 1  Chocolate 1 " },
                           { "id":"5004""type":"Maple 1 Maple 1  Maple 1  Maple 1  Maple 1  Maple 1  Maple 1  Maple 1  Maple 1  Maple 1 " }
                       ]

                       },
                       { "id":"5002""type":"Glazed" },
                       { "id":"5005""type":"Sugar" },
                       { "id":"5007""type":"Powdered Sugar" },
                       { "id":"5006""type":"Chocolate with Sprinkles" },
                       { "id":"5003""type":"Chocolate" },
                       { "id":"5004""type":"Maple" }
                   ]
               });

           grid.expandAll();
           grid.validateNow();
   });

</script>


In this example we can see that the top level object with the ID of 5001 has a list of children.


 


Hierarchical Data - Lazy Load - Java and PHP SampleParent Previous

 

In the previous section we looked at the structure of a hierarchical treegrid - we talked about the concept of levels, nested vs grouped and dynamic levels. Now, lets talk about another complex topic, lazy load. In this example in addition to the top level lazy load we talked about in the previous example (using filterPageSortMode), we introduce a concept of itemLoadMode.


There are also two different modes of loading hierarchical data.

itemLoadMode=client (default) - This assumes the parent objects and child objects are all loaded in client memory upfront.

itemLoadMode=server - This assumes only the top level items are loaded, and the grid will trigger an event that you will then listen for, and load children in a lazy load mechanism (or load on demand). This is more appropriate when there are very large datasets.


When itemLoadMode is server, you may want to set childrenCountField.

A property on the object that identifies if the object has children. Only needed in itemLoadMode=server In lazy loaded hierarchical grids levels, the child level items are loaded when the user actually expands this level for the first time. In scenarios where it is known initially that there are children, set this property to the value of the object that identifies whether there are children. If this property is set, the expand collapse icon will be drawn only when the value of this property on the object returns an integer greater than zero. Otherwise, the level will always draw the expand collapse icon.


To summarize, In client mode, the grid will assume that the children of items at this level are pre-fetched. In server mode, the grid will dispatch a ITEM_LOAD event (itemLoad) that should be used to construct an appropriate query to be sent to the back-end, to retrieve the child objects at this level. Once the results are retrieved, please call the "setChildData" method on the grid to set the results at this level. Please note, the "childrenField" is still required at this level, because that is where the setChildData method persists the loaded children. Future itemOpen events do not result in itemLoads because the data for this particular entity has already been loaded and persisted.


So, lets take a quick look at what this example looks like:


1) Java Version : http://flexicious.com:8400/HtmlTreeGridSpring/

2) PHP Version : http://flexicious.com:8081/php-sql-demo-app


The source code for this example can be downloaded from :

1) Javahttp://www.htmltreegrid.com/demo/javasample.zip

2) PHP: http://www.htmltreegrid.com/demo/phpsample.zip


Below is the code for this example (client side only) - the server side code is included in the above zip file for you to inspect:


/**

* This Example demonstrates the next level of lazy load capabilities of the Flexicious DataGrid, that is each level of detail, as well as paging at each level in a lazy loaded configuration.

* There are two properties to pay attention to here, both of which are defined at the column level:

* FilterPageSortMode : The Filter/Page/Sort Mode. Can be either "server" or "client". In client mode, the grid will take care of paging, sorting and filtering once the dataprovider is set. Inserver mode, the grid will fire a filterPageSortChange event that should be used to construct an appropriate query to be sent to the backend.

* ItemLoadMode : The Item Load Mode. Can be either "server" or "client". In client mode, the grid will assume that the children of items at this level are prefreched. In server mode, the grid willdispatch a ITEM_LOAD event (itemLoad) that should be used to construct an appropriate query to be sent to the backend, to retrieve the child objects at this level. Once the results are retrieved,please call the "setChildData" method on the grid to set the results at this level. Please note, the "childrenField" is still required at this level, because that is where the setChildData methodpersists the loaded children.

* Future itemOpen events do not result in itemLoads because the data for this particular entity has already been loaded and persisted.

*

* In this example, we see how to wire up a partially lazy loaded Hierarchical DataGrid. That is, we load up the top level with no children records, and then lazy load them in as the user clicks onexpand.

* Please note, it is not advisable to set enableDrillDown on lazy loaded grids, because this will result in too many server calls being issued.

*/

var GridConFig = window.GridConFig = {

   //ApiCallBaseUrl : "http://localhost:63343/php-sql-demo-app/api/sever_records/", // - php call

   ApiCallBaseUrl : window.location+"api/server_records/"//- java call

   XmlConfig : {

       sampleGrid: '<grid ' +

                       'height="100%" ' +

                       'width="100%" ' +

                       'enablePrint="true" ' +

                       'enableExport="true" ' +

                       'forcePagerRow="true" ' +

                       'enableFilters="true" ' +

                       'pagerRowHeight = "35" ' +

                                               'enablePreferencePersistence="true" '+

                       'rowHeight = "30" ' +

                       'pageSize = "15" '+

                       'pageIndex = "1" '+

                       'horizontalScrollPolicy="auto" ' +

                       'selectionColor="transparent" ' +

                       'showSpinnerOnFilterPageSort="true" ' +

                       'enableDrillDown = "true" ' +

                       'nestIndent="36" ' +

                       'filterPageSortMode="server" ' +

                       'enableDefaultDisclosureIcon="false" '+

                       'enablePaging="true" ' +

                       'pagerRenderer="flexiciousNmsp.CustomPagerRenderer" ' +

                       'multiSortRenderer="flexiciousNmsp.CustomMultiColumnSortPopupRenderer" '+

                       'filterPageSortChange="filerPageSortHandle" '+

                       'enableMultiColumnSort="true" ' +

                       'enableColumnHeaderOperation="true" ' +

                       'selectionMode="multipleRows"> ' +

                           '<level name="Top Level" headerHeight="30" childrenField="children" itemLoad="itemLoadHandler"  itemLoadMode="server" childrenCountField="childCounts">' +

                               '<columns>' +

                                   '<column dataField="record_type" width="200" headerText="Record Type" ' +

                                           'enableHierarchicalNestIndent="true" paddingLeft="25" enableExpandCollapseIcon="true"  filterControl="MultiSelectComboBox" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" filterComboBoxDataProvider= "eval__getFilterComboBoxDP_RecordType()" />' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="record" width="350" headerText="Record"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="site" width="100" headerText="Site"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="system" width="100" headerText="System"/>' +

                                   '<column filterControl="DateComboBox"  dataField="start_time"  width="350" headerText="Start Time" labelFunction="startTimeLabelFunction"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="run_time"  width="100" headerText="Run Time" labelFunction="runTimeLabelFunction"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="status" headerRenderer="CustomHeaderRender"  width="100" headerText="status" labelFunction="statusLabelFunction"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="jenkins" width="100" headerText="jenkins" />' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="result"  width="100" headerText="result"/>' +

                                                                       '<column filterControl="DateComboBox"  dataField="start_time"  width="350" headerText="Start Time" labelFunction="startTimeLabelFunction"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="run_time"  width="100" headerText="Run Time" labelFunction="runTimeLabelFunction"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="status" headerRenderer="CustomHeaderRender"  width="100" headerText="status" labelFunction="statusLabelFunction"/>' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="jenkins" width="100" headerText="jenkins" />' +

                                   '<column filterControl="TextInput" filterOperation="Contains" filterTriggerEvent="enterKeyUpOrFocusOut" dataField="result"  width="100" headerText="result"/>' +

                               '</columns>' +

                               '<nextLevel>' +

                                   '<level name="Second Level" nestIndent="36" headerHeight="35" reusePreviousLevelColumns="true" rowHeight="35" childrenField="children" filterVisible="false" />' +

                               '</nextLevel>' +

                           '</level>' +

                       '</grid>'

   }

};

/**

// grid callbacks

//These have lookup based filters. Since at any time, we only load the top level filter, we need to query the database for all possible values for this pickers.

//This is not a problem with filterPageSortMode=client, because we load up the entire dataset and run a distinct on it to figure out the values for the picker.

//However with server based filterPageSortMode, we need to query the server to get the entire list of possible values to pick from. In this case we are just hardcoding the list

//look at the Dot.Net example to show how to load it from server and wire up in the return call.

*/

function getFilterComboBoxDP_RecordType(){

        return [    

       {label:'Batch',data:'batch'},

               {label:'Profile',data:'profile'},        

       {label:'Testcase',data:'testcase'}    

   ];

  }


/**

* The item load handler receives a filterPageSortChangeEvent, which contains a parentObject property that represents the item being opened.

* We basically issue a server request for the children of that time, and in the result event, call the setChildData method passing in the parent item, the children,

* the level at which the parent item exists, and the total number of records that we have, if it is different than the length of the children collection (i.e.) if we are just pushing down a singlepage of data.  

* This handler basically calls out to the services layer, gets the data, and calls the setChildData method on the grid on result.

**/

var itemLoadHandler = function(event){

   var parentData = event.filter.parentObject;

   $.ajax({

       url : GridConFig.ApiCallBaseUrl,

       data : {name : "child_data", parent_id : parentData.id},

       type : "GET",

       success : function(res){

           var response = JSON.parse(res);

           if(response.success){

               var grid = event.target;

               grid.setChildData(parentData, response.data, event.filter.level.getParentLevel());

           } else {

               alert(response.message);

           }

       }

   });

};

/**

The filterPageSortChange Event: You have to wire up the "filterPageSortChange" event. This is the event that get dispatched when the user user clicks on the sort header  on any of the columns,   orrequest a change using either the page navigation buttons or the page navigation drop down in the toolbar, or runs a filter with in any of the columns. This event has 2 properties that are ofinterest:

event.filter: This object contains all the information that you would potentially need to construct a SQL statement on the backend. Full documentation on this object can be found here:  

http://www.flexicious.com/resources/docs29/com/flexicious/grids/filters/Filter.html

event.cause - This can be one of the three values:

public static const FILTER_CHANGE:String = filterChange

public static const PAGE_CHANGE:String = pageChange

public static const SORT_CHANGE:String = sortChange

**/

var filerPageSortHandle = function(event){

  setTimeout(function () {

      var filterPageSort = {};

      var grid = event.target;

      if(event.cause == "pageChange"){

          filterPageSort.pageIndex = event.triggerEvent.currentTarget._pageIndex;

      } else{

          filterPageSort.pageIndex = grid.getColumnLevel()._pageIndex;

      }

      filterPageSort.pageSize = grid.getPageSize();

      var sorts = event.target.getCurrentSorts();

      if(sorts && sorts.length){

          filterPageSort.sorts = [];

          for(var i = 0; i <  sorts.length; i++){

              filterPageSort.sorts.push({

                  sortColumn : sorts[i].sortColumn,

                  isAscending : sorts[i].isAscending,

                  sortNumeric : sorts[i].sortNumeric

              });

          }

      }

      var filter = event.target.getRootFilter();

      if(filter.filterExpressions && filter.filterExpressions.length){

          filterPageSort.filters = [];

          for(var i = 0; i <  filter.filterExpressions.length; i++){

              filterPageSort.filters.push({

                  columnName : filter.filterExpressions[i].columnName,

                  expression : filter.filterExpressions[i].expression,

                  filterOperation : filter.filterExpressions[i].filterOperation,

                  filterComparisonType : filter.filterExpressions[i].filterComparisionType

              });

          }

      }


      $.ajax({

          url : GridConFig.ApiCallBaseUrl,

          data : {name : "top_data", filterPageSort : JSON.stringify(filterPageSort)},

          type : "GET",

          success : function(res){

              if(res.trimLeft().indexOf("<") == 0){

                  grid.hideSpinner();

                  alert("Error occur while loading the data.");

                  return;

              }

              var response = JSON.parse(res);

              if(response.success){

                  grid.setDataProvider(response.data);

                  if(event.cause == "filterChange")

                      grid.setTotalRecords(response.details.totalRecords);

              }else{

                  alert(data);

              }

          }

      });

  },1);

};


var runTimeLabelFunction  = function(data, col){

   var totalSec = Math.round(data["run_time"]/1000);

   var hours = parseInt( totalSec / 3600 ) % 24;

   var minutes = parseInt( totalSec / 60 ) % 60;

   var seconds = totalSec % 60;


   return (hours < 10 ? "0" + hours : hours) + ":" + (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds  < 10 ? "0" + seconds : seconds);

};


var startTimeLabelFunction = function(data, col){

   var start_date = data["start_time"];

   return new Date(Date.parse(start_date)).toString();

};


var statusLabelFunction=function(data, col){

   var status = data["status"];

   if(status.toLowerCase() == "stopped")

       return "<span style='color: #ff4545; font-weight: bold'>Stopped</span>";

   else if(status.toLowerCase() == "running")

       return "<span style='color: #3434FF; font-weight: bold'>Running</span>";

   else if(status.toLowerCase() == "passed")

       return "<span style='color: #458F00; font-weight: bold'>Passed</span>";

   else if(status.toLowerCase() == "failed")

       return "<span style='color: #FF0000; font-weight: bold'>Failed</span>";

   return "";

};



On the server side, the key class to note is the FilterBuilder class


This class is responsible for taking the Filter object and build the filter from it.