This blog post explaining a possible “hack” to answer the following questions: How to accomplish a better integration of UI5 apps with IFrames in SAP Build Work-Zone.
Situation:
- CAP Multi-Tenant application with UI5 (freestyle/fiori-elements) application
- SAP Build Work Zone (Standard Edition) is used by the customer. The customer wants to include the application into his Launchpad.
- A integration using content providers is – as far as I understood – not possible for SaaS applications ==> Integration of CAP app is done using IFrame Integration.
Problem: IFrame Integration diesables the following functionality:
- Language of the app does not correspond to the language of the launchpad
- Semantic Navigation (Semantic Navigation from Launchpad is not updated if Semantic Navigation changes within App)
- Cross Application Navigation
- Missing functionality for Variant Management / Shell Navigation / …
Note that the described solution is really far from beeing perfect, and is based on my limited knowledge about the overall integration of Build-Workzone with SaaS apps. There might be more “standard” ways of accomplishing the required functionality – from the documentation I was not able to find any though. If you know any –> please let me know in the comments.
Solution:
Step 1: Configure Build Workzone to pass intent and default SAP parameters to URL:
This simple change will enable:
- Passing the launchpad theme (note: no custom one)
- Passing the launchpad language
Step 2: Configure the application to parse the inbound hash
The above configuration will most certainly “destroy” your freestyle application, as the semantic navigation from the portal is not available in your manifest.json. To fix that, add the following code to your index.html.
This code will:
- Check if the passed iframe hash contains the semantic object –> if no –> we are not called from the launchpad so ignore
- If yes: we are called from a launchpad. We have to check if navigation parameters (after “&”) are available –> if yes, navigate to that hash. Otherwise clear our hash, as this is not relevant for us.
Step 3: Foreward in-App navigation events to the calling Fiori Launchpad
After changing the navigation in your application, we will have to update the URL of the calling launchpad. It is possible to communicate between your IFrame UI5 application and the Fiori Launchpad using the “postMessage” functionality.
Fiori Launchpad allows calling all UShell services and functions (basically all documented methods in all sap.ushell.services.* classes) using postMessage. One example is shown in the following screenshot:
The postData contains the following attributes (note: I found those attributes using source code analysis of Fiori.. Therefore this is ofc a hack.. 🙂 ):
- service: The Service function from any of the services mentioned here: https://sapui5.hana.ondemand.com/sdk/#/api/sap.ushell.services. In this case we are using the CrossApplicationNavigation Service to trigger the “toExternal” function.
- type: Always “request”
- request_id: A unique ID (guid/number.. whatever you want) which is passed in a possible answer later on
- body: The data to be passed to the function (here “toExternal”)
Using window.addEventListener(“message”,… ) we can retrieve the corresponding answer (in case there is any).
This small “trick” allows us to navigate the calling window (= Fiori Launchpad) to a new hash containing the hash of our own freestyle application using the two services:
- sap.ushell.services.CrossApplicationNavigation.hrefForExternal (to get the target hash)
- sap.ushell.services.CrossApplicationNavigation.toExternal (to navigate to the target hash)
Step 4: Mock sap.ushell.*
I havn’t tried it yet, but it might be possible to completly mock dynamically all sap.ushell services to also enable additional functionality besides routing (i.E. variant management) in your SaaS “IFrame integrated” UI5 application. I will probably give it a try in the next days and update the blog post accordingly.
—
Attached is the complete code (typescript) for your freestyle app. You must initialze the RoutingHelper in your component with your used router and the semantic navigation name of your app in the Fiori Launchpad. Afterwards all navigation must work via the helper “navTo” function of the RoutingHelper singleton.
import Router from "sap/ui/core/routing/Router";
class RoutingHelperDef {
private _router: Router;
private _fioriHash: string;
private _requestId = 0;
private _promises: { [requestId: string]: (value: unknown) => unknown } = {};
init(router: Router, fioriHash: string) {
this._router = router;
this._fioriHash = fioriHash;
this._attachMessageEvent();
}
public navTo(sName: string, oParameters?: object, bReplace?: boolean): void {
this._router.navTo(sName, oParameters, undefined, bReplace);
setTimeout(() => {
this._informParent();
}, 0);
}
private async _informParent() {
if (!window.parent) {
return;
}
const requestId = "navigation_hrefForExternal_" + this._requestId;
this._requestId += 1;
const hrefForExternalResult = new Promise(async (resolve) => {
this._promises[requestId] = resolve;
let hash = this._fioriHash;
if ( window.location.hash.length > 1 ) {
hash += "&" + window.location.hash.substring(1);
}
const postData = {
"service": "sap.ushell.services.CrossApplicationNavigation.hrefForExternal",
type: "request",
request_id: requestId,
body: {
oArgs: {
target: {
shellHash: hash
}
}
}
};
window.parent.postMessage(JSON.stringify(postData), "*");
});
const resp = await hrefForExternalResult as { result: string };
const requestIdNav = "toExternal_" + this._requestId;
this._requestId += 1;
const postData = {
"service": "sap.ushell.services.CrossApplicationNavigation.toExternal",
type: "request",
request_id: requestIdNav,
body: {
oArgs: {
target: {
shellHash: resp.result
}
}
}
};
window.parent.postMessage(JSON.stringify(postData), "*");
}
public _attachMessageEvent() {
window.addEventListener("message", (evt) => {
const data = JSON.parse(evt.data) as { request_id: string, body: unknown };
if(!this._promises[data.request_id]) {
return;
}
this._promises[data.request_id](data.body);
})
}
}
let RoutingHelper = new RoutingHelperDef();
export default RoutingHelper;