Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
MikeDoyle
Active Contributor

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



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



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

 


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" }
]
}

33 Comments
Labels in this area