Implementing Re-use Components in SAPUI5 libraries and consuming them in SAPUI5 apps

"Don't repeat yourself" – DRY! This is one of the challenges that drives software engineers, no matter in what environment we design or code software. Sometimes UI5 developers think about (custom) controls when asked about "re-use" in UI5. However, controls are not always the best choice; and UI5 has way more to offer. In this article we'll focus on UI components implemented in libraries and on how to consume these components in own apps. We will implement two different Re-use Components which allow to select customers from a simple TableSelectDialog. Everything works with both SAPUI5 and OpenUI5. You will see some new features introduced in UI5 1.50 that make life easier.

Hint: We will not focus on how to implement UI5 libraries. The library used here is available at https://github.com/nzamani/ui5-nabi-demo. It's based on UI5Labs.

Table of contents:

  1. Re-use options in SAPUI5
  2. Re-use Components vs Components in UI5
  3. Scenarios for using UI5 Re-use Components
  4. The library descriptor of nabi.demo (manifest.json)
  5. The Re-use Component selectionBtn
    1. The component descriptor of selectionBtn
    2. Overriding init() inside Component.js
    3. Overriding createContent() inside Component.js
    4. Implementing the CustomerTableSelectDialog Fragment
  6. The Re-use Component selection
    1. The component descriptor of selection
    2. Overriding init() inside Component.js
    3. Overriding createContent() inside Component.js
    4. Implementing the CustomerTableSelectDialog Fragment
  7. Consuming Re-use Components in own apps
    1. Referencing components in the manifest.json
    2. Consuming components in XMLViews (Home.view.xml)
    3. Loading components and handling component events in Controllers (Home.controller.js)
  8. Additional hints
  9. Running the code

1. Re-use options in SAPUI5

When it comes to "re-use" UI5 offers many options. Depending on your use case you can decide which option fits best. The most popular options are:


Controls (i.e. Button.js)
A control is basically something that renders in a view. Controls offer data binding capabilities for the consumer of the control out of the box. However, as a best practice controls never create models and they don't talk with backends (not considering edge cases). Implementing a control once allows to re-use the control instead of repeating code (DRY). In order to share controls with multiple apps controls can be deployed as part of a UI5 library.
Views (i.e. Details.view.xml)
A view contains the controls that shall be rendered. Typically, the data binding for the controls is declared here. However, as a best practice views never create models, i.e. instantiating a model in a JSView is discouraged (breaks the idea of MVC). Just like controls, views can be re-used. Views are often needed only in one specific app and thus they are often part of that single app. However, in some cases you might want to share views across different apps. Again, to achieve this you can implement your view in a UI5 library from which everyone can seamlessly re-use your view.
Fragments (i.e. CreateProductDialog.fragment.xml)
Same as views, see above. Usually, fragments are only used in the app for which they are designed. Sharing them in libraries is possible.
Controllers (i.e. Detail.controller.js)
Controllers belong to views, thus they are loaded together with the view. A controller can be used by multiple views. However, each view gets an own instance of the controller. JavaScript inheritance is another way of reusing code inside controllers, i.e. think of the BaseController paradigm in UI5. Sharing controllers via UI5 libraries allows better re-use across multiple UI5 apps. For example, in case you have multiple apps that need exactly the same BaseController (or parts of it) you might want to put the BaseController into a UI5 library.
Modules (i.e. formatter.js)
JavaScript utility functions, UI5 formatters, and the UI5 mockserver.js are well known examples for modules. If needed, they can also be shared across multiple apps as part of a UI5 library. For example, if you use a given set of formatters in multiple apps you can easily put a formatter.js module into one of your shared UI5 libraries.
Non-UI5 Libraries (i.e. D3, jQuery,…)
UI5 comes with some popular JavaScript libraries which can be used as needed. It's possible to integrate additional JavaScript libraries into UI5 libraries or into own apps. As part of a library, they are typically used in the library itself. However, they can also be used by (external) consumers of the library.
UI5 Libraries (i.e. nabi.m)
UI5 libraries can be considered as containers for different kinds of re-usable UI5 artefacts (and even for non-ui5 artefacts). UI5 libraries are a first class option for sharing UI5 code/artefacts.
UI5 Components
As a best practice UI5 apps should be implemented as so called component based apps. A component based app extends sap.ui.core.UIComponent, which in turn extends sap.ui.core.Component. sap.ui.core.Component is used in cases where the component does not render anything. sap.ui.core.UIComponent is used in cases where the component renders something. Components have a manifest.json file, often simply called descriptor. More precisely, depending on what kind of component we are talking about, the manifest.json is called app descriptor, library descriptor, or component descriptor. In contrast to controls, views, fragments, etc. a component is allowed to create models and work directly with a backend. By doing so, the component abstracts the backend communication and thus the consumer of a component does not need to know much about the backend APIs.
Class hierarchy for Component/UIComponent (astah UML file)
Class hierarchy for Component/UIComponent

2. Re-use Components vs Components in UI5

First of all, a Re-use Component is a Component (i.e. something that extends sap.ui.core.Component or sap.ui.core.UIComponent). As already mentioned, it's a good practice (and sometimes a must) to implement your UI5 apps as so called component based apps. Sure, that means it's technically possible to consider each component based app as something that you can re-use in other apps. For example, you could create a custom UI5 app (probably a component based app) which in turn integrates multiple other component based apps as a whole. A real-life example of this approach is (of course) the SAP Fiori Launchpad, which incorporates component based UI5 apps. Another real-life example is the good old Explored App shipped as a part of the demo kit (recently made available under Samples in the demo kit). The Explored/Samples App is basically a Master-Detail app which displays other UI5 component based apps (for example) in a certain area. Of course, you could create your own app that incorporates other component based UI5 apps, too.

In contrast, Re-use Components are typically not full featured apps. They are more like small chunks of features (UI and/or backend) with high cohesion which can and should be used to compose other UI5 apps or components. They allow you to create new apps more efficiently. The term Re-use Component is not communicated well or maybe not even at all. However, I've been using this term for quite some time now and I'm not the one who invented it. It seems whenever developers talk about that term they have an implicit but not complete understanding of it. I hope this incomplete definition helps at least a little for clarification. If not, by the end of this tutorial you should be able to see the big picture…


3. Scenarios for using UI5 Re-use Components

As a rule of thumb, every time you duplicate code it's a good idea to think about re-use. This get's even more important in environments where multiple apps are developed. Here are a few explicit examples giving you an idea when exactly a Re-use Component fits perfectly:

1. Selection Dialogs
Typically triggered by an implicit or explicit action, i.e. pressing a button, an icon, or some other control. The dialog is often implemented by using a sap.m.TableSelectDialog which offers a search field to find an entry in a simple table/list. In a given environment the data comes typically always from the same backend service. Examples are selecting customers, products, suppliers, invoices, sales orders, employees,... Instead of designing and implementing (for example) a fragment including data binding, search, and backend communication multiple times in multiple apps we could implement a Re-use Component which encapsulates everything: one consistent UI/UX, no code duplication, encapsulates all required backend calls, offers a clean Re-use Component API, single source of truth, increased maintainability, increased dev efficiency, etc.
2. Quick Views
The SAPUI5 sap.m.QuickView control is used to display a popover containing additional information of a given object/entity. Examples are QuickViews for products, employees, companies, suppliers, business partners in general,… Think of a detail page listing all orders of a given customer while the customer is visible in the object header. In order to see the contact details of the customer an in-place QuickView to display the contact data (phone number, address etc.) can be used. All these cases are perfect examples for Re-use Components because they are often needed in different kinds of apps. Again, implementing and sharing such QuickViews using Re-use Components all the benefits mentioned in the previous example apply.
3. Master Lists
You might be surprised, but the master list of a Master-Detail app can also be a good candidate for Re-use Components. Think of Master-Detail apps in which you select a customer from the master list and then see or maintain
  • the contacts of the customer (maybe from your CRM)
  • the complaints of the customer
  • the orders of the customer
  • and so on…
Although you might have multiple apps to implement the features mentioned above all the apps have at least one thing in common: a master list that allows to select a customer from a list of customers received from the same backend service. In case you have master lists displaying different ObjectNumbers etc. you can consider this during the implementations of your Re-use Component (i.e. by making it customizable via public APIs which you as the developer of the component decide to offer).

There are way more good and most probably even better examples. Check your app ecosystem; the one who searches will find. Let's continue with the code now.


4. The library descriptor of nabi.demo (manifest.json)

The components implemented in this article are part of a UI5 library called nabi.demo which is available on Github at https://github.com/nzamani/ui5-nabi-demo (including additional instructions).

manifest.json of nabi.demo (/src/nabi/demo/manifest.json)
{
    "_version": "1.9.0",
    "sap.app": {
        "id": "nabi.demo",
        "type": "library",
        "embeds":[
            "comp/reuse/northwind/customer/selection",
            "comp/reuse/northwind/customer/selectionBtn"
        ],
        "applicationVersion":{
            "version":"0.2.0"
        },
        "title":"Nabi Demo Library",
        "description":"Nabi Demo Library by nabisoft",
        "openSourceComponents": [],
        "resources": "resources.json",
        "offline": true
    },

    "sap.ui": { //... },
	
    "sap.ui5": { //... }
}

As you can see sap.app.type is library; nothing new so far. In sap.app.embeds we define the relative paths to the components embedded into the library. As you can see our library has two components. Both implement a customer selection dialog in different ways using the Northwind OData service (we'll use the v2.ODataModel for the Northwind service later in the corresponding component descriptors):

1. comp/reuse/northwind/customer/selectionBtn
Renders a button which opens the customer selection dialog. The user can then select one customer from the list of customers. The component offers an API to get the selected customer: the event customerSelected.
2. comp/reuse/northwind/customer/selection
Can be configured to render with or without a button. If rendered without a button then the consumer app must trigger the selection dialog to open, i.e. by offering an own button. This Re-use Component illustrates how you can offer some kind of flexibility for different consumers.

5. The Re-use Component selectionBtn

The metadata configuration of this Re-use Component looks as follows:

var Component = UIComponent.extend("nabi.demo.comp.reuse.northwind.customer.selectionBtn.Component", {
    metadata : {
        manifest: "json",
        properties : {
            text: { type : "string", defaultValue : "Default Text"}
        },
        aggregations : { },
        events : {
            customerSelected : {
                parameters : {
                    customer : {type : "object"}
                }
            }
        }
    }
});
	

As you can see we have a manifest.json file – the component descriptor (discusses below). Additionally, we have a public property called text of type string which holds the text for the button rendered by this component. Pressing the button will automatically open a customer selection dialog allowing the user to select a customer from a list of customers. Once a customer is selected the component fires an event customerSelected event and passes the data of the selected customer via the event parameter customer. We will create the button later but in the same file.


5.1. The component descriptor of selectionBtn

Inside the manifest.json file of the component it's important to set sap.app.type to component. Furthermore, we have to set the relative path to the folder containing the library.js file via sap.app.embeddedBy. It's worth mentioning that the manifest.json declares a dataSource called mainService which references the V2 Northwind OData service (for details have a look at the full code of the manifest.json). This is needed because our component loads the list of customers from the Northwind OData service. We've also set sap.ui5.componentName inside the manifest.json (in our case it's equal to our component id). The other parts of the component descriptor are pretty much straight forward if you are a little familiar with SAPUI5 and app descriptors (check the code for details).

manifest.json of our selectionBtn Re-use Component (/src/nabi/demo/comp/reuse/northwind/customer/selectionBtn/manifest.json)
{
    "_version": "1.9.0",
    "sap.app": {
        "id": "nabi.demo.comp.reuse.northwind.customer.selectionBtn",
        "type": "component",
        "embeddedBy" : "../../../../../",
        "i18n": "i18n/i18n.properties",
        "title": "{{compTitle}}",
        "description": "{{compDescription}}",
        "resources" : "resources.json",
        "applicationVersion": {
            "version": "1.0.0"
        },
        "dataSources": {
            "mainService": {
                "uri": "/destinations/northwind/V2/Northwind/Northwind.svc/",
                "type": "OData",
                "settings": {
                    "odataVersion": "2.0"
                }
            }
        }
    },

    "sap.ui": { //... },

    "sap.ui5": {
        "componentName" : "nabi.demo.comp.reuse.northwind.customer.selectionBtn",
        "dependencies": { //... },
        //...
    }

}


5.2. Overriding init() inside Component.js

Component.prototype.init = function () {
    var oModel;

    // call the init function of the parent - ATTENTION: this triggers createContent()
    UIComponent.prototype.init.apply(this, arguments);
    //now this here would work:
    //var oRoot = this.getRootControl();

    // we could create a device model and use it
    oModel = new JSONModel(Device);
    oModel.setDefaultBindingMode("OneWay");
    this.setModel(oModel, "device");
};
	

Inside the overridden init() function the parent's init function is called. This will trigger the component's createContent function which by default reads the sap.ui5.rootView from the component descriptor. In our scenario, sap.ui5.rootView from the manifest.json is not used. Instead, we override createContent (see next section below).

We don't need a device model in this little demo (it's only added for demonstration purposes). However, you could basically create any kind of model and put it on the component so that you can easily access the model data later in the component.


5.3. Overriding createContent() inside Component.js

Component.prototype.createContent = function() {
    var oDialog, oButton;

    oDialog = this._getCustomerSelectDialog();

    oButton = this._getOpenButton();
    oButton.addDependent(oDialog);

    return oButton;
};

By overriding createContent() we can return both a view or even a control. The latter one is not possible via the sap.ui5.rootView setting of the manifest.json. We are going to make use of this.

Firstly, we load our dialog from a fragment by using a private getter. Secondly, we create an instance of sap.m.Button and add the dialog as a dependent of the button. This makes sure the dialog is attached to the models and lifecycle of the button. Finally, createContent() returns the button. This makes sure that the button can access the models of the component and the button is attached to the component's lifecycle. In other words: if the component is destroyed then the button is destroyed, and when the button is destroyed the dialog gets destroyed as well. Furthermore, the returned button is the control that gets rendered when the component is put into the DOM.

The private API _getCustomerSelectDialog() loads the dialog from a fragment and applies the content density class. Please note that there is no addDependent() call after the fragment is loaded:

Component.prototype._getCustomerSelectDialog = function () {
    if (!this._oTSD) {
        this._oTSD = sap.ui.xmlfragment(this.getId(), "nabi.demo.comp.reuse.northwind.customer.selectionBtn.fragment.CustomerTableSelectDialog", this);
        this._oTSD.addStyleClass(this.getContentDensityClass());
    }
    return this._oTSD;
};

Our private getter for the button creates a new instance of sap.m.Button. The constructor is used to set the text property of the button to the value of the component's text property. To be in sync at any time we have to override the setter of the component's text property. Additionally, an event handler for the button's press event is registered via the constructor. Here is the private getter for our button:

Component.prototype._getOpenButton = function () {
    if (!this._oBtn) {
        this._oBtn = new Button(this.createId("openSelectDialogBtn"),{
            text : this.getText(),
            press : this.onShowCustomerSelectDialog.bind(this)
        });
    }
    return this._oBtn;
};

The setter for the component's text property sets (of course) the component's text property and forwards the same text value to the button's text property:

Component.prototype.setText = function(sText) {
    this._getOpenButton().setText(sText);
    this.setProperty("text", sText);
    return this;
};

The press event handler for the button is also implemented in the Component.js file. The event handler does nothing but opening the (singleton) dialog:

Component.prototype.onShowCustomerSelectDialog = function () {
    var oTSD = this._getCustomerSelectDialog();
    //oTSD.getBinding("items").filter();    //reset not needed here (done in onCustomerSearch which is also triggered if dialog closes)
    oTSD.open();
};

The same event handler is called when someone calls the public API which we want to offer now to open the dialog via JavaScript:

Component.prototype.open = function () {
    this.onShowCustomerSelectDialog();
};


5.4. Implementing the CustomerTableSelectDialog Fragment

The dialog that opens when the button is pressed is pretty straight forward. In fact, in both components the fragment is the same. I decided to duplicate the fragment's code instead of implementing another demonstration of re-use. However, in a real-life project I would most probably go for the re-use option. Now back to the fragment:

<core:FragmentDefinition
    xmlns="sap.m"
    xmlns:core="sap.ui.core">

    <TableSelectDialog
        contentWidth="80%"
        title="{i18n>compTitle}"
        search="onCustomerSearch"
        confirm="onCustomerSelected"
        cancel="onCustomerSelectDialogCancelled"
        growingThreshold="50"
        busyIndicatorDelay="0"
        items="{
            path: '/Customers',
            templateShareable:false
        }">

        <items>
            <ColumnListItem>
                <cells>
                    <Text text="{CustomerID}"/>
                    <Text text="{CompanyName}"/>
                </cells>
            </ColumnListItem>
        </items>

        <columns>
            <Column>
                <header>
                    <Text text="{i18n>comp.col.customer.id}"/>
                </header>
            </Column>
            <Column>
                <header>
                    <Text text="{i18n>comp.col.customer.companyName}"/>
                </header>
            </Column>
        </columns>

    </TableSelectDialog>

</core:FragmentDefinition>

The fragment contains a simple sap.m.TableSelectDialog which allows to select a customer from a list of customers. Please note that the fragment registers event handlers for the following events: search, confirm, cancel. The event handlers are implemented in our Component.js:

Component.prototype.onCustomerSearch = function (oEvent) {
    var oTSD, oFilter, sQuery, oBinding;

    oTSD = this._getCustomerSelectDialog();
    oBinding = oTSD.getBinding("items");
    if (!oBinding){
        return;
    }

    sQuery = $.trim( oEvent.getParameter("value") );

    if (sQuery) {
        oFilter = new Filter({
            filters : [
                new Filter("CustomerID", FilterOperator.Contains, sQuery),
                new Filter("CompanyName", FilterOperator.Contains, sQuery)
            ],
            and : false
        });
    }
    oBinding.filter(oFilter);
};

Component.prototype.onCustomerSelected = function(oEvent) {
    var aContexts, oCustomer;

    aContexts = oEvent.getParameter("selectedContexts");
    if (aContexts.length) {
        oCustomer = jQuery.extend({}, aContexts[0].getObject());    //clone
        this.fireCustomerSelected({
            customer : oCustomer
        });
    }
};

Component.prototype.onCustomerSelectDialogCancelled = function (oEvent) {
    //oEvent.getSource().unbindItems();		//we don't want this
};

The search event handler onCustomerSearch() triggers an OData $filter request. The filter properties CustomerID and CompanyName are used for the filtering.

The confirm event handler is called onCustomerSelected(). It is triggered when the user selects a giver customer from the list. The event handler fires the customerSelected event of the component and passes the data of the selected customer as an event parameter.

The cancel event handler is doing nothing. Feel free to change as needed, i.e. you could forward the cancel event by firing a customerSelectionCancelled event...


6. The Re-use Component selection

Compared to our other component (see above), the metadata configuration of our second Re-use Component has one additional property renderButton of type boolean with a defaultValue of true:

var Component = UIComponent.extend("nabi.demo.comp.reuse.northwind.customer.selection.Component", {
    metadata : {
        manifest: "json",
        properties : {
            text: { type : "string", defaultValue : "Default Text"},
            renderButton: { type : "boolean", defaultValue : true}
        },
        aggregations : { },
        events : {
            customerSelected : {
                parameters : {
                    customer : {type : "object"}
                }
            }
        }
    }

});

The additional property renderButton allows the consumer of this component to decide whether a button shall be rendered or not. However, we will consider this setting only during initialization of our component (to make things not too complicated).


6.1. The component descriptor of selection

The manifest.json is basically similar to the manifest.json of our other component. Please check the code for details:

manifest.json of our selection Re-use Component (/src/nabi/demo/comp/reuse/northwind/customer/selection/manifest.json)
{
    "_version": "1.9.0",
    "sap.app": {
        "id": "nabi.demo.comp.reuse.northwind.customer.selection",
        "type": "component",
        "embeddedBy" : "../../../../../",
        "i18n": "i18n/i18n.properties",
        "title": "{{compTitle}}",
        "description": "{{compDescription}}",
        "resources" : "resources.json",
        "applicationVersion": {
            "version": "1.0.0"
        },
        "dataSources": {
            "mainService": {
                "uri": "/destinations/northwind/V2/Northwind/Northwind.svc/",
                "type": "OData",
                "settings": {
                    "odataVersion": "2.0"
                }
            }
        }
    },

    "sap.ui": { //... },

    "sap.ui5": {
        "componentName" : "nabi.demo.comp.reuse.northwind.customer.selection",
        "dependencies": { //... },
        //...
    }
}


6.2. Overriding init() inside Component.js

Component.prototype.init = function () {
    var oModel, oCompData;

    oCompData = this.getComponentData();
    if (typeof oCompData.renderButton === "boolean"){
        this.setRenderButton(oCompData.renderButton);
    }

    // call the init function of the parent - ATTENTION: this triggers createContent()
    UIComponent.prototype.init.apply(this, arguments);
    //now this here would work:
    //var oRoot = this.getRootControl();

    // we could create a device model and use it
    oModel = new JSONModel(Device);
    oModel.setDefaultBindingMode("OneWay");
    this.setModel(oModel, "device");
};
	

Components are typically consumed via sap.ui.core.ComponentContainer. However, how would you pass the settings from the ComponentContainer to a specific component? A clean way to achieve this is by using componentData. By calling this.getComponentData() we can easily access the data received from the outer world. In our case we are checking if there is a renderButton property of type boolean available. If so, we simply pass its value to the component's renderButton property. Keep this piece of code in mind when we see how to consume our Re-use Components in an own app.


6.3. Overriding createContent() inside Component.js

Component.prototype.createContent = function() {
    var oBtn, oTSD;

    oTSD = this._getCustomerSelectDialog();
    if (this.getRenderButton()) {
        oBtn = this._getOpenButton();
        oBtn.addDependent(oTSD);
        return oBtn;
    }
    return oTSD;
};

Again, by overriding createContent() we can return both a view or even a control. The latter one is not possible via the sap.ui5.rootView property of the manifest.json file.

This time our implementation of createContent() dynamically returns either a dialog or a button. If the renderButton property is true then we add the previously loaded dialog as a dependent to the button before the button is returned. If renderButton=false we simply return the dialog (false means "dear component, do not render a button").


6.4. Implementing the CustomerTableSelectDialog Fragment

Same as previous component – see above or code on Github for details.


7. Consuming Re-use Components in own apps

I have added a simple component based app which consumes the two Re-use Components implemented above. There are exactly three files of interest for us: manifest.json, Home.view.xml, and Home.controller.js - we will have a look at each of them now. You will also learn different ways of loading components (i.e. in a declarative way or with JavaScript) and you'll see some nice UI5 features added with 1.50+.


7.1. Referencing components in the manifest.json

When re-using components in your own apps you can leverage from the (async/sync) component pre-loading feature available in UI5. This is basically the same as pre-loading libraries. We want to add dependencies to the components we want to re-use by changing sap.ui5.dependencies.components in our app descriptor ():

{
    "_version": "1.9.0",
    "sap.app": {
        "id": "nabi.sample.customerSelection",
        "type": "application",
        //...
    },

    "sap.ui": { //... },

    "sap.ui5": {
        //...
        "dependencies": {
            "minUI5Version": "1.44.0",
            "libs": {
                "sap.ui.core": {},
                "sap.m": {},
                "nabi.demo": {}
            },
            "components" : {
                "nabi.demo.comp.reuse.northwind.customer.selection" : {
                    //"lazy" : true    //default is false
                },
                "nabi.demo.comp.reuse.northwind.customer.selectionBtn" : {
                    //"lazy" : true    //default is false
                }
            }
        },
        "componentUsages":{
            "simpleCustomerSelectionWithoutButton" :{
                "name" : "nabi.demo.comp.reuse.northwind.customer.selection",
                "settings" : {},
                "componentData" : {
                    "renderButton" : false
                }
                //,"manifest" : true
            },
            "simpleCustomerSelectionWithButton" :{
                "name" : "nabi.demo.comp.reuse.northwind.customer.selection",
                "settings" : {},
                "componentData" : {
                    "renderButton" : true
                }
                //,"manifest" : true
            },
            "simpleCustomerSelectionBtn1" :{
                "name" : "nabi.demo.comp.reuse.northwind.customer.selectionBtn",
                "settings" : {},
                "componentData" : {}
                //,"manifest" : true
            },
            "simpleCustomerSelectionBtn2" :{
                "name" : "nabi.demo.comp.reuse.northwind.customer.selectionBtn",
                "settings" : {},
                "componentData" : {}
                //,"manifest" : true
            }
        },
        //...
    }
}

We want to re-use both of our Re-use Components in our little app. Thus, we have added both of them as a dependency. Thanks to this UI5 will pre-load both the components by requesting the corresponding Component-preload.js file (lazy is per default false). However, both our components are part of the UI5 library nabi.demo and thus they are already part of the library-preload.js of nabi.demo. In other words: we could easily remove the dependencies to our two components because we already have a dependency to the nabi.demo library, or we could remove the dependency to nabi.demo but keep the dependencies to the two components.

In version 1.50 SAPUI5 has introduced the so called componentUsages – such a great and very useful feature via sap.ui5.componentUsages in the manifest.json file. Under sap.ui5.componentUsages you can declare the used components including some kind of configuration. In our case we have four componentUsages, each one with a different key. The keys are random but must be unique in the manifest.json file. You can have as many componentUsages as you want. Although we have only two Re-use Components we are referencing these two to declare four componentUsages. The latter two use our selectionBtn Re-Use Component without any additional settings. The first two use our selection Re-use Component with different configurations: one renders a button while the other one doesn't.

Internally, the components defined via componentUsages are created/loaded by calling sap.ui.component(...). Since SAPUI5 1.49 this API allows to set a property called manifest (replacing the old manifestFirst setting) in oder to load the manifest asynchronously and evaluate it before the Component.js file (see docs for additional details). Unfortunately, with SAPUI5 below 1.54.x it's not possible to pass this property using the sap.ui.core.ComponentContainer because the sap.ui.core.ComponentContainer simply never offered such a property before SAPUI5 1.54.x. However, the github commit [FEATURE] ComponentContainer: introduce manifest property has introduced the manifest property on the sap.ui.core.ComponentContainer available with SAPUI5 1.54.0. The commit message is important:

[FEATURE] ComponentContainer: introduce manifest property

The manifest property can be used similar to the manifest option on the component factory function "sap.ui.component".

It controls when and from where to load the manifest for the Component. When set to any truthy value, the manifest will be loaded asynchronously by default and evaluated before the Component controller, if it is set to a falsy value other than "undefined", the manifest will be loaded after the controller. A non-empty string value will be interpreted as the URL location from where to load the manifest. A non-null object value will be interpreted as manifest content.

Important: The default value of the async property of the Component Container is false. If the manifest property has a truthy value and the async property has not been provided in the initial settings the async property will be set to true.

In other words: with SAPUI5 1.54.x you can set the manifest property directly on the sap.ui.core.ComponentContainer in an XMLView instead of using the component factory before embedding the ComponentContainer for achieving the same result (loading manifest first). Now let's see how easy it is to consume components in XMLViews.


7.2. Consuming components in XMLViews (Home.view.xml)

The magic of consuming components is typically handled by an instance of the ComponentContainer. Let's have a look at the last three ComponentContainers from below in our XMLView:

<mvc:View
    controllerName="nabi.sample.customerSelection.controller.Home"
    xmlns:mvc="sap.ui.core.mvc"
    xmlns:core="sap.ui.core"
    xmlns="sap.m"
    displayBlock="true"
    height="100%">

    <Page
        title="Sample: nabi.sample.customerSelection"
        class="sapUiResponsiveContentPadding">

        <content>
            <VBox id="myVBox">

                <!--
                    our own button to open the component's dialog - component loaded in
                    controller (usage = simpleCustomerSelectionWithoutButton)
                -->
                <Button
                    id="myCustomBtn"
                    text="My Own Trigger Button"
                    press="onOpenCustomerSelection"
                    enabled="{view>/customerSelectionLoaded}"/>

                <!--
                    old ui5 versions don't have the event componentCreated.
                    They don't even support usages. This example places the ComponentContainer
                    into the view while the component is set after it has been loaded via controller.
                    See _loadSecondComponentManually() in controller for workaround.
                -->
                <core:ComponentContainer id="compOldUi5Versions"/>

                <!-- old way -->
                <core:ComponentContainer
                    id="compOld"
                    name="nabi.demo.comp.reuse.northwind.customer.selectionBtn"
                    async="true"
                    componentCreated="onComponentCreated"/> <!-- componentCreated event since 1.50 -->

                <!-- newer versions of UI5 support this -->
                <core:ComponentContainer
                    id="compNew1"
                    usage="simpleCustomerSelectionBtn1"
                    async="true"
                    componentCreated="onComponentCreated"/> <!-- componentCreated event since 1.50 -->
                <core:ComponentContainer
                    id="compNew2"
                    usage="simpleCustomerSelectionBtn2"
                    async="true"
                    componentCreated="onComponentCreated"/> <!-- componentCreated event since 1.50 -->
                <core:ComponentContainer
                    id="compNew3"
                    usage="simpleCustomerSelectionWithButton"
                    async="true"
                    componentCreated="onComponentCreated"/> <!-- componentCreated event since 1.50 -->

            </VBox>

        </content>

    </Page>

</mvc:View>

In SAPUI5 1.50+ the ComponentContainer has a property usage and even componentCreated.

The property usage is for supporting componentUsages. This property can only be applied once and is used to create a component as defined in the componentUsages of the corresponding manifest.json file. In other words, the property usage is referencing a key inside sap.ui5.componentUsages from the manifest.json and uses the defined configuration to create a new instance of the component. We could also use sap.ui.core.ComponentContainer in our XMLView without using the usage property. In that case we use the name property of the ComponentContainer to specify which component shall be used by the ComponentContainer (see "old way" in code).

The event componentCreated is fired after the component was created. This event is of special interest for us: in the onComponentCreated event handler we want to access the created component (either our selection or selectionBtn Re-use Component) in order to attach an event handler for the customerSelected event of the component(s). This can only be done after the component has been created, and thus the componentCreated is important. As already mentioned, the event componentCreated was introduced in SAPUI5 1.50. That means we can't use it with older UI5 versions, i.e. 1.44.x or 1.48.x. They don't even support usages. One workaround would be placing an empty ComponentContainer (<core:ComponentContainer id="compOldUi5Versions"/>) into the view and set the component after it has been loaded via JavaScript in the controller. See the XMLView Home.view.xml for details. The corresponding controller code is discussed below.

Now, let's assume we have an own control in our view which allows to open the customer selection dialog. We cannot use our selectionBtn Re-use Component because it always renders a button (which we don't want). However, our selection Re-use Component can be configured to not render a button. For that scenario we have even added the componentUsage simpleCustomerSelectionWithoutButton to our manifest.json file. For demonstration purposes I have added an sap.m.Button control to the view. In its press event handler the customer selection dialog is opened. Of course, this only works if the corresponding control has been created. Thus, the button is only enabled if the component has been created.

Seeing the controller code will clarify open questions. Let's go for it now.


7.3. Loading components and handling component events in Controllers (Home.controller.js)

In our Home.view.xml we have added ComponentContainers referencing event handlers in the controller. Furthermore, we have added one empty ComponentContainer and an sap.m.Button control. For both we want to create components in the controller and wire things together. Here is the complete controller code of Home.controller.js:

sap.ui.define([
    "nabi/sample/customerSelection/controller/BaseController",
    "sap/ui/model/json/JSONModel",
    "sap/m/MessageToast",
    "sap/ui/core/ComponentContainer"
], function(BaseController, JSONModel, MessageToast, ComponentContainer) {
    "use strict";

    return BaseController.extend("nabi.sample.customerSelection.controller.Home", {

        onInit : function(){
            var oModel;

            oModel = new JSONModel({
                customerSelectionLoaded : false
            });
            this.getView().setModel(oModel, "view");

            this._loadFirstComponentManually();
            this._loadSecondComponentManually();    //works also in older ui5 versions
        },

        _loadFirstComponentManually : function (){
            this.getOwnerComponent().createComponent({
                usage: "simpleCustomerSelectionWithoutButton",
                settings: {},
                componentData: {
                    renderButton : false
                },
                async: true
            }).then(function(oComp){
                var oModel = this.getView().getModel("view");
                // needed to open the component's dialog
                this._oCustomerSelectionComp = oComp;

                oModel.setProperty("/customerSelectionLoaded", true);
                oComp.attachCustomerSelected(this.onCustomerSelected);
                //to avoid "The Popup content is NOT connected with a UIArea and may not work properly!"
                this.byId("myVBox").addItem(new ComponentContainer({
                    //settings: { renderButton : false },
                    component : oComp
                }));
            }.bind(this)).catch(function(oError) {
                jQuery.sap.log.error(oError);
            });
        },

        /**
         * This is a workaround for older versions of UI5 where the ComponentContainer
         * doesn't support the event componentCreated and the property usage. This approach allows to place the
         * ComponentContainer somewhere in your view and set the component later after
         * the component has been loaded. This also allows to access the loaded component directly right after
         * it's available.
         */
        _loadSecondComponentManually : function (){
            sap.ui.component({
                name: "nabi.demo.comp.reuse.northwind.customer.selection",
                settings: {},
                componentData: {},
                async: true,
                //manifestFirst : true,  //deprecated - replaced with "manifest" from 1.49+
                manifest : true          //SAPUI5 >= 1.49
            }).then(function(oComp){
                oComp.setText("For old UI5 versions");
                oComp.attachCustomerSelected(this.onCustomerSelected);
                this.byId("compOldUi5Versions").setComponent(oComp);
            }.bind(this)).catch(function(oError) {
                jQuery.sap.log.error(oError);
            });
        },

        onComponentCreated : function(oEvent){
            var oComp = oEvent.getParameter("component");

            // use documented public Component APIs only
            oComp.setText("Select Customer");
            //...

            // ATTENTION: working with getRootControl() might break code in future, i.e. if the result is a different control.
            // Unless you know it will never change avoid the following code here:
            //var oBtn = oComp.getRootControl();    // will the result be a button any time in future?
            //oBtn.setText("Don't do this");

            oComp.attachCustomerSelected(this.onCustomerSelected);
        },

        onOpenCustomerSelection : function(){
            this._oCustomerSelectionComp.open();
        },

        onCustomerSelected : function(oEvent){
            var oCustomer = oEvent.getParameter("customer");
            MessageToast.show("Selected Customer: CustomerID=" + oCustomer.CustomerID + ", CompanyName=" + oCustomer.CompanyName);
            console.log(oCustomer);
        },

        onExit : function() { }

    });
});

In onInit() a view model is created. It's only used for the enabled property of our button in the view. After that we asynchronously create two components using different APIs.

The first component is loaded with _loadFirstComponentManually(). It uses this.getOwnerComponent().createComponent() in combination with the usage simpleCustomerSelectionWithoutButton and passes renderButton=false via componentData. After the component has been created the promise resolves with a reference to the created component (in this case it's the selection Re-use Component, configured not to render a button). When resolved, we can enable our button by setting the corresponding property in the view model to true. Additionally, we keep a reference to the component for later use in the controller. This reference is used later for programmatically opening the customer selection dialog. Then we attach an event handler for the customerSelected event. Once the user selects a customer from the list of customers this event handler function is called. We re-use this event handler for all our components to display the data of the selected customer in a simple sap.m.MessageToast and additionally in the console. It's important to note that in this case the component does not initially render anything because its dynamic createContent() returns only a dialog (without going any deeper here). Without any additional code in our controller we would see the following warning message in the console: The Popup content is NOT connected with a UIArea and may not work properly!

We can easily solve this by creating a ComponentContainer on the fly, assigning the created component to it, and finally (!) adding the ComponentContainer somewhere to the view. As you will see nothing will be rendered, and the warning will disappear.

Now let's continue with the second component loaded in _loadSecondComponentManually(). This time we use the good old API sap.ui.component() to create the selection Re-use Component by name (without usage). This even works for SAPUI5 versions below 1.50, i.e. it works just fine with SAPUI5 1.44. When the promise has resolved we change the text property of the component (only for demonstration purposes). Of course, we attach an event handler for the customerSelected event of the created component. Finally, the created component is set on the ComponentContainer with id=compOldUi5Versions.

That's it, you made it. The rest of the controller code should be easy to understand.


8. Additional hints

Why didn't we use neither jQuery.sap.registerModulePath(...) nor jQuery.sap.require(...)?
Simply because it's not needed! However, that only works if you have deployed your UI5 apps, components, and libraries correctly assuming you deploy either to SAP Cloud Platform or to an on-premise SAP NW ABAP. There, the magic happens thanks to the App Index. For details have a look at my blog here: The UI5 App Index – a demo using SAP Web IDE Full-Stack to clone a Github repo + build via Grunt. In fact, I suggest trying to avoid jQuery.sap.registerModulePath(...) and jQuery.sap.require(...) in the deployed app code whenever possible (of course, there might be exceptions...).
Why does this.getOwnerComponent().createComponent() not offer a setting for manifestFirst or manifest?
The API documentation for sap.ui.core.Component.createComponent() says: "Creates a nested component that is declared in the sap.ui5/componentUsages section of the descriptor (manifest.json)." So this API is meant to be used in combination with the componentUsages defined in the manifest.json file and that's exactly where you would set manifest. Don't use manifestFirst because it's deprecated since since 1.49.0. The configuration in componentUsages is internally passed to sap.ui.component(). Thus, you can set manifest in componentUsages.
Why does this tutorial not set the property manifest or manifestFirst for the sap.ui.core.ComponentContainer in the XMLView?
Well, because the sap.ui.core.ComponentContainer did not offer such a property before SAPUI5 1.54.0 (which was not out yet at the time of writing this tutorial)! Even if you have seen this in other (older) tutorials or code make sure not to copy and paste that code (unless you are using SAPUI5 1.54.x or newer). Most probably it won't harm you, but it won't help you neither when using a SAPUI5 version below 1.54. However, it might make developers think it works in a certain way, but it doesn't.
Then how can I pass manifest = true via sap.ui.core.ComponentContainer in an XMLView?
Do I have to use a library for my Re-use Component?
No. Working with libraries brings some overhead at development time, especially if you create the library only for one single component. You could implement your Re-use Component as a standalone component and deploy it just the way you are used to deploy typical SAPUI5 apps. Make sure to check your App Index afterwards, see here fore details: The UI5 App Index – a demo using SAP Web IDE Full-Stack to clone a Github repo + build via Grunt. Are you wondering when to use a library and when not? I don't have a clear answer to that; decide from case to case! You could put all your components into a control library, or maybe into a component library depending on whether you're fine with mixing controls and components into the same library or not. You could also think about structuring your components into several component libraries, each one offering components for a certain scenario (finance components, sales components, communication components, etc.)
Why do I get "Couldn't rerender '__dialog1-table', as its DOM location couldn't be determined - sap.ui.Rendering" after closing a dialog?
That's something I did not see in older versions of SAPUI5. Here is what happens: The dialog has a binding and thus the data gets loaded from the Northwind OData service. The sap.m.TableSelectDialog has a search field which can be filled by the user. If the user triggers a search and then closes the dialog (i.e. by pressing cancel or selecting an entry from the list) it could be a good idea to reset the search field's value. This is because in most cases you always want the dialog to open with an initial state when it gets opened again, in other words: the next time the TableSelectDialog opens you want the search field to be empty and thus the items (well, actually the binding) in the list should also consider that. SAPUI5 has become really smart recently I guess, because when the TableSelectDialog is closed the search event is fired which in turn calls our onCustomerSearch() event handler. There the binding is updated with an empty filter by calling oBinding.filter(oFilter);. In the meanwhile the dialog has closed (it's not "rendered"), but the binding is still active. As a result, the new data received from the OData service matching the empty filter triggers a re-rendering of the items. However, the re-rendering cannot be executed because the dialog has already closed. There are some more or less smart workarounds for this little issue, but that's not the focus of this tutorial here - plus I'm a little lazy to write more... :-)
Why do I get "2018-01-08 14:27:18.598090 Can't remove control from a non-managed category id: _DIALOG_ -" in my console?
If you enable "manifest" : true in the manifest.json discussed above by removing the comments (//) you will see that the dialog data might not be loaded anymore. Additionally, you'll see the following warning in the console: 2018-01-08 14:27:18.598090 Can't remove control from a non-managed category id: _DIALOG_ -. If you get this warning in the console for this tutorial then the UI5 version you are using has a bug. I reported this bug to Peter Muessig and he fixed it with the following commit: [FIX] ComponentContainer: component must be loaded only once. Make sure your UI5 has this fix.

9. Running the code

CLI commands
git clone https://github.com/nzamani/ui5-nabi-demo
cd ui5-nabi-demo
npm install
npm start

Then open your browser


Comments
  • If you like this tutorial help us to maintain it:
  • And don't forget to let others know about it:
Nice work
posted by Yellappa Madigonde
Fri Oct 05 19:20:18 UTC 2018
Thank you nabi for your detailed explanation. I have been looking for a such nice and easy explanation of component re-use.

Regards,
Yellappa.
problem in 1.52 for reuse component
posted by radhey
Mon Jun 18 14:40:39 UTC 2018
hi nabi

i am trying to implement PR on hana, etc continue getting problem for dependencies, reusable component, like attachment,account reuse preload missing.Any idea?