Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
juanjos_prez
Discoverer
Hello,

 

This blog post aims to talk about a technique that, while possible to implement, is not really documented (as far as I know) as such across the extensive notes and documentation that UI5 offers. Specifically, we're talking about how we can use embedded routing (that is, multiple routers working in tandem to allow multiple components, each with their own routing, to be in a single UI5 app).

We found this technique especially useful in large-scale applications with upwards of 50-60 view-controller pairs, where it can be really useful to modularize and isolate the different parts of the app into their own individual silos, as if every part of the app was its own small application.

This approach to development has been getting more traction as of lately under the name Micro Frontend, since it is analogous to the Micro Service development approach on the backend (https://micro-frontends.org/). Technically the Micro Frontend approach advocates for multiple micro web apps that can use different frameworks embedded onto a single SPA, but here we're just going to detail how to embed multiple micro UI5 applications onto a bigger UI5 app which will act as a container.

A typical use case for these kinds of apps is Apache Cordova, where big hybrid mobile applications can have upwards of a hundred views. In these cases, modularizing into multiple components to isolate logic and UI, as well as to allow future development and code scaling without having to save dozens of XML and JS files into a single folder can be essential.

We'll illustrate this technique by using a sample application (the GitHub link is available at the end of the article). First we'll show a diagram showing the UI structure:



 

This basic diagram gives us an idea of what we want to accomplish with the sample app. Namely:

  • We want a Root Component which contains the navigation list (in the master) and the multiple views which contain the Component Containers for our subcomponents (in the detail).

  • We want two Sub Components, each with their own routing, which have compatible routes with the root router.


 

The basic routing scheme we need to accomplish this is illustrated here:

Root Router:


      "routes": [
{
"pattern": "component1/:viewPattern:",
"name": "sub1ComponentView",
"target": ["masterView", "component1View"]
},
{
"pattern": "component2/:viewPattern:",
"name": "sub2ComponentView",
"target": ["masterView", "component2View"]
}
],
"targets": {
"masterView": {
"viewName": "Master",
"viewId": "master",
"title": "Navigation List",
"controlAggregation": "masterPages"
},
"component1View": {
"viewName": "Component1View",
"viewId": "component1View",
"controlAggregation": "detailPages"
},
"component2View": {
"viewName": "Component2View",
"viewId": "component2View",
"controlAggregation": "detailPages"
}
}

SubComponent Router:


      "routes": [
{
"pattern": "component1/view1",
"name": "sub1view1",
"target": "targetSub1View1"
},
{
"pattern": "component1/view2",
"name": "sub1view2",
"target": "targetSub1View2"
}
],
"targets": {
"targetSub1View1": {
"viewName": "Sub1View1",
"viewLevel": 1,
"viewId": "idSub1View1",
"controlAggregation": "pages"
},
"targetSub1View2": {
"viewName": "Sub1View2",
"viewLevel": 2,
"viewId": "idSub1View2",
"controlAggregation": "pages"
}

 

As can be surmised by the routing pattern, the idea here is to make the Root router and the Sub routers routes compatible with each other. We accomplish this by ending all patterns of the Root router with a ":viewPattern:" optional routing parameter and implementing all patterns of the Sub routers as subroutes of the component route in the Root router. Breaking this pattern (for example, having a hypothetical view in a subcomponent with a pattern "view3"), would mean that the Sub router would route correctly, but because the Root router does not have any pattern with that name, it would show no component container and therefore the detail would show up empty.

 

As for the folder structure, a possible one would be this:



We're using the UI5 basic template found here: https://github.com/SAP/openui5-basic-template-app

An argument could be made in favour of storing the subcomponents in a folder inside the RootComponent, to reflect the UI structure. Speaking for myself, I like the immediacy of having all components available in a single click by using a flat structure instead of a nested one, but your mileage may vary. Both approaches are perfectly possible leveraging the flexibility UI5 offers with namespaces in the different modules that comprise the app.

 

The RootComponent should have the following structure:



That is, one view for the Root SplitApp control, one for the master List, plus one for each component container, for all components that we wish to expose.

The component container views have been implemented like this:

View
<mvc:View busyIndicatorDelay="0"
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns="sap.m"
controllerName="ui.demo.multiComponent.app.RootComponent.controller.Component1View">
<core:ComponentContainer id="sub1CmpCtr" height="100%" />
</mvc:View>

Controller
sap.ui.define(
["sap/ui/core/mvc/Controller", "sap/ui/core/Component", "../model/formatter"],
function(Controller, Component, formatter) {
"use strict";

return Controller.extend(
"ui.demo.multiComponent.app.RootComponent.controller.Component1View",
{
formatter: formatter,

onInit: function() {
if (!Component.get("sub1Component")) {
Component.create({
name: "ui.demo.multiComponent.app.Sub1Component",
id: "sub1Component"
}).then(
function(Component) {
this.getView()
.byId("sub1CmpCtr")
.setComponent(Component);
}.bind(this)
);
}
}
}
);
}
);

Not much to note here, the view simply contains a ComponentContainer control that only gets instantiated and filled with a Component when the onInit method of the controller gets triggered. This only happens the first time we navigate to a new component, thus allowing components to be lazy loaded (which should help performance if the user only navigates to some specific parts of the app while leaving others alone in a session).

As for the subcomponents, they are perfectly normal full screen (instead of master detail) UI5 components, with two caveats:

  • As discussed earlier, all routes should be prefixed with the component route

  • The App.view.xml file should not have a sap.m.Shell inside since that UI control is already emitted by the Root Controller


 

With this, we have almost every piece of the puzzle figured out, but we still need to know:

  • How to trigger a navigation from the root component into the subcomponents (necessary for the master list)

  • How to trigger a navigation from one subcomponent view into a different subcomponent view


 

Let's answer the first question.

Triggering a navigation from a controller owned by the RootComponent into a specific view is as simple as:
this.getOwnerComponent()
.getRouter()
.navTo("sub1ComponentView", {
viewPattern: "view1"
});

The reason this works is that we're telling the Root router to write the hash "sub1ComponentView/view1". When it does that both the Root router and the sub component router capture that event, thus the root router navigates to the correct component container and the sub component router navigates to the correct view, provided the above constraints on the routing structure of the app are met.

Knowing this, answering the second question is trivial.

From the controller of the view of the subcomponent we fire an event using the EventBus attached to the core (not the one attached to the subcomponent, since obviously that one won't be able to emit events outside its domain).
sap.ui.getCore().getEventBus().publish("nav", "sub2component:view1");

And then, from the RootComponent, we should have a dedicated method with the following code:
sap.ui.getCore().getEventBus().subscribe("nav", "sub2component:view1", function() {
this.
.getRouter()
.navTo("sub2ComponentView", {
viewPattern: "view1"
});
}, this);

 

Note that the reason we're doing this in RootComponent instead of Sub2Component is that we're not guaranteed at any point that Sub2Component will be instantiated, since we're lazy loading all our components.

With this, we have all the elements we need, and this is the result of our hard work:



 

Which may not be the prettiest UI5 application you've seen, but it's a proof of concept so oh well.

 

 

An example use case of this coding pattern was used when a certain customer asked us to implement a "Market Monitor" mobile app. What started out as something simple enough (provide a means for their sales people to log their activities and visits to customer to CRM) soon grew, until they were asking to be able to take notes, see info sheets for their products, stock levels, see the data of their contacts inside the customers... until the structure was something like this:



Implementing this as a monolithic app would have been even more of a challenge than it needed to be. With this, every part of the app was implemented and tested in isolation before easily merging it with the whole.

 

Finally, as promised, here's the GitHub repo:

https://github.com/JuanjoPP/openui5-multicomponent-app

 

Hope you found this useful!
3 Comments
Labels in this area