"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:
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:
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…
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:
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.
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).
{ "_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):
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.
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).
{ "_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": { //... }, //... } }
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.
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(); };
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...
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).
The manifest.json is basically similar to the manifest.json of our other component. Please check the code for details:
{ "_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": { //... }, //... } }
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.
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").
Same as previous component – see above or code on Github for details.
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+.
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 order 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.
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.
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 theComponentContainer
* doesn't support the eventcomponentCreated
and the propertyusage
. 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.
git clone https://github.com/nzamani/ui5-nabi-demo cd ui5-nabi-demo npm install npm start
Then open your browser