This blog post describes how to implement tree-like hierarchy using Office UI Fabric React (OUIFR) Grouped DetailsList component.
We'll be working with a hierarchy similar to the one displayed on the image below:
TL;DR
Office UI Fabric React (OUIFR) DetailsList ignores (doesn't render) items for the group if it (the group) has subgroups. As a result for a tree-like hierarchy - some of the items could be lost.Jump to solution.
The Problem
We need to display a hierarchical grid, or grouped grid where each node (group) can have child nodes as well as leaf items.For the hierarchy from the picture above we want to see something like this:

So, we have a root level node code that contains leaf item index.ts and two subgroups with their own items: components and webparts.
As we're developing SharePoint Framework solution (of course - this blog is about O365 and SharePoint =)) it would be great to use OUIFR DetailsList component to display grouped grid with a header.
Initial Implementation
Initial implementation for our component will be pretty simple.We need to define our leaf item interface:
export interface IItem {
key: string;
title: string;
}
export interface INode extends IItem {
items?: IItem[];
children?: INode[];
}
private readonly _nodes: INode[] = [{
key: 'code',
title: 'code',
items: [{
key: 'index',
title: 'index.ts'
}],
children: [{
key: 'components',
title: 'components',
items: [{
key: 'component',
title: 'Component.tsx'
}]
}, {
key: 'webparts',
title: 'webparts',
items: [{
key: 'scss',
title: 'WebPart.module.scss'
}, {
key: 'ts',
title: 'WebPart.ts'
}]
}]
}];
Let's assume that the hierarchy is passed in the props of our component:
export interface IOuifrGroupedDetailsListProps {
nodes: INode[];
}
/**
* Gets flat items array and groups array based on the hierarchy from the props
*/
private _getItemsAndGroups = (props: IOuifrGroupedDetailsListProps): void => {
const nodes = props.nodes;
const items: IItem[] = [];
const groups: IGroup[] = [];
// processing all the nodes recursively
this._processNodes(nodes, groups, items, 0);
// setting the state
this.setState({
groups: groups,
items: items
});
}
/**
* Recursively process hierarchy's nodes to build groups and add items to the flat array
*/
private _processNodes = (nodeItems: INode[] | undefined, groups: IGroup[], items: IItem[], level: number): void => {
// end of recursion
if (!nodeItems || !nodeItems.length) {
return;
}
// processing current level of the tree
nodeItems.forEach(nodeItem => {
const newGroup: IGroup = {
key: nodeItem.key,
name: nodeItem.title,
startIndex: items.length,
count: 0,
children: [],
level: level, // level is incremented on each call of the recursion
data: nodeItem // storing initial INode instance in the group's data
};
groups.push(newGroup);
if (nodeItem.items && nodeItem.items.length) {
// adding items to the flat array
items.push(...nodeItem.items);
}
// processing child nodes
this._processNodes(nodeItem.children, newGroup.children!, items, level + 1);
// current group count is a sum of group's leaf items and leaf items in all child nodes
newGroup.count = items.length - newGroup.startIndex;
});
}
public render(): React.ReactElement<IOuifrGroupedDetailsListProps> {
const {
items,
groups
} = this.state;
return (
<div className={styles.ouifrGroupedDetailsList}>
<DetailsList
columns={this._columns}
items={items || []}
groups={groups}
/>
</div>
);
}

The problem here is we're missing index.ts leaf item in the code group. So, it's kinda data loss situation from user perspective.
The reason for that, as mentioned above, is DetailsList component ignores group's items if there are subgroups.
The Solution
The solution here consists of two parts.First, we'll add "fake" subgroup and place all the missing leaf items in it. For our sample we'll add fake subgroup to code group and move index.ts into it:
private _processNodes = (nodeItems: INode[] | undefined, groups: IGroup[], items: IItem[], level: number): void => {
// ...
// processing current level of the tree
nodeItems.forEach(nodeItem => {
// ...
groups.push(newGroup);
if (nodeItem.items && nodeItem.items.length) {
// adding fake group with no data
if (nodeItem.children && nodeItem.children.length) {
newGroup.children!.push({
key: `${nodeItem.key}-fake`,
name: '',
startIndex: items.length,
count: nodeItem.items.length,
level: level
});
}
// adding items to the flat array
items.push(...nodeItem.items);
}
// ...
});
}
Now our component looks like that:

The index.ts item is there, but, as expected, it is displayed as a child of fake group.
The second step of the solution is to override rendering of group's header and hide the header for any fake group.
As we know, fake groups don't have data assigned, so we can use it to determine if the group is fake.
And we can override group's header rendering using groupProps property of the DetailsList:
public render(): React.ReactElement<IOuifrGroupedDetailsListProps> {
// ...
return (
<div className={ styles.ouifrGroupedDetailsList }>
<DetailsList
/* ... */
groupProps={{
onRenderHeader: this._onRenderGroupHeader,
isAllGroupsCollapsed: groups ? groups.filter(gr => !gr.isCollapsed).length === 0 : true,
collapseAllVisibility: CollapseAllVisibility.visible
}}
/>
</div>
);
}
private _onRenderGroupHeader = (props: IDetailsGroupDividerProps, _defaultRender?: IRenderFunction<IDetailsGroupDividerProps>): JSX.Element => {
// for fake groups - return empty element
if (!props.group!.data) {
return <></>;
}
// default rendering for "real" groups
return _defaultRender(props);
}
You can find full sample code for this post here.
That's all for today!
Have fun!
Comments