Skip to Content

Demystifying the art of component reuse in SAPUI5

This is the second part of my blog series around component reuse. These are the contents of the blog series

  1. Demystifying the art of component reuse in SAPUI5
  2. Component Reuse – 4 ways of sharing data between the apps
  3. Simplifying the component reuse to work both on premise and in cloud and using the manifest.json

 

One of the things I’ve always found being really complex was component reuse. This means to use one app inside another. In this blog series, I will try my best to demystify this, as it really isn’t that hard at all.

First of all, why would you even want to do this? Well let me try to explain. As you probably know, developers are lazy and if we can reuse previous generated code for other apps, not only is it smarter, because we only change in one place, but also much more effective of our precious lazy time. As an example, I am working for a customer right now, where the search helps we use in the app are other independent apps and also we are building extensions to My Inbox workflow items, that will be consisting of about 6 different apps, one for each tab in the app. Sounds complicated right, but hopefully by the end of this blog series, you’ll realise that it really isn’t.

I’ve been wanting to write this blog series for quite a while, so here is the contents of the blog series:

  1. Demystifying the art of component reuse in SAPUI5
  2. Component Reuse – 4 ways of sharing data between the apps
  3. Simplifying the component reuse to work both on prem and in cloud and using the manifest.json
  4. Taking your component reuse to a whole new level!

 

My first blog will be a basic example on how to use one app within another and also a library. A library is a set of custom made controls or javascript files, that makes sense to share among multiple apps to create a modular application landscape. There is already a good blog about this stuff, but i thought i’d try to simplify and explain in a bit more detail of the important parts of this.

First I have created two independent apps using the SAPUI5 application template. One named MyParentApp and the other MyChildApp (Classy right!)

MyChildApp doesn’t really have anything special, but I’ve added a view with a hard coded text for now.

<mvc:View controllerName="bourneMyChildApp.controller.View1" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true" xmlns="sap.m">
	<App>
		<pages>
			<Page title="{i18n>title}">
				<content>
					<Text text="World" />
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

Now deploy our first app to SAP Cloud Platform, otherwise this won’t work. Also keep in mind that for your reuse applications, you need to deploy your changes to see them, WebIde can’t pick them up from an undeployed app.

MyParentApp also has a simple view with a similar text and then I’ve added a component container. When you read the documentation about component reuse, you will realise that you can’t initialise another component without a component container.

<mvc:View controllerName="bourneMyParentApp.controller.View1" xmlns:core="sap.ui.core" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true" xmlns="sap.m">
	<App>
		<pages>
			<Page title="{i18n>title}">
				<content>
					<Text text="Hello"/>
					<core:ComponentContainer width="100%" 
						name="bourneMyChildApp" 
						component="bourneMyChildApp" />
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

Now the magic happens in two specific files, when you are working in SAP Cloud Platform (SCP), the component.js file and the neo-app.json file.

The component.js file is initialised and this is where we declare the path and namespace for our child application. This is done with two lines of code:

jQuery.sap.registerModulePath("bourneMyChildApp", "../../mychildapp/");
			
jQuery.sap.require("bourneMyChildApp.Component");

The first registers the namespace and name of my child application and the path to the app. The second initialises the component.

The namespace and name of my child application is also what you add in the component container of your view, see a pattern here? 😉

Now for the neo-app.json file, the way SCP works is really via this file, you use it already for pointing odata calls through your cloud connector to your backend system. Now we have to do a similar thing, so when SCP sees that particular URL pattern, it will be routed to where we point it to.

In my example it is the path for /mychildapp/ that is pointing to an application deployed on SCP named mychildapp.

 {
      "path": "/mychildapp/",
      "target": {
        "type": "application",
        "name": "mychildapp"
      },
      "description": "My Child App"
    }

That is it, when you run the app, you should see something similar to this. 

Neat right!

 

Alright now for our second course of actions, which is slightly more tricky, how to use a shared library. For more info on that topic, have a look at this blog.

The idea for a shared library is that if you have custom controls or maybe a formatter or other shared functions, you can store them in a shared library that can be used by multiple apps.

In my example I will show both usecases.

Firstly I have added another textfield to my view in the parent app, where I want to use a formatter.

<mvc:View controllerName="bourneMyParentApp.controller.View1" xmlns:core="sap.ui.core" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true" xmlns="sap.m">
	<App>
		<pages>
			<Page title="{i18n>title}">
				<content>
					<VBox>
					<Text id="myNumber" text="{ path: '/MyNumber', 
						formatter: '.sharedFormatter.numberUnit' }" />
					<Text text="Hello"/>
					</VBox>
					<core:ComponentContainer width="100%" 
						name="bourneMyChildApp" 
						component="bourneMyChildApp" />
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

Secondly I have initialised a jsonmodel that I am binding my field to. I have added a new jsonmodel to my parent application and added the following data to the model

	var oModel = this.getModel(),
				oJsonData = {
					MyNumber: 100000.1234
				};
			oModel.setData(oJsonData);

Now in the view controller is where i declare my formatter.

sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"mysharedlibrary/SharedFormatter"
], function(Controller, SharedFormatter) {
	"use strict";

	return Controller.extend("bourneMyParentApp.controller.View1", {
		sharedFormatter: SharedFormatter
	});
});

This is done by the mysharedlibrary/SharedFormatter to declare the path to th efile and then I initialise the javascript file in the  sharedFormatter: SharedFormatter line.

Now we need to make SCP recognise that path, we do this again in the neo-app.json file.

{
      "path": "/resources/mysharedlibrary/",
      "target": {
        "type": "application",
        "name": "mysharedlibrary",
        "entryPath": "/"
      },
      "description": "My shared library"
    }

Please notice that we need to use the resources path here, as SAPUI5 automatically will look in that folder to try and find our library. Also we need to set the entrypath of our library to the root. I’ll explain why later.

Alright, to extend the example I will now add a custom control into the child applications view.

For that we declare the namespace in the XML view, similar to how you do it normally, and then you refer to that namespace, when declaring your control

<mvc:View controllerName="bourneMyChildApp.controller.View1" 
	xmlns:bourne="mysharedlibrary" 
	xmlns:html="http://www.w3.org/1999/xhtml" 
	xmlns:mvc="sap.ui.core.mvc"
	displayBlock="true" xmlns="sap.m">
	<App>
		<pages>
			<Page title="{i18n>title}">
				<content>
					<Text text="World" />
					<bourne:ProductRating />
				</content>
			</Page>
		</pages>
	</App>
</mvc:View>

Also add the following to the neo-app.json file in the child component

{
      "path": "/resources/mysharedlibrary/",
      "target": {
        "type": "application",
        "name": "mysharedlibrary",
        "entryPath": "/"
      },
      "description": "My shared library"
    }

REMEMBER to redeploy your application. Otherwise you won’t see the changes.

Now for the library. Create a new folder in your workspace and name it “MySharedLibrary”.

Copy a neo-app.json file from your parent app as well as a manifest.json file to the folder.

Remove all references except the vanilla ones in the neo-app.json file, so it looks like this:

{
  "routes": [
    {
      "path": "/resources",
      "target": {
        "type": "service",
        "name": "sapui5",
        "entryPath": "/resources"
      },
      "description": "SAPUI5 Resources"
    },
    {
      "path": "/test-resources",
      "target": {
        "type": "service",
        "name": "sapui5",
        "entryPath": "/test-resources"
      },
      "description": "SAPUI5 Test Resources"
    }
  ]
}

 

Now for the manifest.json file, we need change the type to be of “library” instead of “application”

{
	"_version": "1.7.0",
	"sap.app": {
		"id": "mysharedlibrary",
		"type": "library",
		"i18n": "i18n/i18n.properties",
		"applicationVersion": {
			"version": "1.0.0"
		},
		"title": "{{appTitle}}",
		"description": "{{appDescription}}",
		"sourceTemplate": {
			"id": "ui5template.basicSAPUI5ApplicationProject",
			"version": "1.40.12"
		}
	},
	"sap.ui": {
		"technology": "UI5",
		"icons": {
			"icon": "",
			"favIcon": "",
			"phone": "",
			"phone@2": "",
			"tablet": "",
			"tablet@2": ""
		},
		"deviceTypes": {
			"desktop": true,
			"tablet": true,
			"phone": true
		},
		"supportedThemes": ["sap_hcb", "sap_belize"]
	},
	"sap.ui5": {
		"dependencies": {
			"minUI5Version": "1.30.0",
			"libs": {
				"sap.ui.core": {},
				"sap.m": {},
				"sap.ui.layout": {},
				"sap.ushell": {},
				"sap.collaboration": {},
				"sap.ui.comp": {},
				"sap.uxap": {}
			}
		},
		"contentDensities": {
			"compact": true,
			"cozy": true
		},
		"models": {
			"i18n": {
				"type": "sap.ui.model.resource.ResourceModel",
				"settings": {
					"bundleName": "BournemySharedLibrary.i18n.i18n"
				}
			}
		},
		"resources": {
			"css": [{
				"uri": "css/style.css"
			}]
		}
	}
}

 

Everything else is also vanilla.

Now create we need to create the library file, which tells SAPUI5 that this is a library. So create a .library file

Copy in the following

<?xml version="1.0" encoding="UTF-8" ?>
<library xmlns="http://www.sap.com/sap.ui.library.xsd" >

  <name>sap.ui.unified</name>
  <vendor>SAP SE</vendor>
  <copyright>${copyright}</copyright>
  <version>${version}</version>
  
  <documentation>Unified controls intended for both, mobile and desktop scenarios</documentation>
  
  <dependencies>
	<dependency>
	  <libraryName>sap.ui.core</libraryName>
	</dependency>
  </dependencies>
  
  <appData>
	<selenium xmlns="http://www.sap.com/ui5/buildext/selenium" package="com.sap.ui5.selenium.unified" />
	<!-- excludes for the JSCoverage -->
	<jscoverage xmlns="http://www.sap.com/ui5/buildext/jscoverage" >
	  <exclude name="sap.ui.unified.js." />
	</jscoverage>
	<documentation xmlns="http://www.sap.com/ui5/buildext/documentation"
		indexUrl="../../../../test-resources/sap/ui/unified/demokit/docuindex.json"
		resolve="lib" />
  </appData>

</library>

 

Next create a library.js file and load the following

/* global my:true */

sap.ui.define([
		"jquery.sap.global",
		"sap/ui/core/library"
	], // library dependency
	function(jQuery) {

		"use strict";

		sap.ui.getCore().initLibrary({
			name: "mysharedlibrary",
			version: "1.0.0",
			dependencies: ["sap.ui.core"],
			types: [],
			interfaces: [],
			controls: [
				"mysharedlibrary.ProductRating"
			],
			elements: []
		});

		return my.custom.control;

	}, /* bExport= */ false);

In here, we keep a reference to our ProductRating custom control, the formatter is independently referenced.

Create a ProductRating.js file and add the following:

sap.ui.define([
	"sap/ui/core/Control",
	"sap/m/RatingIndicator",
	"sap/m/Label",
	"sap/m/Button"

], function (Control, RatingIndicator, Label, Button) {
	"use strict";
	return Control.extend("mysharedlibrary.ProductRating", {
		metadata : {
			properties : {
				value: 	{type : "float", defaultValue : 0}
			},
			aggregations : {
				_rating : {type : "sap.m.RatingIndicator", multiple: false, visibility : "hidden"},
				_label : {type : "sap.m.Label", multiple: false, visibility : "hidden"},
				_button : {type : "sap.m.Button", multiple: false, visibility : "hidden"}
			},
			events : {
				change : {
					parameters : {
						value : {type : "int"}
					}
				}
			}
		},
		init : function () {
			this.setAggregation("_rating", new RatingIndicator({
				value: this.getValue(),
				iconSize: "2rem",
				visualMode: "Half",
				liveChange: this._onRate.bind(this)
			}));
			this.setAggregation("_label", new Label({
				text: "{i18n>productRatingLabelInitial}"
			}).addStyleClass("sapUiTinyMargin"));
			this.setAggregation("_button", new Button({
				text: "{i18n>productRatingButton}",
				press: this._onSubmit.bind(this)
			}));
		},

		setValue: function (iValue) {
			this.setProperty("value", iValue, true);
			this.getAggregation("_rating").setValue(iValue);
		},

		_onRate : function (oEvent) {
			var oRessourceBundle = this.getModel("i18n").getResourceBundle();
			var fValue = oEvent.getParameter("value");

			this.setValue(fValue);

			this.getAggregation("_label").setText(oRessourceBundle.getText("productRatingLabelIndicator", [fValue, oEvent.getSource().getMaxValue()]));
			this.getAggregation("_label").setDesign("Bold");
		},

		_onSubmit : function (oEvent) {
			var oResourceBundle = this.getModel("i18n").getResourceBundle();

			this.getAggregation("_rating").setEnabled(false);
			this.getAggregation("_label").setText(oResourceBundle.getText("productRatingLabelFinal"));
			this.getAggregation("_button").setEnabled(false);
			this.fireEvent("change", {
				value: this.getValue()
			});
		},
		renderer : function (oRM, oControl) {
			oRM.write("<div");
			oRM.writeControlData(oControl);
			oRM.addClass("myAppDemoWTProductRating");
			oRM.writeClasses();
			oRM.write(">");
			oRM.renderControl(oControl.getAggregation("_rating"));
			oRM.renderControl(oControl.getAggregation("_label"));
			oRM.renderControl(oControl.getAggregation("_button"));
			oRM.write("</div>");
		}
	});
});

 

And lastly create a SharedFormatter.js file and paste the following:

sap.ui.define(["sap/ui/core/format/DateFormat"], function(DateFormat) {

	"use strict";

	return {

		/**
		 * Rounds the number unit value to 2 digits
		 * @public
		 * @param {string} sValue the number string to be rounded
		 * @returns {string} sValue with 2 digits rounded
		 */
		numberUnit: function(sValue) {
			if (!sValue) {
				return "";
			}
			return sValue.toLocaleString();
		}
	};

});

 

Now deploy your application to SCP and try and test your parent application.

Hopefully you get a similar result to this:

 

It isn’t pretty, but that wasn’t the intention. Hopefully this gives you the necessary explanation of how you do component reuse.

So to summarise, the important factors is

  • your jquery calls to register your nested component.
  • Your neo-app.json file to get SCP to recognise the URL pattern and point to seperate applications
  • Component container inside your parent app, to nest another application
  • The library.js and .library file as well as declaring a library as type in the manifest.json file of a library.

Please give feedback on the blog or post questions to me via twitter on @uxkjaer

My next blog will be about how we can now share data between our applications, because right now they are running independently of each other.

 

30 Comments
You must be Logged on to comment or reply to a post.
  • A very comprehesive blog on an important topic. Thanks Jakob! In my opinion component re-use is rather under-documented and not often discussed. A key point is that as well as eliminating code duplication it leads to a more consistent user experience too.

  • Great ! 

    But how to load different components dynamically without listed all the paths of possible components in the neo-app.json file?

    Because if new reusable component developed, we can get the component info like its name from back-end service, but the front-end need to add the path to the neo-app.json and redeployment. So that maybe hard for maintaining with front-end.

  • Hi Jakob ,

    I did similar but when i tied to run the application from Fiori Launchpad its giving Error.  Fiori Launchpad is generating wrong URL.  There is a small change in ” registerModulePath “.

    jQuery.sap.registerModulePath(“bourneMyChildApp”, “../mychildapp/”);

    But over all nice blog …. 🙂

     

    Regards ,

    Meghal Shah

  • Hello Jakob,

    I am now doing an intership in a company and totally new in sapui5. My supervisor told me that I need to learn more about modularization and I have already read many others articles from the interent. However, they are all too differicult for me to understand, because most of them just mentioned about the concept and didn’t show the step cleary, and it’s let me feel so frustrated. After reading your blogs, I feel the idea reusing view is much more clear to me. However, I still have many problems about the coding part, could you send me the source code from the above program? My email is k.lam@tu-bs.de

    Many many thanks!!

    Regards,

    Cora

  • Hello Jakob,

    I really want to thank you for the effort in doing this blog, this clears many of the questions I had, just one doubt, if I want to do the same with openui5 deployed in an apache server, what would be the replacement of the neo-app.json in this kind of app’s to reference the another app.

    Thank you again.

    Jhon Jairo.

  • Hi Jakob,

    Many thanks for such a detailed blog.

    I was able to create a reusable library on SCP using the steps mentioned in this blog. I am facing couple of issues. Appreciate if you can give some insights.

    1) Reusable library does not work when I specify exact UI5 version in parent app’s index.html. e.g. src=”https://sapui5.hana.ondemand.com/1.52.9/resources/sap-ui-core.js”

    2) How can this library be used for apps deployed on SCP’s portal?

    Thanks,
    Fazal.

     

  • Hi All,

    i am able to display childApp(UI5 App) inside the ParentApp(UI5 App),

     

    But when i try the same with Web Content Widget(ChildApp) type Application i am getting the below error

    Kindly Help 🙂

     

    Regards,

    Navya

  • Hi

     

    Thanks for a comprehensive blog.

    When deploying mySharedLibrary to the ABAP repository I am getting error “Unknown project guidelines”. Am I missing something?

     

    Thanks and kind regards

     

  • Hi Jacob, I´m trying to create an app like yours, but the component of child app is being read from “<server>/sap/bc/zbsp_app/Component.js”, so no file is found.

    I changed the app to use componentUsages in manifest.js of parent app, and the error persists. The child component is being read from “<server>/sap/bc/ui5_ui5/ui2/ushell/resources/~20180316143600~/NAME/SPACE/Component.js”

     

    Any idea of how to read from

    “<server>/sap/bc/ui5_ui5/sap/zbsp_app/~/Component.js” ?

     

    the thread is  https://answers.sap.com/questions/668485/ui5-failed-to-load-componentjs-while-using-compone.html

  • Hi Jakob

    I tried running the MyParentApp by following the instructions upto the point . I should have got something similar to the screenshot.

     

    But what I get is a popup saying the app could not be displayed. The error log is quite long. Here is some portion of it that might help us understand the error.

    Can you please help me out with what might be wrong here.

  • Hello Jacob,

    Please I need your help with something, I`ve already made the call to an app inside another app, but I’m troubleshooting with routing in the child app, every time I open the child app, goes directly to the Empty page (Empty.view.xml). You know how to resolve this?, The app runs well independently.

    Thank you in advanced, and I appreciate any help.

    Best regards.

    Jhon Jairo.

  • Hi Jacob,

    We have our UI project integrated into a JAVA application and that java application is deployed on SCP. I am creating a dashboard kind of application which can use components of different applications which are present in different folders within the java app. How can I access these components if the UI is not deployed on SCP and neither on ABAP stack? (I suppose neo-app.json doesn’t work in this scenario)

    Also, how to instantiate the components on click events through the controller of the dashboard app?

  • Hello Jakob,

    Thank you for the blog.

    I am trying to use “NW_APS_NTE_LIB” Notes for Application Object (ReuseLibrary) in one of the standard Fiori app by extending it. As you mentioned, I have added this reuse library in extended app component.js and neo-app.json files.

    (function() {
    	jQuery.sap.registerModulePath("sap.nw.core.gbt.notes.lib.reuse", 
    "/sap/bc/ui5_ui5/sap/nw_aps_nte_lib/sap/nw/core/gbt/notes/lib/reuse");
    }());

     

     {
          "path": "/webapp/resources/sap/nw/core/gbt/notes/lib/reuse",
          "target": {
            "type": "destination",
            "name": "IIO",
            "entryPath": "/sap/bc/ui5_ui5/sap/nw_aps_nte_lib/sap/nw/core/gbt/notes/lib/reuse",
            "preferLocal": true
          },
          "description": "sap/nw/core/gbt/notes/lib/reuse Reuse Library"
        },

    Now, I am trying to bind the file from reuse library to the extended app. Can you please help me out to bind the reuse library to the extended app.

    <core:FragmentDefinition xmlns:core="sap.ui.core" xmlns="sap.m" xmlns:form="sap.ui.layout.form" xmlns:l="sap.ui.layout" xmlns:ca="sap.ca.ui"
    	xmlns:html="http://www.w3.org/1999/xhtml">
    	<!-- This extension point can be used to add fields to items -->
    	<IconTabFilter xmlns="sap.m" id="notes" key="onNotes" icon="sap-icon://notes" text="{i18n&gt;NOTES_TITLE}" tooltip="{i18n&gt;NOTES_TITLE}">
    						<content>
    						</content>
    					</IconTabFilter>
    </core:FragmentDefinition>

     

    Thank you.

     

  • Hi

     

    Your blogs helped to implement a solution that really needed re-use, so I just want to appreciate you and appreciate the effort you put in making these blogs.

     

    Thank you