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.
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;
}
});