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
kashif_bashir
Explorer
0 Kudos

super! 

very nice article.

:smile:

fredericorocha
Explorer
0 Kudos

Good article! Thanks for sharing

Former Member
0 Kudos
Could I add tree table in master view of splitapp & click on each names to navigate data in detail view using local Json
MikeDoyle
Active Contributor
0 Kudos
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.
Viswanath
Participant
0 Kudos
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

 
MikeDoyle
Active Contributor

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.

 

MikeDoyle
Active Contributor
0 Kudos
There is now a 'TreeList' control, Kaushik.  It's called sap.m.Tree and it's available from UI5 version 1.42.
Viswanath
Participant
0 Kudos
Hi Mike,

Thanks for Sharing

Regards,

Viswanath
0 Kudos

Very Informative.
Thanks Mike.

0 Kudos

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! ?

MikeDoyle
Active Contributor
0 Kudos
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.

 
former_member193947
Participant
0 Kudos
Excellent blog Mike. Will be definitely helpful for those who try to implement treetable.

I am trying to implement one and facing issues with events, where no events of sap.m.tree is working. We are on 1.44.11 library. Have you tried to implement event like selectionChange ? Since sap.ui.commons library is depreciated, we will need to go with sap.m library.

Appreciate your views on this.

Thanks.
MikeDoyle
Active Contributor
0 Kudos
Hi there SAP Seeker, I'm glad you found the blog useful.

I haven't had any problems with the events myself.  First of all I would double check your code where you register your event handlers.  Second I would try to debug the standard code to see where the events are raised.

I assume you aren't using your tree with an aggregation binding (i.e. having many trees)?  That can sometimes cause issues with events.

 
Former Member
0 Kudos
Hello, Thank you for sharing this solution. I have a question about the tree table(in a gantt) using your transformation routine. I am unable to set a filter...it's like my path for a field of the table wasn't good. Can you tell me how I sould implement a filter for a field of the tree structure. I'm really stuck with this thing, and I don't understand why.

Thank you again.

Laurent
0 Kudos
Hi  Mike,

can it work for below scenario like



















































Europe
Austria
Belgium
Germany
France
Greece
Italy
Luxembourg
Netherlands
United Kingdom
Europe South
Italy

Here Italy is present in 2 parent nodes Europe as well as Europe South.

Regards,

Raju.

 
MikeDoyle
Active Contributor
0 Kudos
Hi Raju, the nodes have to be unique, so your two Italy nodes would have to have different IDs.
MikeDoyle
Active Contributor
0 Kudos
Hi Laurent, I haven't tried a filter on a treetable.  I'm sure you know that a filter is applied to the context.  The context sits between the model and the UI control.

There is a filter method which you can see here:

https://sapui5.hana.ondemand.com/#/api/sap.ui.table.Table/methods/filter

If you're still having trouble I suggest you raise a question on SAP community with some details of exactly what you are doing.

 

 
deep_desai2
Participant
0 Kudos
Hi Mike,

I am working on a tree table app and able to display the tree table and do operations on top of the data. Basically the tree table is a budgetary table and end users would change the budget values for the table which should then be saved on the backend.

How should i save the tree structure data back to the ABAP system. I will need to convert the tree data back to flat structure ? If yes, how should i do it?

Regards

Deep

 
MikeDoyle
Active Contributor
0 Kudos
Hi Deep,

You should be able to use the nodeMap for this.  Remember that the nodeMap has a reference to each of the nodes in the tree.  When the node values are updated (if the input fields are bound to the JSON model) then the new value will be available via the nodeMap too.  You can loop through the properties of the nodeMap object to prepare your updates.

Mike

 

 
former_member214651
Active Contributor
0 Kudos
Hi Mike,

Thanks for the wonderful article. I am able to display the content as a tree table from a flat file as a JSON output in view. However, my requirement now is to allow users to add / remove entries within the tree table.

Please could you let me know how to achieve this functionality?

Regards,

Poojith
MikeDoyle
Active Contributor
0 Kudos
Hi Poojith,

Glad you found the blog useful.

You should try setting the mode property of the Tree to Delete.  As with a regular list, a delete icon appears on each line.  The delete event on the Tree will be raised when they click the icon.

If you want some kind of add child button on each line item you could use the sap.m.CustomTreeItem control instead of sap.m.StandardTreeItem.

If you change the JSON model data the tree will update, although you may need to trigger an explicit update of the binding.

Mike

 

 

 
former_member214651
Active Contributor
0 Kudos
Hi Mike,

Thanks for the response. I will try the approach and let you know the outcome.

Regards,

Poojith
former_member214651
Active Contributor
0 Kudos
Hi Mike,

I tried performing some changes and was able to delete records from the treetable successfully. However, adding new rows is still a problem:(

Please could you help on this? Is there a way wherein I can loop thru the JSON model bound to the treetable and add a new record?

Regards,

Poojith M V
MikeDoyle
Active Contributor
0 Kudos
Hi Poojith, why not post a new question? Add some more details and post a link to it here....

 
former_member214651
Active Contributor
0 Kudos
Hi Mike,

Thanks for the reply. I have created a new question in SDN https://answers.sap.com/questions/740941/add-new-rows-in-sap-ui5-treetable.html.

Regards,

Poojith
former_member214651
Active Contributor
0 Kudos
Hi Mike,
I have created a question with the required details. I have managed to achieve 1 part of the scenario.
However, there is a new requirement which I am trying to realize which has some errors. I have attached my code which I am using in the reply.

Please could you help with the corrections?

Regards,
Poojith
manish_9_gupta
Discoverer
0 Kudos
Hi Mike,

My oData returns me the below output. I tried your code for converting this to tree table format but it's not working. Can you please help me?



Regards

Manish Gupta

 
MikeDoyle
Active Contributor
0 Kudos
Hi Manish, do you have a field which has the ID of the parent node?  It would be easier to see if you paste the output as JSON, rather than a screenshot.
manish_9_gupta
Discoverer
0 Kudos
Dear Mike,

Greetings.

Thanks for your reply. I checked the result but i don't think there is any such parent node. Below is he output of oData in JSON format.

Looking forward to hear from you. Thanks.

 

{
"d": {
"results": [{
"__metadata": {
"id": "https://URL,
"uri": "https://URL",
"type": "{SericeName}.{Entity Set}"
},
"ResulltCol1": "10650625_FLM_0004",
"User": "SUDESH JAITIYA",
"ID": " 1"
}, {
"__metadata": {
"id": "https://URL,
"uri": "https://URL",
"type": "{SericeName}.{Entity Set}"
},
"ResulltCol1": "10650625_FLM_0004",
"User": "GOVIND KUMAR",
"ID": " 2"
}, {
"__metadata": {
"id": "https://URL,
"uri": "https://URL",
"type": "{SericeName}.{Entity Set}"
},
"ResulltCol1": "10650625_FLM_0004",
"User": "SUDESH JAITIYA",
"ID": " 3"
}, {
"__metadata": {
"id": "https://URL,
"uri": "https://URL",
"type": "{SericeName}.{Entity Set}"
},
"ResulltCol1": "10650625_FLM_0004",
"User": "SRIRAM SHARMA",
"ID": " 4"
}, {
"__metadata": {
"id": "https://URL,
"uri": "https://URL",
"type": "{SericeName}.{Entity Set}"
},
"ResulltCol1": "10650625_FLM_0004",
"User": "SUDESH JAITIYA",
"ID": " 5"
}]
}
}

Regards

Manish
MikeDoyle
Active Contributor
0 Kudos
To use my approach you need another property which contains (if applicable) the ID of the parent node.  The distinguishing feature of a tree is that all nodes below the very top level have a parent. If you want instead to lazy load your tree (making a call each time a node is expanded) then the approach I describe here isn't suitable.
khloud_1
Discoverer
0 Kudos
Thanks a lot for your solution.
sanjoy0308
Active Participant
0 Kudos
Hi mike.doyle4 I appreciate for your solution.
Ahmet
Explorer
0 Kudos

Hi mike.doyle4 thank you very very much for your solution.

Labels in this area