How to Access Elements from XML Fragment by ID - sapui5

I am working on a SAPUI5 application. I have an XML view which contains an XML Fragment and a Button to save.
The fragment contains a few controls like drop-down, text field and a table.
When I press on the save button, I need to get all the rows in the table and call an OData update service.
The problem is in the onSave method in view controller. I get an error while accessing the table using its ID. Can anyone help me and advice how can I access controls used in fragments by their ID in the controller?
Here is the code snippet:
View:
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns:core="sap.ui.core" xmlns:form="sap.ui.layout.form" xmlns="sap.m">
<Page>
...
<form:SimpleForm>
<core:Fragment id ="fr1" fragmentName="first" type="XML" />
<Button id="id1" press="onSave" />
</form:SimpleForm>
</Page>
</mvc:View>
Fragment definition:
<core:FragmentDefinition xmlns="sap.m" xmlns:core="sap.ui.core">
<Table id="tab1" mode="MultiSelect">
...
</Table>
</core:FragmentDefinition>
Controller:
sap.ui.controller("view", {
onSave: function() {
//var tab = this.getView().byId("tab1"); // Not working
var tab = sap.ui.getCore().byId("tab1"); // Not working
},
// ...
});

Accessing controls inside a fragment depends on how your fragment was created in the first place. Here is a list of cases with respective API to use to get the control reference.
Given:
this as a reference to the current controller instance
Fragment required from the module sap/ui/core/Fragment
<MyControl id="controlId"/> in the fragment definition
API to choose
👉this.byId("controlId");
... if the fragment was created with the view ID (either indirectly or directly):
this.loadFragment({ name: "..." }); // id: view ID given by default, API since 1.93
<!-- In the view embedding the fragment declaratively: -->
<core:Fragment fragmentName="..." type="XML"/><!-- id = view ID given by default -->
Fragment.load({ // API since 1.58
id: this.getView().getId(),
name: "...",
controller: this,
});
sap.ui.xmlfragment(this.getView().getId(), "...", this); // Deprecated
Resulting global ID: "componentId---viewId--controlId" *
👉this.byId(Fragment.createId("fragmentId", "controlId"));
... if a fragment ID was given with the view ID combined:
this.loadFragment({ id: this.createId("fragmentId"), name: "..." });
<core:Fragment id="fragmentId" fragmentName="..." type="XML"/>
Fragment.load({
id: this.createId("fragmentId"),
name: "...",
controller: this,
});
sap.ui.xmlfragment(this.createId("fragmentId"), "...", this); // Deprecated
Resulting global ID: "componentId---viewId--fragmentId--controlId" *
👉Fragment.byId("fragmentId", "controlId");
... if only the fragment ID was given without combining with the view ID:
this.loadFragment({
id: "fragmentId",
name: "...",
autoPrefixId: false, // Explicitly disabled view ID as prefix
});
Fragment.load({
id: "fragmentId",
name: "...",
controller: this,
});
sap.ui.xmlfragment("fragmentId", "...", this); // Deprecated
Resulting global ID: "fragmentId--controlId" *
👉sap.ui.getCore().byId("controlId");
... if no ID to prefix was given. The below settings are not recommended as all control IDs within the fragment definition will be registered globally without any prefix. The uniqueness of the IDs is not guaranteed!
this.loadFragment({ name: "...", autoPrefixId: false }); // Not recommended if no id
Fragment.load({ name: "...", controller: this }); // Not recommended
sap.ui.xmlfragment("demo.view.MyFragment", this); // Deprecated
Resulting global ID: "controlId"
* Do not rely on the resulting global ID, for example, concatenating ID parts manually in your application. Always use the dedicated APIs mentioned above such as byId and createId. See Stable IDs: All You Need to Know.
Favor model-first approach over byId
Instead of accessing the fragment controls directly, consider manipulating the UI via data binding. Changes in the model will be reflected in the UI automatically, and, if two-way binding is enabled, user inputs from the UI will be stored in the model directly.
SAP Fiori elements guidelines
When developing Fiori elements extensions, make sure to adhere to the documented compatibility guidelines, especially regarding byId:
[...] Don't access or manipulate SAP Fiori elements' internal coding.
[...] Must not access any UI elements that are not defined within your view extensions.
âš  Caution
If you do not adhere to this guideline, your app may not work with future SAPUI5 versions because SAP Fiori elements might exchange controls for new ones that have a different API.

Looking at the OpenUI5 code at GitHub, it seems that the Fragment delegates the local ID generation to the containing view if the <Fragment/> itself does not have an explicit ID.
So your code this.getView().byId("tab1") should work as soon as you remove the id="fr1" attribute from your <Fragment/> element.
When using explicit IDs there is a static Fragment.byId method to retrieve the control. I guess you have to use it like this:
// Fragment required from "sap/ui/core/Fragment"
var fragmentId = this.getView().createId("fr1");
var tab = Fragment.byId(fragmentId, "tab1");

To make it work without explicit fragment ID and without static Fragment.byId() I used the following code snippet:
var prefix = this.getView().createId("").replace("--", "");
var fragment = sap.ui.xmlfragment(prefix, "-- XML fragment name --", this);
after this you can use this.getView().byId("tab1") as with any other control.

Related

How to get binding context of the second model?

My Fiori application has a view for which two models are set. The first model is set by default in manifest.json (OData), the second model I set in the controller.
The data of the second model are displayed in sap.m.TextArea. If I try to get its binding context, the output is undefined.
This is how I set second model:
onInit: function() {
var oLocalModel = {
Text: "test"
};
// JSONModel required from "sap/ui/model/json/JSONModel"
var oModel = new JSONModel(oLocalModel);
this.getView().setModel(oModel, "localData");
},
This is how I try to get the binding context of the TextArea:
onSendMail: function(oEvent) {
var oLocalContext = oEvent.getSource().getBindingContext("localData"); // undefined
var oLocalContext = this.byId("zgutMailFormText").getBindingContext("localData"); // undefined too
},
<TextArea id="zgutMailFormText" value="{localData>/Text}">
<layoutData>
<l:GridData span="XL2 L3 M3 S8" />
</layoutData>
</TextArea>
How to get the context depends on your application code which I don't see much in your question. But since the localData model is a client-side model, and since the binding path is already known (absolute path), you could simply create the context with the method createBindingContext.
onSendMail: function(oEvent) {
const oLocalContext = this.getView().getModel("localData").createBindingContext("/");
// ...
},
There could be many reasons why a control returns no context:
The control has simply no context. Contexts are created by the framework automatically for resolving relative binding paths. Since your TextArea binds data with an absolute path (localData>/Text), no context needs to be created. The path can be resolved immediately.
The control you accessed (e.g. this.byId("zgutMailFormText")) is part of a template control. Templates don't contain any contexts. Only the rendered clones do. See this answer for more explanation.
Parent controls themselves have no contexts bound, so there is no context to propagate.
From the API reference: sap/ui/model/Context:
The Context is a pointer to an object in the model data. A relative binding needs a context as a reference point in order to resolve its path; without a context, a relative binding is unresolved and does not point to model data. Context instances can, for example, be created in the following ways:
by a sap.ui.model.ListBinding for each list entry,
as the single context associated with a sap.ui.model.ContextBinding,
by calling sap.ui.model.Model#createBindingContext.

How to synchronize control values within different views

I would like to know how to get the content of TextArea, assign the value to a variable, set it to a model, and then set the variable to another TextArea in another view. I have coded some examples and it works, but not on TextArea.
Here is the example code:
// In init of the Component.js
this.setModel(new JSONModel(), "TransportModel"); // JSONModel required from "sap/ui/model/json/JSONModel"
// In *.controller.js
this.getView().getModel("TransportModel").setProperty("/", {
"Serial": this.byId("mat_serial").getValue() // "mat_serial" == id of the Input box in XML view
});
In the last step, I set the Text from a different View (also XML and Input Box) with the Value of the Model Element.
<Text text="{TransportModel>/Serial}" />
That worked pretty well.
But how to do the same with the TextArea? How can I do it based on this model? The value that I want to use from the first TextArea should also be on a TextArea in another view.
UI5 supports two-way data binding. I.e. if the user changes something in the UI (e.g. user types something in the text area), that change will be reflected automatically in other bindings that listen to the change.
<!-- In view 1 -->
<TextArea value="{TransportModel>/Serial}" />
<!-- In view 2 -->
<Text text="{TransportModel>/Serial}" />
No need to get input values by hand. Simply let the framework synchronize the value.
How to use a local json model:
Create
initItemViewModel: function () {
return new JSONModel({
Serial: ""
});
}
this._oViewModel = this.initItemViewModel();
this.setModel(this._oViewModel, "TransportModel");
Using
this.getView().getModel("TransportModel").setProperty("/Serial", serial);
<Text text="{TransportModel>/Serial}" width="auto" maxLines="1"/>

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

How do I change the name of 'component0' in OpenUi5/SapUI5?

I've just jumped in to OpenUI5, and had a problem where I couldn't get navigation targets to open up in aggregations within sub views. I found an answer on Stack Overflow that said that I needed to specifically name each View in the parent hierarchy to stabilise the name - this mostly worked, and I've been able to get my routing and targets to function.
My problem now is that the ID prefix of my item is __component0. I cannot find any way to change the name of this part, so really I'm not fully in control of my IDs.
I've tried changing the sId of the component before and after initialisation, but then the app doesn't function. I've also set both sap.app.componentName and sap.ui5.componentId, and neither are used. It appears that component0 is constructed by getting the type name of the controller class, but that has to be called Component.js.
Does anybody know how to change this value, or otherwise derive a control ID in the DOM that is fully defined in code?
In response to boghyon, I don't doubt that setting the ID via the factory function works, I don't know how to apply that ID value or factory function to the style of code that I have. This style comes from the SAP starter template on GitHub. Simply specifying id: "mycomponentname" in the extend object does not work.
So how would I go about refactoring what I have to the factory function format?
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/Device",
"com/uk/ia/air/cpanel/model/models"
], function(UIComponent, Device, models) {
"use strict";
return UIComponent.extend("com.uk.ia.air.cpanel.Component", {
metadata: {
manifest: "json"
},
/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* #public
* #override
*/
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// set the device model
this.setModel(models.createDeviceModel(), "device");
// create the views based on the url/hash
this.getRouter().initialize();
}
});
});
The ID of a component can be manually assigned when instantiating the component
Either with sap/ui/core/Component#createAPI
Or with new ComponentContainer(/*...*/)API
With ComponentContainer
E.g. in index.html when bootstrapping:
<head>
<!-- ... -->
<script id="sap-ui-bootstrap" src="https://ui5.sap.com/resources/sap-ui-core.js"
data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
data-sap-ui-async="true"
data-sap-ui-resourceroots='{"demo": './'}'
...
></script>
</head>
<body id="content" class="sapUiBody">
<div data-sap-ui-component
data-id="myComponentContainer"
data-name="demo"
data-height="100%"
data-settings='{"id": "myComponent"}'
></div>
</body>
The module, that can create a ComponentContainer in index.html, is sap/ui/core/ComponentSupport. Once it's loaded, it will look for the HTML element that has the attribute data-sap-ui-component. That HTML element should provide constructor settings for the ComponentContainer which provides settings for the component constructor same as in Component.create.
With Component.create()
Component.create({ // Component required from "sap/ui/core/Component"
id: "myComponent", // or this.getView().createId("myComponent") if applicable
name: "demo",
// ...
});
For more information, see Stable IDs: All You Need to Know.
Additionally, every view must have an ID as well so that the given component ID is included in the full ID of an element. Otherwise, the component ID won't be available even if we defined it in the component factory function. View IDs can be set via the property id and viewId respectively in the application descriptor (manifest.json):
sap.ui5 / rootView /id
sap.ui5 / routing / targets /targetName/viewId
You can change the autogenerated ID of the extended component if you override the constructor of the UIComponent. With your code it should look like this:
return UIComponent.extend("com.uk.ia.air.cpanel.Component", {
constructor: function(sId, mSettings) {
UIComponent.call(this, "MyComponentId", mSettings);
},
metadata: {
manifest: "json"
},
/**
* The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
* #public
* #override
*/
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// set the device model
this.setModel(models.createDeviceModel(), "device");
// create the views based on the url/hash
this.getRouter().initialize();
}
});
});

How to set ID of a XML-View in a component environment?

I want to access a view's controller from a custom module with some utility functions. Basically you can do this that way:
var oController = sap.ui.getCore().byId("__xmlview1").getController();
The problem is that the above coding will not work in a real environment because __xmlview1is dynamically created by the framework. So I tried to find a possibility to set the ID of the view during instantiation. The problem is - I could not find one:
Trying to give the view an ID in the view.xml file does not work:
<mvc:View
controllerName="dividendgrowthtools.view.dividendcompare"
id="testID"
xmlns="sap.m"
...
Trying to set the ID in the router configuration of the component does not work either:
...
name: "Dividend Compare",
viewId: "test",
pattern: "Dividend-Compare",
target: "dividendcompare"
...
The problem is that I do not have direct control over the instantiation of the XML view - the component respectively the router does it.
So, is there a solution for that problem? Or at least a save way to get the view ID by providing the name of the view?
You should have a look at the SAPUI5 EventBus.
I am pretty sure, you want to let the controller to do something with the dividentcompare view. With the SAPUI5 Eventbus, you can publish actions from one controller to another witout braking MVC patterns.
In your dividendcompare.controller.js:
onInit : function() {
var oEventBus = sap.ui.getCore().getEventBus();
oEventBus.subscribe("MyChannel", "doStuff", this.handleDoStuff, this);
[...]
},
handleDoStuff : function (oEvent) {
var oView = this.getView();
[...]
}
Now, in your anothercontroller.controller.js:
onTriggerDividendStuff : function (oEvent){
var oEventBus = sap.ui.getCore().getEventBus();
oEventBus.publish("MyChannel", "doStuff", { [optional Params] });
}
You are now able to get the view from the dividentcontroller in every case from every other controller of your app. You dont access the view directly, this would brake MVC patterns, but can pass options to its controller and do the handling there.