Skip to Content
Technical Articles

Leveraging embedded routing in multiple components to structure big UI5 applications

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. 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!

2 Comments
You must be Logged on to comment or reply to a post.
  • Hi,

    It is a nice feature, but 50-60 views for a mobile apps… It is not a Fiori like application, isn’t it ? 🙂

    Regards,

    Joseph

    • Nope, not at all, haha. If you’re having to rely on something like this, you’re probably not developing a Fiori like app (though it certainly can be done, depending on how you like to structure your applications). Of course, the Fiori approach is not always feasible.

      We do try to warn customers that the recommended approach is to create small, role-based applications that focus on a specific action over a single kind of semantic object, but sometimes they just want to keep their UX and adapt their stuff so they can run it in Chrome or Firefox.

      Or for whatever reason there are projects where we don’t have a Launchpad available. In these cases having a fullscreen Root component where you access your different subcomponents using some of the tile controls that UI5 gives you (while checking backend for roles and so on) can be kind of the next best thing. You still have a “huge” UI5 app, but really what you’re doing is creating several, small Fiori-like apps and embedding them in a single, bigger one that acts as our Launchpad, which can be similar to a Fiori Launchpad or it can be something completely different.

      You could then deploy that app over something like a Tomcat Server and have something pretty similar to Fiori UX (or not, depending on customer preference) while maintaining full control over the code to deal with any possible requirement that might or might not be possible to implement over a more traditional Launchpad.

      And of course, as I said in the main post, there’s also the case of mobile apps. Most customers are not going to want to install several small apps in their phones, so sometimes you need to cram all functionality in a single mobile app and modularize.