Skip to Content
Author's profile photo Jakob Marius Kjær

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.

 

Assigned Tags

      28 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Mike Doyle
      Mike Doyle

      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.

      Author's profile photo Arjun Biswas
      Arjun Biswas

      Very nice blog. Thanks !

      Author's profile photo Frank Liu
      Frank Liu

      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.

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      You will always need to handle it in the neo-app.json file for SCP content. However for backend is a different story. Look out for my blog next week on how to handle that in the manifest.json file

      Author's profile photo Frank Liu
      Frank Liu

      Thanks !  The problem seems do not exist If the apps deployed to ABAP Server.  Looking forward your next blog!

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      It's ready now

      Author's profile photo MEGHAL SHAH
      MEGHAL SHAH

      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

      Author's profile photo Former Member
      Former Member

      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

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      Hi Cora,

       

      Please have a look here.

      https://bitbucket.org/kjaerj/sharing_data

      Author's profile photo Former Member
      Former Member

      Hi Jakob,

      Thanks so much! I'm now reading the code. It's really nice.

      Regards,

      Cora

      Author's profile photo Jhon Jairo Teran Salazar
      Jhon Jairo Teran Salazar

      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.

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      Sorry for not getting gnback to you earlier. You should be able to do something similar to on the abap stack. So you need to declare the paths as they are hosted on the Apache server. Then it should be fine.

      Author's profile photo Jhon Jairo Teran Salazar
      Jhon Jairo Teran Salazar

      Thank you Jakob!, simple :).

      Author's profile photo Fazal Ilahi
      Fazal Ilahi

      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.

       

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      Hi fazal, I had the same issues. As I recall you need to declare the name space specifically for the library in the resource path.

      Author's profile photo Navya Shree
      Navya Shree

      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

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      IT would be your resourcepath that needs to tweaking or your neo-app.json file.

      RIght now you go two sub directories up.

      Author's profile photo Timothy Muchena
      Timothy Muchena

      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

       

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      Have a look at this blog and see if you done it accordningly. That error doesn't ring a bell

      https://blogs.sap.com/2018/08/16/how-sap-web-ide-supports-development-of-sap-fiori-reusable-libraries/

      Author's profile photo Timothy Muchena
      Timothy Muchena

      Hi

       

      It worked thank you

       

      Kind regards

      Author's profile photo Cristiano Marques
      Cristiano Marques

      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

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      Check Christian s reply. That should help you. You need the modulepath registered either in the manifest or in the beginning of your component.js file.

      Author's profile photo Manu Maheshwar Puthiyadath
      Manu Maheshwar Puthiyadath

      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.

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      I think something has gone wrong in your path decleration when you registered the app on the index page or maybe you have an error on your XML.

      Author's profile photo Jhon Jairo Teran Salazar
      Jhon Jairo Teran Salazar

      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.

      Author's profile photo Jakob Marius Kjær
      Jakob Marius Kjær
      Blog Post Author

      Hi Jhon,

       

      That would be the pattern of your routhing that makes it navigate to the not found page.

      Author's profile photo Kodandaram S
      Kodandaram S

      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.

       

      Author's profile photo Timothy Muchena
      Timothy Muchena

      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