Search code examples
kendo-uitelerikkendo-gridodatahierarchical-data

Paged TreeList or collapsible hierarchical Grid


I'm looking to into using the Kendo UI Grid. And it seems very feature complete. However it has one fatal flaw. Their Grid / Hierarchy is just nested grids and not really a hierarchy. Definitely not a recursive or leveled hierarchy.

Luckily they have a TreeList grid which does just that. But it's not nearly as feature complete as the normal Grid. It actually lacks support for paging which makes it completely unusable :(

I read somewhere that they didn't implement paging because when expanding parents then the page would contain one extra item and possibly exceed the page size. Rather weak argument if you ask me... obviously it needs to page by roots.

It's really quite simple. Here's an example using OData

OData Example

GET ~/Products?$filter=ParentId eq null&$expand=Children&$top={pagesize}&$skip={pagenumber}&$count=true

Result

{
    "@odata.count": 33,
    "value": [
        {
            "Id": 1,
            "ParentId": null,
            ...
            "Children": [
                {
                    "Id": 2,
                    "ParentId": 1,
                }
            ]
        },
        ...
    ]
}

Perfect! In other words it's not a limitation of the backend. Though I can see show filtering can get tricky.

Back on topic. I'm looking for advice on how I would implement paging for the TreeList or how I could get the collapsible rows (recursive hierarchy style) in the normal grid. Actually the latter would be preferable as it's more feature complete.


Solution

  • Found an example here: http://www.telerik.com/forums/does-treelist-support-pagination#5VlH4IMBXEuh819pYvyEvQ

    View:

    <kendo-treelist k-options="treeListOptions"></kendo-treelist>
    <kendo-pager k-options="pagerOptions"></kendo-pager>
    

    Controller logic:

    // default filtering and sorting
    var defaultFilter = { field: "ParentId", operator: "eq", value: null };
    var defaultSort = { field: "Name", dir: "asc" };
    
    var myODataSource = new kendo.data.DataSource({
        type: "odata-v4",
        transport: {
            read: {
                url: "/odata/Products",
                data: {
                    "$expand": "Children($levels=max)"
                }
            }
        },
        pageSize: 15,
        serverPaging: true,
        serverSorting: true,
        serverFiltering: true,
        filter: defaultFilter,
        sort: defaultSort,
        schema: {
            parse: function(response) {
                if (myODataSource.transport.options.read.data.$expand == "Parent($levels=max)") {
                    // if "$expand=Parent($levels=max)" then the hierarchy will be reversed Children -> Parent
                    // thus we need to flatten Parents
                    var ary = _.flattenHierarchy(response.value, 'Parent');
                    // and remove duplicate parents coming from different tree branches
                    response.value = _.uniq(ary);
                } else {
                    // if "$expand=Children($levels=max)" then the hierarchy will be as expected Parent -> Children
                    // thus we need to flatten Children
                    response.value = _.flattenHierarchy(response.value, 'Children');
                }
    
                return response;
            }
        },
        change: function(e) {
            treeListDataSource.read();
        }
    });
    
    // filter hack!
    // http://www.telerik.com/forums/any-filtering-event#--cNXsvF5U6zinsTsyL4eg
    var originalFilterFn = kendo.data.TreeListDataSource.fn.filter;
    kendo.data.TreeListDataSource.fn.filter = function (e) {
        if (arguments.length > 0) {
            if (e === null) {
                // if e is null, then the filter is cleared. So we need to filter by roots to get the normal tree
                myODataSource.transport.options.read.data.$expand = "Children($levels=max)";
                myODataSource.filter(defaultFilter);
            } else {
                // else we're filtering and the result nodes need to include parents to maintain the tree hierarchy
                myODataSource.transport.options.read.data.$expand = "Parent($levels=max)";
                myODataSource.filter(e);
            }
        }
    
        return originalFilterFn.apply(this, arguments);
    };
    
    // sort hack!
    var originalSortFn = kendo.data.TreeListDataSource.fn.sort;
    kendo.data.TreeListDataSource.fn.sort = function (e) {
        if (arguments.length > 0) {
            myODataSource.sort(e);
        }
    
        return originalSortFn.apply(this, arguments);
    };
    
    var treeListDataSource = new kendo.data.TreeListDataSource({
        transport: {
            read: function (options) {
                var data = myODataSource.data().toJSON();
                options.success(data);
            }
        },
        sort: defaultSort,
        schema: {
            model: {
                id: "Id",
                fields: {
                    parentId: { field: "ParentId", type: "number", nullable: true },
                    Id: { field: "Id", type: "number" }
                },
                expanded: true
            }
        }
    });
    
    $scope.treeListOptions = {
        autoBind: false,
        dataSource: treeListDataSource,
        filterable: true, //{ mode: "row"}, not supported (yet) by treelist
        sortable: true,
        resizable: true,
        reorderable: true,
        columns: [
            { field: "Name" },
            { field: "Description" }
        ]
    };
    
    $scope.pagerOptions = {
        autoBind: false,
        dataSource: myODataSource,
        info: true,
        pageSizes: [2, 3, 4, 5, 6],
        refresh: true,
    };
    
    myODataSource.read();
    

    Underscore function

    _.mixin({
        flattenHierarchy: function self(objAry, childPropertyName) {
            // default values
            childPropertyName = typeof childPropertyName !== 'undefined' ? childPropertyName : 'children';
    
            var result = [];
            _.each(objAry, function(obj) {
                // the object it self without children
                var strippedObj = _.omit(obj, childPropertyName);
                result.push(strippedObj);
    
                if (obj.hasOwnProperty(childPropertyName) && obj[childPropertyName] !== null) {
                    // child object(s)
                    var children = obj[childPropertyName];
                    if (!_.isArray(children))
                        children = [children];
    
                    result.pushArray(self(children, childPropertyName));
                }
            });
    
            return result;
        }
    });