Skip to Content

I love trees

In my experience it’s very common to need hierarchies in apps, and a tree is a great way to represent them.  Of course, we need to be careful where we use them, especially when it comes to mobile devices with small screens.  We should always have a good user experience in mind.  That said, I’m sure trees will find a place in many UI5 apps.

The problem

You can see the UI5 Treetable control in the Explored section of the UI5 SDK (sap.ui.table.TreeTable).  The sample app maps the control to a JSON model, and the JSON file begins like this

  1. {
  2.   “catalog”: {
  3.   “clothing”: {
  4.   “categories”: [
  5.   {“name”: “Women”, “categories”: [
  6.   {“name”:“Clothing”, “categories”: [
  7.   {“name”: “Dresses”, “categories”: [
  8.   {“name”: “Casual Red Dress”, “amount”: 16.99, “currency”: “EUR”, “size”: “S”},
  9.   {“name”: “Short Black Dress”, “amount”: 47.99, “currency”: “EUR”, “size”: “M”},
  10.   {“name”: “Long Blue Dinner Dress”, “amount”: 103.99, “currency”: “USD”, “size”: “L”}
  11.   ]},
  12.   {“name”: “Tops”, “categories”: [
  13.   {“name”: “Printed Shirt”, “amount”: 24.99, “currency”: “USD”, “size”: “M”},
  14.   {“name”: “Tank Top”, “amount”: 14.99, “currency”: “USD”, “size”: “S”}
  15.   ]},

This is what you might think of as a recursive JavaScript data format.  Each node in the structure can have an attribute called categories which contains an array of child node objects.  Each child node can similarly contain an array of its own children.

This kind of structure is common in the JavaScript world, but not so common in the ABAP world.  If I expand an HR org. structure with function module RH_GET_STRUC (or a PM functional location tree with PM_HIERARCHY_CALL_REMOTE) I will get the data in a flat format.  There will be one table row per node, with each row having an ID, a level, a parent ID and a text.

How can we convert that kind of data to the JSON format you see above?

The solution

Tomasz Mackowski has explained here TreeTable Odata binding how you can map the TreeTable to an OData service.  Today I’d like to show you another approach, which might be simple for you to implement.  It maps the control to a JSON model, just like the sample used in the SDK.  If your hierarchy is small, and you don’t need lazy-loading (i.e. you prefer eager-loading), it might be a good way to go.  Those SAP-standard function modules are quite efficient at returning a few levels of a tree in one go.  A large number of tiny, unbatched OData calls (a consequence of lazy-loading) can be disproportionately ‘expensive’. You may be able to make an asynchronous call to get the tree data before the user reaches the point where the tree is shown, and hence they won’t need to wait before using the tree.

With this approach the OData service can be very simple, returning the tree data in a flat structure of one entity per node.  As you can see, the JavaScript is very simple too.  This function does all the transformation we need, in about 25 lines.

    transformTreeData: function(nodesIn) {

         var nodes = [];          //'deep' object structure
         var nodeMap = {};      //'map', each node is an attribute

         if (nodesIn) {
              var nodeOut;
              var parentId;

              for (var i = 0; i < nodesIn.length; i++) {
                   var nodeIn = nodesIn[i]
                   nodeOut = {      id: nodeIn.ID,
                                            text: nodeIn.Text,
                                            type: nodeIn.Type,
                                            children: [] };


                   parentId = nodeIn.ParentID;

                   if (parentId && parentId.length > 0) {
                        //we have a parent, add the node there
                        //NB because object references are used, changing the node
                        //in the nodeMap changes it in the nodes array too
                        //(we rely on parents always appearing before their children)
                        var parent = nodeMap[nodeIn.ParentID];

                        if (parent) {
                             parent.children.push(nodeOut);
                        }

                   } else {
                        //there is no parent, must be top level
                        nodes.push(nodeOut);
                   }

                   //add the node to the node map, which is a simple 1-level list of all nodes
                   nodeMap[nodeOut.id] = nodeOut;

              }

         }

         return nodes;

    }

The only explanation required (I hope) is around the use of the nodeMap.  Each node must be added to the children array of its parent.  This could be tricky due to the recursive nature of the structure (how do we find the parent?).  To make it simple we create a ‘map’ (in fact just an object) which contains an entry for each node, keyed on the nodeId.  This map contains a reference to the node, not the node itself.  Therefore if we add a node to the parent in the map it is also added in the main structure (nodes).

The function transforms this (an array of simple objects)….

Before.jpg

..to this (a complex, recursive object), which matches the example from the SDK shown above

After.jpg

If we store this object in a JSON model we can map our TreeTable directly to it, and the result looks like this:

 

Result.jpg

Options are good

In many cases you will want your tree to lazy-load and it will be best to map the TreeTable control directly to an OData model.  I hope that this example will prove helpful for the ‘other’ times.  Perhaps the core transformation function will come in handy for whenever you want to use a deep, recursive JavaScript object, even without the TreeTable control.

Appendix: Build the demo app

If you would like to build a demo app, which you can then extend to meet your own requirements then then follow these steps.  To keep things simple we will use a local JSON-format file rather than an external datasource.

  • Log into Web-IDE.  Create a new project from the SAPUI5 Application template.  I used the name TreeTableDemo and the namespace com.scn.demo.treetable.  I left the view details at the default of type=XML and name = View1
  • Add the new functions readFile, transformTreeData and setModelData to Component.js (see example code below)NB Be careful with your commas, you must have one after every function except the last.  Add the following 3 lines to the end of the Init function:
var flatData = this.readFile();
var deepData = this.transformTreeData(flatData);
this.setModelData(deepData);
  • Copy the code sample below into your View1.view.xml. NB You must be sure to include the extra xmlns entries, but don’t alter your controllerName.
  • Create a new file, FlatData.json, in the model folder.  Copy the contents from the sample below.
  • Add this line to your i18n.properties file: treetitle=My Team
  • Right-click on your project and choose Run->Run as->Web Application to launch a preview
  • Change the code, try new things, change the data in the JSON file, add an external datasource……..

 

Component.js

sap.ui.define([
	"sap/ui/core/UIComponent",
		"sap/ui/Device",
		"com/scn/demo/treetable/model/models"
],             function(UIComponent, Device, models) {
		"use strict";

	return UIComponent.extend("com.scn.demo.treetable.Component", {

		metadata: {
			manifest: "json"
		},

		/**
		 * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.

		 * @public
		 * @override
		 */

		init: function() {
			// call the base component's init function
			UIComponent.prototype.init.apply(this, arguments);

			// set the device model
			this.setModel(models.createDeviceModel(), "device");

			var flatData = this.readFile();
			var deepData = this.transformTreeData(flatData);
			this.setModelData(deepData);
		},

		readFile: function() {

			var flatData = null;
			//load the data from the JSON file
			//NB same format as gateway service could be
			var inModel = new sap.ui.model.json.JSONModel();
            		inModel.loadData("/webapp/model/FlatData.json", "", false);
 			
			var data = inModel.getData();

            		if (data) {
				flatData = data.nodes;
                        }

			return flatData; 
		},

		transformTreeData: function(nodesIn) {

			var nodes = [];		//'deep' object structure
            		var nodeMap = {};	//'map', each node is an attribute

			if (nodesIn) {

            			var nodeOut;
            			var parentId;

		            	for (var i = 0; i < nodesIn.length; i++) {
            				var nodeIn = nodesIn[i];
            				nodeOut = { id: 	nodeIn.ID,
            		            		    text:	nodeIn.Text,
           		            		    type:	nodeIn.Type,
            		            		    children: [] };
 	            
			parentId = nodeIn.ParentID;


            		
			if (parentId && parentId.length > 0) {
           					//we have a parent, add the node there
            					//NB because object references are used, changing the node
            					//in the nodeMap changes it in the nodes array too
            					//(we rely on parents always appearing before their children)
            					var parent = nodeMap[nodeIn.ParentID];

            					if (parent) {
							parent.children.push(nodeOut);
            					}
            				} else {
						//there is no parent, must be top level
           					nodes.push(nodeOut);
            				}

			//add the node to the node map, which is a simple 1-level list of all nodes

            				nodeMap[nodeOut.id] = nodeOut;

            			}

            		}
      
        		return nodes;
		},
            
		setModelData: function (nodes) {
        		//store the nodes in the JSON model, so the view can access them
		        var nodesModel = new sap.ui.model.json.JSONModel();
            		nodesModel.setData({nodeRoot: { children: nodes }});
                        this.setModel(nodesModel, "nodeModel");
		}
	});
});

 

View1.view.xml    

<mvc:View xmlns:html="http://www.w3.org/1999/xhtml"
		  xmlns="sap.m" xmlns:table="sap.ui.table" xmlns:mvc="sap.ui.core.mvc"
		  controllerName="com.scn.demo.treetable.controller.View1">
	<App>
		<pages>
			<Page title="{i18n>title}" class="sapUiSizeCompact">
				<content>
					<table:TreeTable	id="TreeTable"
			                    		rows="{path:'nodeModel>/nodeRoot', parameters: {arrayNames:['children']}}"
			                    		enableSelectAll="false"
			                    		expandFirstLevel="true">	
				    	<table:columns>
		                    <table:Column width="13rem">
		                        <Label text="{i18n>treetitle}"/>
		                        <table:template>
		                            <Text text="{nodeModel>text}" />
		                        </table:template>
		                    </table:Column>
	                	</table:columns>
            		</table:TreeTable>
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

 

FlatData.json    

{ "nodes": [
		{ "ID": "O100",		"Text": "Software Development",	"ParentID": "", 		"Type": "O" },
		{ "ID": "O110",		"Text": "Team A",				"ParentID": "O100", 	"Type": "O" },
		{ "ID": "S111",		"Text": "Product Owner",			"ParentID": "O110", 	"Type": "S" },
		{ "ID": "S112",		"Text": "Scrum Master",			"ParentID": "O110", 	"Type": "S" },
		{ "ID": "S113",		"Text": "Team Member",			"ParentID": "O110", 	"Type": "S" },
		{ "ID": "S114",		"Text": "Team Member",			"ParentID": "O110", 	"Type": "S" },
		{ "ID": "S115",		"Text": "Team Member",			"ParentID": "O110", 	"Type": "S" },
		{ "ID": "O120",		"Text": "Team B",				"ParentID": "O100", 	"Type": "O" },
		{ "ID": "S121",		"Text": "Product Owner",			"ParentID": "O120", 	"Type": "S" },
		{ "ID": "S122",		"Text": "Scrum Master",			"ParentID": "O120", 	"Type": "S" },
		{ "ID": "S123",		"Text": "Team Member",			"ParentID": "O120", 	"Type": "S" },
		{ "ID": "S124",		"Text": "Team Member",			"ParentID": "O120", 	"Type": "S" },
		{ "ID": "S125",		"Text": "Team Member",			"ParentID": "O120", 	"Type": "S" },
		{ "ID": "S126",		"Text": "Team Member",			"ParentID": "O120", 	"Type": "S" },
		{ "ID": "S127",		"Text": "Team Member",			"ParentID": "O120", 	"Type": "S" },
		{ "ID": "O130",		"Text": "Support",				"ParentID": "O100", 	"Type": "O" },
		{ "ID": "S131",		"Text": "Team Lead",			        "ParentID": "O130", 	"Type": "S" },
		{ "ID": "S132",		"Text": "Support Analyst",	                "ParentID": "O130", 	"Type": "S" },
		{ "ID": "S133",		"Text": "Support Analyst",	                "ParentID": "O130", 	"Type": "S" },
		{ "ID": "S134",		"Text": "Support Analyst",		        "ParentID": "O130", 	"Type": "S" },
		{ "ID": "S135",		"Text": "Support Analyst",			"ParentID": "O130", 	"Type": "S" }
	]
}
To report this post you need to login first.

11 Comments

You must be Logged on to comment or reply to a post.

    1. Mike Doyle Post author

      Yes, but you would need to have some clickable content in your tree table.  A table row (sap.ui.table.Row) isn’t selectable in the same way that a list item (see sap.m.ListItemBase) is.  For example, the tree table column could contain a Link control.

      I’m not sure if we will ever see a TreeList control.

      (0) 
  1. Viswanath Golakoti

    Hi Mike,

    Good article, Thanks for sharing.

    I am unable to find the attachments of code  you mentioned in code. Could you please share them.

    Thanks,

    Best Regards,

    Viswanath

     

    (0) 
    1. Mike Doyle Post author

      Thanks Viswanath for pointing that out.  I don’t think the new platform supports attachments and they haven’t been migrated.  For that reason I have added the sample code at the end of the blog.

       

      (2) 
  2. Aditya Adhauliya

    Hey Mike , I have read your article thoroughly and I really appreciate how informative you have made it . It’s very helpful.
    I have tried to use your example to create a “Tree” not a tree table .
    I need a little more help.
    Can you clarify if the transformtreedata function will work also if you have data coming in a random order and not in a hierachial order (i.e. Parents may not appear before the children)as opposed to your FlatData.json file .

    e.g. The data could be coming in like this ..
    Countries and Cities
    City 1 in US
    City 2 in US
    City 3 in US
    City 1 in India
    City 2 in India
    City 1 in UK
    City 1 in China 

    US 

    INDIA

    UK

    CHINA

    etc

    And we have to display it like this(Nodes Not Expanded)
    Countries
         US
         UK
         India
         China

     

    Nodes Expanded

    Countries
         US
            City 1
            City 2
            City 3 
         UK
         China                         //Non-expanded nodes(UK & China)
         India 
            City 1
            City 2

    Thanks in Advance ,
    Cheers! 🙂

    (0) 
    1. Mike Doyle Post author

      Hi Aditya, actually this simple version does rely on parents appearing before the children.  That’s because the node is added to it’s parent’s children array and if the parent is missing then, well, we don’t have anywhere to add it.

      Normally the kind of standard function modules I’m targeting here give a level for each node in the hierarchy.  In your example the countries would be level 1 and the cities level 2.  As long as you can sort ascending by the level then that’s enough to guarantee that each parent comes before it’s children.  I would do the sorting in the back-end if I were you.

      This method should work fine with the new sap.m.Tree control.  Judging by the example in the explored app you can replace the property name “children” with “nodes” and omit the arrayNames parameter in the binding.  Use the network tab in the browser dev tools to view the data file Tree.json.

       

      (0) 

Leave a Reply