Skip to Content
Technical Articles

Hierarchy Selection in Tree Table

Hi community,

in our project we had a problem and on the SC various questions talk about the topic without final solution.

In this blog post I’m exposing a custom selection of hierarchies node in a tree table. The standard tree table does not allow the selection of child nodes automatically after the selection of their parent.

For our “project” I used the sample demo of JSONTreeBinding:

https://sapui5.hana.ondemand.com/#/entity/sap.ui.table.TreeTable/sample/sap.ui.table.sample.TreeTable.JSONTreeBinding

e.g.: If you select the checkbox in node Women we expect that Clothing, Jewelry, Handbags, Shoes and all sub nodes will be checked. But this is not how standard works.

Then I’ve created a GitHub repo with the new solution for resolve this case. The base of project is a copy of TreeTable sample with JsonTreeBinding.

Let’s start :

1) Modify base structure and binding

addSelectOnModel: function () {
			var oModel = this.getView().getModel();
			var oData = oModel.getData();
			var aCategories = oData.catalog.clothing.categories;
			var fSetSelect = function (aArray) {
				for (var i = 0; i < aArray.length; i++) {
					aArray[i].selected = false;
					if (aArray[i].categories) fSetSelect(aArray[i].categories);
				}
			}(aCategories);
			oModel.setData(oData);
			return oModel;
		}

 

This function adds the boolean property to all the nodes. I use it when application is initialized.

It’s a recursive function expression. When the loop founds a sub node it’s calling the same function for the node child.

The model is already binding on the view with classic construct on the controller and the xml file:

onInit: function () {
			var that = this;
			var oModel = new JSONModel("model/Clothing.json");
			oModel.attachRequestCompleted(function (oEvent) {
				that.addSelectOnModel();
			});
			this.getView().setModel(oModel);
		}

Of course, we need to add a new column on the table in xml.
It’s a simple column with important event: select (onSelectCheckBoxTreeTable).

<mvc:View controllerName="selectnodetreetable.selectnodetreetable.controller.View" xmlns="sap.ui.table" xmlns:mvc="sap.ui.core.mvc"
	xmlns:m="sap.m" xmlns:u="sap.ui.unified" xmlns:core="sap.ui.core" height="100%">
	<m:Page showHeader="false" enableScrolling="false">
		<m:content>
			<TreeTable id="TreeTableBasic"
				rows="{path:'/catalog/clothing', parameters: {arrayNames:['categories']}, events: { dataReceived: '.onDataReceived'} }" selectionMode="None"
				enableSelectAll="false" ariaLabelledBy="title" class="sapUiMediumMargin">
				<extension>
					<m:OverflowToolbar>
						<m:Title id="title" text="Clothing"/>
						<m:ToolbarSpacer/>
						<m:Button text="Collapse all" press="onCollapseAll"/>
						<m:Button text="Collapse selection" press="onCollapseSelection"/>
						<m:Button text="Expand first level" press="onExpandFirstLevel"/>
						<m:Button text="Expand selection" press="onExpandSelection"/>
					</m:OverflowToolbar>
				</extension>
				<columns>
					<Column width="9rem">
						<template>
							<m:CheckBox selected="{selected}" select="onSelectCheckBoxTreeTable"/>
						</template>
					</Column>
					<Column width="13rem">
						<m:Label text="Categories"/>
						<template>
							<m:Text text="{name}" wrapping="false"/>
						</template>
					</Column>

 

2) onSelectCheckBoxTreeTable: the event where everything started

This event is the core of the solution. When you press the check box the table have to know if you are selecting or unselecting and actuate the hierarchy procedure.

Let’s check the code :

onSelectCheckBoxTreeTable: function (oEvent) {
			var oObject = {};
			var oModel = oEvent.getSource().getParent().getModel();
			oObject.path = oEvent.getSource().getBindingContext().sPath;
			oObject.object = oModel.getObject(oObject.path);
			if (oObject.object.categories !== undefined) {
				//if is not leef
				this.selectedTopDownModel(oObject);
				if (!oObject.object.selected) {
					var sPath = oObject.path;
					this.selectedBottomUpModel(oObject, sPath);
				}
			} else {
				//if is leef
				var sPath = oObject.path;
				this.selectedBottomUpModel(oObject, sPath);
			}
		}

First at all we need the Object and the Path. With the object we know if the node it’s a leaf or not. The object has children depends on the array property categories how corresponding an array of sub nodes.  The same array we have checked in the initialization of model.

The path is useful for the parent node. Because with the path we know all the family tree of that child, and we can apply the logic of hierarchy.

 

3) selectedTopDownModel and selectedBottomUpModel: the functions working behind

Then we have another two important function selectedTopDownModel and selectedBottomUpModel.

The names speak about themselves.

selectedTopDownModel: function (oObject) {
			var bSelected = oObject.object.selected;
			var aElement = oObject.object.categories;
			for (var i = 0; i < aElement.length; i++) {
				if (aElement[i].edit || aElement[i].edit === undefined)
					aElement[i].selected = bSelected;
				if (aElement[i].categories !== undefined) {
					var oElementObject = {
						object: aElement[i]
					};
					this.selectedTopDownModel(oElementObject);
				}
			}
		},
selectedBottomUpModel: function (oObject, sPath) {
			var oModel = this.getView().getModel();
			var aSplitPath = sPath.split("/");
			aSplitPath.pop();
			aSplitPath.pop();
			sPath = aSplitPath.join('/');
			var oParetnObject = oModel.getObject(sPath);
			if (oParetnObject.selected && !oObject.selected) {
				oParetnObject.selected = false;
				this.selectedBottomUpModel(oParetnObject, sPath);
			}
		}

The first one is using (of course) for not leafs node. And It goes deep in the tree for select or unselect the children node. It’s a recursive function and like every time we check all if there are sub nodes on the categories’ property.

The second one is little more complex because we need to work on the string about path.

  1. var aSplitPath = sPath.split(“/”);
  2. aSplitPath.pop();
  3. aSplitPath.pop();
  4. sPath = aSplitPath.join(‘/’);

Those line of code break the string and recreate the node path of father. The function is used only for unselect the father node.
Why is not it uses for select? It’s not used for the select because we don’t know if the “cousins” are selected or not. We can’t choose if father need be select for that. selectedBottomUpModel like the sister It’s a recursive function.

4) Conclusion

I don’t know if it’s the best solution for selection hierarchy on the treetable. But for me it’s working and useful. The community have a lot of questions about this topic.

https://answers.sap.com/questions/12626922/child-selection-on-parent-selection-in-tree-table.html
https://answers.sap.com/questions/12903366/child-selection-when-parent-is-getting-selected-in.html
https://answers.sap.com/questions/11873198/programmatically-multiselect-rows-in-a-treetable.html
https://answers.sap.com/questions/12626922/child-selection-on-parent-selection-in-tree-table.html
https://answers.sap.com/questions/227592/making-rows-of-sapuitabletreetable-selected.html

 

Unfortunately the solution work only with a JSON model, I never collided with the odata model for this issue. In case you have any solution we could discuss together. I’m enthusiastic to work with another exited developers.

 

Regards,

Sebastiano

 

2 Comments
You must be Logged on to comment or reply to a post.
  • Hello!

    Nice article, thanks!
    Could’t you explain, how i can get data from selected indices?

    I used this method before, but now i can’t use .getSelectedIndices()

    getSelectedNames: function () {
                            var aNames= "";
    			var oTable = this.byId("TreeTableBasic");
    			//get indices of selected rows, comma-separated
    			var aIndices = oTable.getSelectedIndices();
    			for(var i=0;i<aIndices.length; i++) {
    				//fetch the data of selected rows by index
    				var tableContext = oTable.getContextByIndex(aIndices[i]);
    				var data = oTable.getModel().getProperty(tableContext.getPath());
    				if(aNames!== ""){
    					aNames= aNames+ "," + data.name;
    				}else{
    					aNames= data.name;	
    				}
    			}
                            return aNames;
    }