Hierarchical Data - Lazy Load - Java and PHP Sample

Parent Previous Next

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) Java: http://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. In server 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 will dispatch 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 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.

*

* 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 on expand.

* 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 ApiCallBaseUrl = "http://localhost:63343/php-sql-demo-app/api/sever_records/", // - php call

var ApiCallBaseUrl = window.location + "api/server_records/", //- java call


/**

// 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 hard-coding the list

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

*/

var getFilterComboBoxDP_RecordType = function() {

   return [

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

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

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

   ];

}


class Example extends React.Component {


   constructor() {

       super();


       this.startTimeLabelFunction = this.startTimeLabelFunction.bind(this);

       this.runTimeLabelFunction = this.runTimeLabelFunction.bind(this);

       this.filerPageSortHandle = this.filerPageSortHandle.bind(this);

       this.statusLabelFunction = this.statusLabelFunction.bind(this);

       this.itemLoadHandler = this.itemLoadHandler.bind(this);

   }


   /**

    * 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 single page of data.  

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

    **/

   itemLoadHandler(event) {

       var parentData = event.filter.parentObject;

       $.ajax({

           url: 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,   or request 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 of interest:

   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

   **/

   filerPageSortHandle(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: 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);

   };


   runTimeLabelFunction(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);

   };


   startTimeLabelFunction(data, col) {

       var start_date = data["start_time"];

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

   };


   statusLabelFunction(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 "";

   };


   render() {

       return (

           <ReactDataGrid 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} enablePaging={true}

               filterPageSortMode={"server"} enableDefaultDisclosureIcon={false} selectionMode={"multipleRows"}

               pagerRenderer={flexiciousNmsp.CustomPagerRenderer} enableMultiColumnSort={true} enableColumnHeaderOperation={true}

               multiSortRenderer={flexiciousNmsp.CustomMultiColumnSortPopupRenderer} filterPageSortChange={this.filerPageSortHandle}>

               <ReactDataGridColumnLevel name={"Top Level"} headerHeight={30} childrenField={"children"} childrenCountField={"childCounts"}

                   itemLoad={this.itemLoadHandler} itemLoadMode={"server"}>

                   <ReactDataGridColumn dataField={"record_type"} width={200} headerText={"Record Type"} paddingLeft={25}

                       enableExpandCollapseIcon={true} enableHierarchicalNestIndent={true} filterControl={MultiSelectComboBox}

                       filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       filterComboBoxDataProvider={getFilterComboBoxDP_RecordType()} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"record"} width={350} headerText={"Record"} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"site"} width={100} headerText={"Site"} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"system"} width={100} headerText={"System"} />

                   <ReactDataGridColumn filterControl={DateComboBox} dataField={"start_time"} width={350} headerText={"Start Time"}

                       labelFunction={this.startTimeLabelFunction} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"run_time"} width={100} headerText={"Run Time"} labelFunction={this.runTimeLabelFunction} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"status"} headerRenderer={CustomHeaderRender} width={100} headerText={"status"}

                       labelFunction={this.statusLabelFunction} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"jenkins"} width={100} headerText={"jenkins"} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"result"} width={100} headerText={"result"} />

                   <ReactDataGridColumn filterControl={DateComboBox} dataField={"start_time"} width={350} headerText={"Start Time"}

                       labelFunction={this.startTimeLabelFunction} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"run_time"} width={100} headerText={"Run Time"} labelFunction={this.runTimeLabelFunction} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"status"} headerRenderer={CustomHeaderRender} width={100} headerText={status}

                       labelFunction={this.statusLabelFunction} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"jenkins"} width={100} headerText={"jenkins"} />

                   <ReactDataGridColumn filterControl={TextInput} filterOperation={"Contains"} filterTriggerEvent={enterKeyUpOrFocusOut}

                       dataField={"result"} width={100} headerText={"result"} />

                   <ReactDataGridColumnLevel name={"Second Level"} nestIndent={36} headerHeight={35} reusePreviousLevelColumns={true}

                       rowHeight={35} childrenField={"children"} filterVisible={false} />

               </ReactDataGridColumnLevel>

           </ReactDataGrid>

       );

   }

}


ReactDOM.render(<Example />, document.getElementByTag('app'));


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.