Skip to Content
Technical Articles

UI5 Composite TreeTable Control with auto row count to optimize space

Introduction

In case you ever used the tree table you might have faced the similar behavior as I’m going explain you in this blog post. When you add a TreeTable into your UI5 app and you want to make it automatically fit to the available space, you can set the property “visibleRowCountMode” to “auto”. This will give you a treetable that show as many rows as available space:

When you expand the tree until not all lines are visible anymore on the same page it will automatically provide a scrollbar.

Until here all is fine but as soon as you start adding controls on the same view the treetable is not able to calculate the available space correctly. As you can see in this example, enough space but already a scrollbar:

When you then expand the tree until not all lines are visible anymore on the same page, you’ll get two scrollbars!

 

I don’t like this behavior and searched for another solution that displays the tree in a proper way. I have to say I went a bit out of control here. It started with improving visualization of the rows in the tree in reusable way and ended up with a Composite TreeTable control that handles much more than just the visualization ?

 

Solution

For solving the main problem (visualization of rows without having multiple scrollbars) I started by counting the number of rows that are expanded and bound it to the property “visibleRowCount”. I also changed the visibleRowCountMode to “Fixed”. This will only show the amount of rows that are actually being used! When collapsed I only show the collapsed rows. When a row is expanded, I recalculate and so on. In the end I’m just playing with the visible amount of rows and not the available space. When all space is used, automatically the page will show a scrollbar.

As lazy as I am, I only want to do this once and be able to reuse this in other views or even apps. So, I decided to wrap this logic into a Composite control.

By doing this, I realized I could wrap more functionalities that can be valuable for every treetable into this Composite control. I expanded the composite control with Collapse all, Collapse selection, expand first level and Expand selection. These functionalities can be applied on every treetable. Why implementing this in every app again and not just bundling into one composite control? I even pre-enabled the drag and drop functionality in the composite tree control. It will react when a row is being moved to another row and trigger a move event which can be handled by the controller of the view that uses the control.

Last but not least, I also added an edit mode to the composite control that comes with an “Add” and “Delete” button. These buttons will simply raise an event that can be handled by the controller of the view that is using the composite tree control.

 

Result:

A Treetable with out-of-the-box buttons that are relevant to any treetable:

When the treetable is expanded, it will only show the amount of lines that are needed. If it needs more space than available it will show a scrollbar:

And even with other controls in the view, it will still show only one scrollbar ?

How to use

The Composite Tree Control can be used like any other UI5 Control. Define your custom namespace in the view:

xmlns:cust="be.wl.CompositeControlExample.control"

 

Use the composite tree control in your view, add the columns with the template and bind the rows to your model:

<cust:TreeWithButtons id="TreeTableBasic" rows="{path:'/catalog/clothing', parameters: {arrayNames:['categories']}}" editable="true" add=".onAddLine" delete=".onDeleteLine" move=".onMoveRow" selectionMode="MultiToggle" enableSelectAll="false" ariaLabelledBy="title">
    <cust:columns>
        <t:Column width="13rem">
            <Label text="Categories"/>
            <t:template>
                <Text text="{name}" wrapping="false"/>
            </t:template>
        </t:Column>
        <t:Column width="9rem">
            <Label text="Price"/>
            <t:template>
                <u:Currency value="{amount}" currency="{currency}"/>
            </t:template>
        </t:Column>
        <t:Column width="11rem">
            <Label text="Size"/>
            <t:template>
                <Select items="{path: '/sizes', templateShareable: true}" selectedKey="{size}" visible="{= !!${size}}" forceSelection="false">
                    <core:Item key="{key}" text="{value}"/>
                </Select>
            </t:template>
        </t:Column>
    </cust:columns>
</cust:TreeWithButtons>>

 

Provide the eventhandlers in the controller to handle the events “add”, “delete” and “move”:

onAddLine: function(oEvent) {
		var treeModel = this.getView().getModel();
		var path = oEvent.getParameter("selectedPath");
		var rowData = treeModel.getProperty(path) || [];
		rowData.push({});
		treeModel.setProperty(path, rowData);
	},
	onDeleteLine: function(oEvent) {
		var selectedRowContext = oEvent.getParameter("selectedContext");
		if (selectedRowContext) {
			this._removeRow(selectedRowContext);
		}
	},
	_removeRow: function(RemoveRowContext) {
		var treeModel = this.getView().getModel();
		var path = RemoveRowContext.getPath().split("/");
		var lastone = parseInt(path[path.length - 1], 10);
		path.pop();
		path = path.join("/");
		var updateDraggedParentItems = treeModel.getProperty(path).filter(function(item, index) {
			return index !== lastone;
		});
		treeModel.setProperty(path, updateDraggedParentItems);
	},
	onMoveRow: function(oEvent) {
		var aDraggedRowContexts = oEvent.getParameter("draggedRowContexts");
		var draggedRowIndex = oEvent.getParameter("draggedRowIndex");
		var oNewTargetContext = oEvent.getParameter("newTargetContext");
		var treeModel = this.getView().getModel();
		if (aDraggedRowContexts.length === 0 || !oNewTargetContext) {
			return;
		}
		var oNewTarget = oNewTargetContext.getProperty();
		for (var i = 0; i < aDraggedRowContexts.length; i++) {
			if (oNewTargetContext.getPath().indexOf(aDraggedRowContexts[i].getPath()) === 0) {
				// Avoid moving a node into one of its child nodes.
				continue;
			}
			var targetPath = oNewTargetContext.getPath().split("/");
			targetPath.pop();
			targetPath = targetPath.join("/");
			var updateTargetItems = treeModel.getProperty(targetPath).reduce(function(items, item, index) {
				//add existing line unless it is the dragged row that was already on the same level
				if (item !== aDraggedRowContexts[i].getProperty()) {
					items.push(item);
				}
				//add dragged row after the place it has been dropped
				if (item === oNewTargetContext.getObject()) {
					items.push(aDraggedRowContexts[i].getProperty());
				}
				return items;
			}, []);
			this._removeRow(aDraggedRowContexts[i]);
			treeModel.setProperty(targetPath, updateTargetItems);
		}
	}

 

How it works

A composite control in general is a control that exists out of other UI5 controls. In this case it is a control that contains a predefined TreeTable control with built-in logic.

The fragment of the composite control contains already the treetable with predefined buttons to handle treetable related actions. The columns are not defined to make it reusable for other views as well. Every time the treetable will be used, it could be with different columns that are forwarded from the view that is using the composite tree table. The composite control will forward the aggregation to the original treetable.

<core:FragmentDefinition
    xmlns:core="sap.ui.core"
    xmlns:mvc="sap.ui.core.mvc" displayBlock="true"
    xmlns="sap.m"
    xmlns:t="sap.ui.table"
    xmlns:dnd="sap.ui.core.dnd"
    xmlns:u="sap.ui.unified">
    <t:TreeTable id="innerTreeTable" visibleRowCountMode="Fixed" selectionMode="Single" toggleOpenState=".onToggleRow" visibleRowCount="{$this>/rowCount}" enableSelectAll="false" ariaLabelledBy="title">
        <t:extension>
            <OverflowToolbar>
                <Title id="title" text="Clothing"/>
                <ToolbarSpacer/>
                <Button text="Add" press=".onAdd" visible="{$this>/editable}"/>
                <Button text="Delete" press=".onDelete" visible="{$this>/editable}"/>
                <Button text="Collapse all" press=".onCollapseAll"/>
                <Button text="Collapse selection" press=".onCollapseSelection"/>
                <Button text="Expand first level" press=".onExpandFirstLevel"/>
                <Button text="Expand selection" press=".onExpandSelection"/>
            </OverflowToolbar>
        </t:extension>
        <t:dragDropConfig>
            <dnd:DragDropInfo enabled="{$this>/editable}" sourceAggregation="rows" targetAggregation="rows" dragStart=".onDragStart" drop=".onDrop"/>
        </t:dragDropConfig>
    </t:TreeTable>
</core:FragmentDefinition>

 

I had some help of Andreas Kunz to make the “Row” aggregation work. I had to add “forwardBinding: true” in the definition to make it work. Thank you for this Andreas Kunz !

The full definition of the control with the implementation of all the functions can be found here:

https://github.com/lemaiwo/CompositeTreeTableControl/blob/master/webapp/control/TreeWithButtons.js

You can have a look and I’m pretty sure you’ll understand how it works.

The full project can be found here: https://github.com/lemaiwo/CompositeTreeTableControl

Hope it can be valuable for you as well!

 

 

 

4 Comments
You must be Logged on to comment or reply to a post.
  • Thanks for sharing.

    Have you tried contacting SAP about this behaviour?

    It doesn't sounds like the expected one (Or maybe you should use a different layout for this to work correctly)

  • I had no idea you could do this. Thanks.

    As there seem to be some tree table experts around..

    I'm using a tree table with OData binding and tree table annotations in my XML view. Works fine.

    But OData tree bindig is only available for OData v2. Has anybody an idea how to make it work for OData v4?

    My second problem is 500 errors as soon as I add children compositions to my tree entity.

    entity Locations: {
    ...
    parent : association to Locations;
    children : composition of many Locations on children.parent=$self;

    I see no reason why this isn't possible.