Shared event handler for XML views with different controllers - event-handling

Given two XML Views:
<mvc:View
controllerName="my.namespace.controller.First"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m">
<Button press=".onBtnPress" />
</mvc:View>
<mvc:View
controllerName="my.namespace.controller.Second"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m">
<Button press=".onBtnPress" />
</mvc:View>
As expected, the press event is handled by First.controller.js or Second.controller.js.
Instead of duplicating the event handler code or implementing handlers in each Controller to chain/hand off the work, I want to declare a shared event handler.
According to docs this should be possible, using a naming convention for the handler:
Names starting with a dot ('.') are always assumed to represent a method in the controller.
Names containing a dot at a later position are assumed to represent global functions and are resolved by calling jQuery.sap.getObject with the full name.
So I change the handler and declare a shared object, like so:
First.view.xml:
<Button press="my.namespace.Shared.onBtnPress" />
Shared.js:
jQuery.sap.declare("my.namespace.Shared");
my.namespace.Shared = (function() {
var onBtnPress = function() {
console.log("button pressed");
};
return { onBtnPress : onBtnPress };
}());
Warning logged (debug sources) during view initialisation:
sap.ui.core.mvc.XMLView#__xmlview1: event handler function "my.namespace.Shared.onBtnPress" is not a function or does not exist in the controller. -
Calling jQuery.sap.getObject("my.namespace.Shared") yields undefined
Same issue when using sap.ui.define to make the object known.

Since UI5 1.69, it has become easier to share JS modules in XML view and fragment.doc
Here is an example: https://embed.plnkr.co/5G80I5HWObCuM5cG
<mvc:View controllerName="..."
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m"
xmlns:core="sap.ui.core"
core:require="{ onButtonPress: 'my/shared/onButtonPress' }">
<Button text="Press" press="onButtonPress" />
</mvc:View>
As we can see, each button displays a different message depending on the view, even though the handler itself isn't included in the controller definition.
The this context is still the controller instance as documented in the topic Handling Events in XML Views:
As long as no event handler parameters are specified and regardless of where the function was looked up, it will be executed with the controller as the context object (this).

Your shard object looks weird
Try something like this:
sap.ui.define([], function() {
return sap.ui.base.Object.extend("my.namespace.Shared", function() {
onBtnPress : function() {
console.log("button pressed");
}
};
});
Also remember to put the object in the right directory.

I was able to copy-paste your sap.ui.define and verified it was created correctly jQuery.sap.getObject.
Make sure that your sap.ui.define has been called prior to your view being rendered.
You can set a breakpoint in the XMLTemplateProcessor where the event callbacks are handled for XML views for further debugging/timing issues.
If you look in the _resolveEventHandler function it should take you here which will perform the jQuery.sap.getObject.

Related

<Component> is not defined in SAPUI5

I tried to set up UI5 development on my local machine follow this turoial. The general setup seems to be fine, but when I add a button and want to let it show a MessageToast or a MessageBox (since I've read that MessageToast is deprecated) it does not work. The Button is showed, but when I click on it I get the following issues:
In Edge Developer Tools: "MessageToast is not defined" (same with MessageBox)
In Console: Error: connect ECONNREFUSED 127.0.0.1:80 at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1187:16)
Here is my coding:
View:
<mvc:View controllerName="testproject.controller.Test123"
xmlns:mvc="sap.ui.core.mvc" displayBlock="true"
xmlns="sap.m">
<Page id="page" title="{i18n>title}">
<content>
<Button id="test" text="Press Button" press="onClick"/>
</content>
</Page>
</mvc:View>
Controller:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast",
"sap/m/MessageBox"
],
/**
* #param {typeof sap.ui.core.mvc.Controller} Controller
*/
function (Controller) {
"use strict";
return Controller.extend("testproject.controller.Test123", {
onInit: function () {
},
onClick: function() {
//MessageToast.show("Test");
MessageBox.information("Test2");
}
});
});
I tried this via VS-Code and since I could not be sure if my setup is wrong I used the trial instance of the BAS. So if you want to reproduce it, just generate a freestyle UI5 project via BAS (or VS-Code) and then basically just add the button and the code of the controller. I've done nothing more.
I am running on UI5 version 1.48.0, have chosen no data source (would have added it manually later, since I just wanted to test first) and thats it. maybe you can tell me what I am missing (I programmed UI5 in the past and this was basically also more or less my hello world, but I can't get my head around what is wrong this time)
You import 3 dependencies here
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/MessageToast",
"sap/m/MessageBox"
],
but only 1 of them (the first) will be accessible because you didn't add them to the method signature here
function (Controller) {
So just add the other dependencies to your signature
function (Controller, MessageToast, MessageBox) {

Setting multiple properties of a component with one function

I have a component in a list in a sapui5 XML view and I want to set multiple properties of that component with one function. E.g. I want to set text, status, tooltip and icon of an ObjectStatus together, because the values of those are all different facets of the same data. The issue is that i have to calculate the values to set to those properties from the model with the same relatively time-heavy function. If I write a separate formatter for each of those properties, it has to run the same function for each property. Instead of this I would like to write one function that runs this time-heavy function once and sets a value to all those properties at the same time.
To accomplish this, I have tried creating a sapui5 fragment that could be placed in the list and filled with different information by the createContent function for each instance of that fragment. However I cannot figure out how to do this.
In the view definitions I'm trying to instantiate the fragment like this:
<core:Fragment fragmentName="QuantificationParameter" type="JS" path="{project>}"/>
And then I'm trying to set different content to each instance of the fragment:
sap.ui.jsfragment("namespace.fragments.QuantificationParameter", {
createContent: function(oParentController) {
//Get the object bound to this list item
var derived; //Calculate some intermediate information from this object
return new sap.m.ObjectStatus({
icon: derived.icon,
text: derived.text,
state: derived.state,
tooltip: derived.tooltip
});
}
});
While debugging it seems that the createContent function of the fragment is run only once and I cannot figure out any way to access the data that I'm trying to bind to the fragment. Is there any way I can render different content to each instance of the fragment?
What you are searching for is called databinding.
But first of all: we do not use JS Fragments, due to the same reason we do not use JS views. Here s a little Blog written on that topic.
https://blogs.sap.com/2018/05/01/why-do-we-use-xml-views-rather-js-views-in-sapui5/
Now the databinding part:
I asume, that Fragment will have the same controlls for each instance and you just want the values to change. To do just that you need to create a JSONModel either in your BaseController or component.js. In this Model you store i.e. your Labels text.
Inside your Fragmet you bind that property to the label. Since JSONModels bindingmode is two way by default the Label will change dynamically if you update the model. You can update the model i.e. everytime the user clicks on one of your list items.
Framgmet example:
<core:FragmentDefinition
xmlns="sap.m"
xmlns:f="sap.ui.layout.form"
xmlns:core="sap.ui.core">
<f:SimpleForm
editable="true">
<f:content>
<Input
value="{baseModel>/inputA}"
type="Text"
placeholder="{i18n>placeHolder}"/>
<TextArea
value="{baseModel>/textA}"/>
<TextArea
editable="false"
value="{baseModel>/textB}"/>
</f:content>
</f:SimpleForm>
</core:FragmentDefinition>
creation of the model i.e in component.js:
var oBaseModel = new JSONModel({
inputA: "",
textA: "",
textB: ""
});
this.setModel(oBaseModel, "baseModel");
example for your press lit item funtion:
(should be in the controller of the view your list is located in)
onListPress: function (oEvent) {
var oLine = oEvent.getSource().getBindingContext("yourRemoteService").getObject();
this._oBaseModel.setProperty("/inputA", oLine.ListPropertyA);
this._oBaseModel.setProperty("/textA", oLine.ListPropertyb);
this._oBaseModel.setProperty("/textB", oLine.ListPropertyC);
}
You should really give that tutorial a go:
https://sapui5.hana.ondemand.com/#/topic/e5310932a71f42daa41f3a6143efca9c

Using SearchField Inside Popover Keeps on Emitting 'select' Event and Rerenders Popover

I am using a custom control for a popover like this:
sap.ui.define([
"sap/m/Popover"
], function(Popover) {
"use strict";
return Popover.extend("name of controle", {
init: function() {
Popover.prototype.init.apply(this, arguments);
this.oPopup.setAutoClose(false);
},
renderer : "sap.m.PopoverRenderer"
});
});
And I am using this popover custom control like this:
<core:FragmentDefinition
xmlns="sap.m"
xmlns:core="sap.ui.core"
xmlns:path="path to custom popover"
>
<path:CustomPopover
placement="Left"
contentHeight="80%"
contentWidth="25%"
>
<path:content>
<!-- xmlview that has search field in it -->
</path:conent>
</path:CustomPopover>
</core:FragmentDefinition>
Now, if I use this fragment to create Popover, then the select event keeps on emitting for that search field.
2017-02-15 09:53:47.456659 Event fired: 'select' on Element sap.m.SearchField#__field0 - sap.ui.core.UIArea
This popover works fine in chrome even after continuous emission of an event but in Internet Explorer, it keeps on rerendering, so not able to use it. How to fix this?

How to get element's id of another view SAPUI5

I have 3 views say first,second & third. I need to access a vertical layout's id of second view from first view. For which I have used sap.ui.getCore().byId("__xmlview1--spend_layt"); to retrieve it. It works but when I navigate to third view & come back, id changes to __xmlview28--spend_layt & the control not works. How to fix this?
EDIT :
First.view.xml
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" controllerName="budgetspend.controller.First" xmlns:html="http://www.w3.org/1999/xhtml"
displayBlock="true">
<App id="BA_APP">
<pages>
<HBox width="100%" height="100%" id="header_container">
<items>
<Image class="logo" src="../images/logo_new.png"/>
<Image class="header" src="../images/header-bg.png"/>
<html:ul class="tab">
<html:li>
<html:a id="onBud" class="tablinks active">Tab 1
</html:li>
<html:li>
<html:a id="onSpend" class="tablinks">Tab 2</html:a>
</html:li>
</html:ul>
<mvc:XMLView viewName="budgetspend.view.second"/>
</items>
</HBox>
</pages>
</App>
</mvc:View>
In the second view, I use 2 vertical layouts. one for tab 1 & another for tab 2. Only one visible at a time. Tab click events are written in First.controller.js. I don't know how to access ID's of vertical layouts(in second view) from First.
I am just exploring how standard html will work on SAPUI5 as I am aware of using sap.m.IconTab.
What you are facing is due to the dynamic creation of elements. Every time you leave a screen and goes back into it, the items are regenerated, but they can't have the same ID, since they were used before.
One solution to this is to create a Utils folder, and create an object, let's say UIHelper.
This file would look like this:
jQuery.sap.declare('my.app.Utils.UIHelper');
my.app.Utils.UIHelper = {
controllerInstance1 : null,
controllerInstance2 : null,
controllerInstance3 : null,
//Add "getters and setters" for each of these attributes (repeat for Controller 2 & 3)
setControllerInstance1 : function(oController){
this.controllerInstance1 = oController;
},
getControllerInstance1 : function(){
return this.controllerInstance1;
}
};
Now, at the controller file for each of your views, you add the following line at the top of the file:
jQuery.sap.require('my.app.Utils.UIHelper');
And on the onInit event of those controllers, you set the controller instance on the UIHelper object:
my.app.Utils.UIHelper.setControllerInstance1(this);
And when you want to get the different views from anywhere in the code, assuming that the views are assigned to one of the controllers, you can just use the following:
my.app.Utils.UIHelper.getControllerInstance1().getView();
Update in my answer after the update on the question:
To access the layout from the second view, you could do the following:
var layout = my.app.Utils.UIHelper.getControllerInstanceView2().byId('spend_layt');

Identifying component rootview viewname

Imagine a component with a root view like so:
rootView: 'myapp.view.App'
The app view looks like this:
<mvc:View
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns="sap.m"
controllerName="myapp.controller.App"
displayBlock="true">
<App id="myapp">
<mvc:XMLView id="page1" viewName="myapp.view.Page1" />
</App>
</mvc:View>
As you can see there is a controller set up for this view. The view myapp.view.Page1 is another XML view, has a controller of its own and contains a sap.m.Page control.
This Page1 controller cannot access any controls without specifying __xmlview0-- in front of the Id. I know it's in a view, because the App is in a view. createId will create something relative to the current controller, so it will return __xmlview0--page1.
For instance, I can get the app from my myapp.controller.Page, but I have to access using the view name too. The same goes for my pages. Navigating to a new page in the app looks like this:
var app = this.byId('__xmlview0--myapp');
app.to('__xmlview0--page1');
this.createId('myapp') will give me __xmlview0--page1--myapp.
I also tried getting the component itself out this.getOwnerComponent() and use the createId and getId methods, but this leaves me with __component0 instead of __xmlview0, so that's a no-go too.
edit
Not a super clear story. Maybe this will go you some visuals:
-Component.js
- App.view.xml <App id="myapp"></App>
- Page1.view.xml <Page id="page1"></Page>
- App.controller.js
- Page1.controller.js
//byId uses createId.
//myapp has id __xmlview0--myapp
this.createId('myapp') = __xmlview0--page1--myapp
this.getView().createId('myapp') = __xmlview0--page1--myapp
this.getOwnerComponent().byId('myapp') = __component0--myapp
this.getOwnerComponent().getView() is not a function
How do I determine through my controllers to add __xmlview0? Which createId method do I use? Is my rootView wrong and is there a more efficient way? How do I get myapp out of the component?
Since you are using the component and you have rootview mentioned in the component configuration there is an API to retrieve the root view
var comp = this.getOwnerComponent().getMetadata() // this is the view i am referring to here.
comp.getRootView()
You could write a recursively navigate this.parent() until you have the sViewName equal to the comp.getRootView(). This entry gets the rootview you are looking for and then sId gets you the ID which you can use to append.
I am not sure what you are trying to achieve but this is a hack i thought you can use to derive the rootview's ID.
Alternatively you can also specify the rootView ID while defining component something like this
rootView: {
viewName : "ui.sample.demo.view.App",
id : "Root",
type : "XML"
},