How To: Component Reuse

One of the things I’ve always found to be really complex is 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 - 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 help we use in the app are other independent apps. We are also we are building extensions to My Inbox workflow items, consisting of about 6 different apps, one for each tab in the app. Sounds complicated, right?! Hopefully by the end of this blog series, you’ll realise that it really isn’t.

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 make sense to share among multiple apps to create a modular application landscape. There is already good blogs about this stuff, but I thought I'd try to simplify and explain in a bit more detail.

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 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. Here we have to do something similar, 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"
   }

When you run the app, you should see something similar to this.

wzcr7myRgBcyY4-Sb0peycdqbSaQuGjuiQ1V6WH3

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, 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);

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 the file. I then initialise the javascript file in the sharedFormatter: SharedFormatter line.

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. We also 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>

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();
    }
  };
});

Deploy your application to SCP and try and test your parent application.

Hopefully you get a similar result to this:

tSJJeV_kTLlCDxHIBr3066Uko70MtbJw-I0A3G81

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 are:

  • Your jquery calls to register your nested component.

  • Your neo-app.json file to get SCP to recognise the URL pattern and point to separate 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.

All source files can be cloned from here

By Jakob Kjær

SAP Technical Architect