Technology Blog Posts by Members
cancel
Showing results for 
Search instead for 
Did you mean: 

This post describes one possible way to structure larger Freestyle SAP UI5 applications in a meaningful way.

The focus here is on a component-based architecture, where the application is split into a parent shell and several functional feature components, following a structure that worked well in our projects.

In this compact example, the application is hierarchically split into multiple UI5 components.

Each component is technically a standalone UI5 application with its own Component.js and its own manifest.
This way, the application grows in a controlled manner and can be extended at any time with new features and modules.

We use a classic list/detail application here. Shell as the framework, Feature List as the entry point, and Feature Detail for the object view.
Modules are smaller (reusable) building blocks within a feature.

image-20251221-103415.png

 

 

 

 

 

 

 

 

 

 

 

image-20251221-103523.png

 

 

 

 

1. Shell

The Shell is a standalone UI5 application and represents the technical framework of the application.

It renders the feature components at the appropriate places and is otherwise responsible for layout, navigation, or UI states (e.g. busy states).

App View

The App view is the root view of the Shell and is only responsible for the pure UI structure.

The Shell view is loaded into the FlexibleColumnLayout.

<mvc:View
  controllerName="app.shell.controller.App"
  displayBlock="true"
  height="100%"
  width="100%"
  xmlns="sap.m"
  xmlns:f="sap.f"
  xmlns:mvc="sap.ui.core.mvc">
  <App 
    id="app"
    busy="{appView>/busy}">
      <f:FlexibleColumnLayout
        id="layout"
        layout="{appView>/layout}"
        backgroundDesign="Translucent" />
  </App>
</mvc:View>

App Controller

The corresponding controller does not contain any notable logic except for initializing the view model appView.

sap.ui.define([
  "app/utility/controller/BaseController",
  "sap/ui/model/json/JSONModel"
], function (
  BaseController,
  JSONModel
) {
  "use strict";

  return BaseController.extend("app.shell.controller.App", {

    onInit: function () {
      const oViewModel = this._createViewModel();
      this.setModel(oViewModel, "appView");

      this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
    },

    _createViewModel: function () {
      return new JSONModel({
        busy: false,
        delay: 0,
        layout: "OneColumn",
        previousLayout: ""
      });
    }
  });
});

Shell View

We keep the Shell view fairly simple.

It contains a layout and an area (the mainContents aggregation) where feature components are rendered, e.g. using a ToolPage.

<mvc:View
  controllerName="app.shell.controller.Shell"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc"
  xmlns:tnt="sap.tnt"
  xmlns:core="sap.ui.core"
  displayBlock="true">
  <tnt:ToolPage id="toolPage" sideExpanded="false">
    <tnt:sideContent>
      <tnt:SideNavigation id="sideNavigation">
        <tnt:item>
          <tnt:NavigationList id="navList" expanded="false" selectedKey="">
            <tnt:NavigationListItem
              id="navListItemList"
              text="List"
              icon="sap-icon://list"
              select="onNavToList" />
          </tnt:NavigationList>
        </tnt:item>
      </tnt:SideNavigation>
    </tnt:sideContent>
    <tnt:mainContents>
    </tnt:mainContents>
  </tnt:ToolPage>
</mvc:View>

Shell Controller

After the Shell has been loaded, we directly navigate to Feature List and render it into mainContents of the ToolPage.

In addition, we define two event listeners for the events "setShellBusy" and "hideShellBusy" for later communication.

sap.ui.define([
  "app/utility/controller/BaseController",
  "sap/ui/model/json/JSONModel",
  "sap/ui/Device"
], function (
  BaseController,
  JSONModel,
  Device
) {
  "use strict";

  return BaseController.extend("app.shell.controller.Shell", {

    onInit: function () {
      const oViewModel = this._createViewModel();
      this.setModel(oViewModel, "shellView");
      this.getRouter().getRoute("shell").attachPatternMatched(this._onShellPatternMatched, this);
      this.getOwnerComponent().attachSetShellBusy(this._setShellBusy, this);
      this.getOwnerComponent().attachHideShellBusy(this._hideShellBusy, this);
    },

    _onShellPatternMatched: function () {
      this._navToList();
    },

    _createViewModel: function () {
      return new JSONModel({
        busy: false,
        delay: 50
      });
    },

    _setShellBusy: function() {
      this.getModel("appView").setProperty("/busy", true);
    },
    
    _hideShellBusy: function() {
      this.getModel("appView").setProperty("/busy", false);
    },

    _navToList: function () {
      const bReplace = !Device.system.phone;
      this.getRouter().navTo("list", {}, bReplace);
    }
  });
});

Shell Manifest

The Shell knows its features via "componentUsages" in the manifest.

In our example, we want to display the List feature as the first feature within the Shell.

For this, we define the List feature once as a component in "componentUsages" and create a route.
The "controlAggregation" of List is the "mainContents" aggregation of the ToolPage in the view.

{
  "_version": "1.32.0",
  "sap.app": {
    "id": "app.shell",
    "type": "application",
    "i18n": "i18n/i18n.properties",
    "applicationVersion": { "version": "0.0.1" },
    "title": "{{appTitle}}",
    "description": "{{appDescription}}"
  },
  "sap.ui": {
    "technology": "UI5",
    "fullWidth": true,
    "deviceTypes": { "desktop": true, "tablet": true, "phone": true }
  },
  "sap.ui5": {
    "dependencies": {
      "minUI5Version": "1.120.0",
      "libs": {
        "sap.ui.core": {},
        "sap.m": {},
        "sap.f": {},
        "sap.tnt": {}
      }
    },
    "models": {
      "i18n": {
        "type": "sap.ui.model.resource.ResourceModel",
        "settings": { "bundleName": "app.shell.i18n.i18n" }
      }
    },
    "componentUsages": {
      "list": {
        "name": "app.list",
        "lazy": false
      }
    },
    "routing": {
      "config": {
        "routerClass": "sap.f.routing.Router",
        "viewType": "XML",
        "viewPath": "app.shell.view",
        "controlId": "layout",
        "controlAggregation": "beginColumnPages",
        "async": true,
        "bypassed": {
          "target": "shell"
        }
      },
      "routes": [
        {
          "pattern": "",
          "name": "shell",
          "target": "shell"
        },
        {
          "pattern": "List",
          "name": "list",
          "target": ["shell", "list"]
        }
      ],
      "targets": {
        "shell": {
          "viewName": "Shell",
          "viewId": "shell",
          "viewLevel": 1
        },
        "list": {
          "type": "Component",
          "usage": "list",
          "parent": "shell",
          "controlId": "toolPage",
          "controlAggregation": "mainContents"
        }
      }
    },
    "rootView": {
      "viewName": "app.shell.view.App",
      "type": "XML",
      "async": true,
      "id": "app"
    }
  }
}

Shell Component

Besides the usual router initialization, we only provide the two events already mentioned: setShellBusy and hideShellBusy.

sap.ui.define([
	"app/shell/model/models",
	"sap/ui/core/UIComponent"
], function(
	models,
	UIComponent
) {
	"use strict";

	return UIComponent.extend("app.shell.Component", {
		metadata: {
			manifest: "json",
			events: {		
				setShellBusy: {},
				hideShellBusy: {}
			}
		},
		init: function () {
            UIComponent.prototype.init.apply(this, arguments);
            this.setModel(models.createDeviceModel(), "device");
            this.getRouter().initialize();
        },
		destroy: function() {			
			UIComponent.prototype.destroy.apply(this, arguments);		
		},
		getContentDensityClass: function() {
			if (this._sContentDensityClass === undefined) {				
				if (document.body.classList.contains("sapUiSizeCozy") ||                
					document.body.classList.contains("sapUiSizeCompact")) {
					this._sContentDensityClass = "";				
				} else if (!Device.support.touch) {				
					this._sContentDensityClass = "sapUiSizeCompact";				
				} else {				
					this._sContentDensityClass = "sapUiSizeCozy";
				}
			}
			return this._sContentDensityClass;
		}
	});
});

2. Feature List

Feature components are also technically standalone UI5 applications and can be executed separately.

Similar to the Shell, we can link feature components (List/Detail) and also integrate smaller modules.

Feature List decides when to navigate to Detail.

In the case of a list/detail linking, we pass the necessary parameters during navigation. Apart from that, Feature List does not know any further details about Feature Detail.

App View

Structured similarly to the Shell. The FlexibleColumnLayout is important here for the interaction between List and Detail.

Layout handling is bound to the view model.

<mvc:View
  controllerName="app.list.controller.App"
  displayBlock="true"
  height="100%"
  xmlns="sap.m"
  xmlns:f="sap.f"
  xmlns:mvc="sap.ui.core.mvc">
  <App id="app" height="100%">
    <f:FlexibleColumnLayout
      id="layout"
      height="100%"
      layout="{appView>/layout}"
      backgroundDesign="Translucent" />
  </App>
</mvc:View>

App Controller

Here we initialize the view model and add a new event listener for the component event setLayout.

sap.ui.define([
	"app/utility/controller/BaseController",
	"sap/ui/model/json/JSONModel"
], function (BaseController, JSONModel) {
  "use strict";

  return BaseController.extend("app.list.controller.App", {
    onInit: function () {
		const oViewModel = this._createViewModel();
		this.setModel(oViewModel, "appView");
		this.getOwnerComponent().attachSetLayout(this._onSetLayout, this);
    },

    _onSetLayout: function (oEvent) {
		const sLayout = oEvent.getParameter("layout");
		this.getModel("appView").setProperty("/layout", sLayout);
    },

    _createViewModel: function() {
		const oViewModel = new JSONModel({
				busy: false,
				delay: 0
			});
			return oViewModel;
		}
  });
});

List View

For simplicity, a normal list with some dummy items:

<mvc:View
  controllerName="app.list.controller.List"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc"
  displayBlock="true">
  <Page title="List Feature" class="sapUiContentPadding">
    <content>
      <List id="demoList" mode="SingleSelectMaster" itemPress="onItemPress">
        <StandardListItem title="Item 1000" description="1000000000" type="Active" />
        <StandardListItem title="Item 2000" description="1000000001" type="Active" />
        <StandardListItem title="Item 3000" description="1000000002" type="Active" />
      </List>
    </content>
  </Page>
</mvc:View>

List Controller

Without any major binding logic, we have direct navigation to the Detail feature here, passing the item ID as a parameter.

sap.ui.define([
  "app/utility/controller/BaseController",
  "sap/ui/Device",
  "sap/ui/model/json/JSONModel"
], function (BaseController, Device, JSONModel) {
  "use strict";

  return BaseController.extend("app.list.controller.List", {

    onInit: function () {
      const oViewModel = this._createViewModel();
      this.setModel(oViewModel, "listView");
      this.getRouter().getRoute("list").attachPatternMatched(this._onListMatched, this);
    },

    _onListMatched: function () {
      // maybe further binding logic
    },

    onItemPress: function (oEvent) {
      const oItem = oEvent.getParameter("listItem");
      const sId = oItem.getDescription();

      const bReplace = !Device.system.phone;
      this.getRouter().navTo("detail", { id: sId }, bReplace);
    },

    _createViewModel: function () {
      const oViewModel = new JSONModel({
        busy: false,
        delay: 0,
        noDataText: this.getResourceBundle().getText("noDataText")
      });
      return oViewModel;
    }

  });
});

List Manifest

As in the Shell, we integrate the next feature (Detail) in the manifest of the Feature List component.

Feature Detail also receives its own route.

{
  "_version": "1.32.0",
  "sap.app": {
    "id": "app.list",
    "type": "component",
    "i18n": "i18n/i18n.properties",
    "applicationVersion": { "version": "0.0.1" },
    "title": "{{appTitle}}",
    "description": "{{appDescription}}"
  },
  "sap.ui": {
    "technology": "UI5",
    "fullWidth": true,
    "deviceTypes": { "desktop": true, "tablet": true, "phone": true }
  },
  "sap.ui5": {
    "dependencies": {
      "minUI5Version": "1.120.0",
      "libs": {
        "sap.ui.core": {},
        "sap.m": {},
        "sap.f": {}
      }
    },
    "componentUsages": {
      "detail": {
        "name": "app.detail",
        "lazy": false
      }
    },
    "contentDensities": { "compact": true, "cozy": true },
    "models": {
      "i18n": {
        "type": "sap.ui.model.resource.ResourceModel",
        "settings": { "bundleName": "app.list.i18n.i18n" }
      }
    },
    "routing": {
      "config": {
        "routerClass": "sap.f.routing.Router",
        "viewType": "XML",
        "viewPath": "app.list.view",
        "controlId": "layout",
        "controlAggregation": "beginColumnPages",
        "async": true,
        "bypassed": { "target": "list" }
      },
      "routes": [
        { "pattern": "", "name": "list", "target": "list" },
        { "pattern": "detail/{id}", "name": "detail", "target": "detail" }
      ],
      "targets": {
        "list": { "viewName": "List", "viewId": "list", "viewLevel": 1 },
        "detail": {
          "type": "Component",
          "usage": "detail",
          "controlAggregation": "midColumnPages"
        }
      }
    },
    "rootView": {
      "viewName": "app.list.view.App",
      "type": "XML",
      "async": true,
      "id": "app"
    }
  }
}

3. Feature Detail

Feature Detail also has an App view and an App controller, but in this example they do not contain any significant logic. Therefore, I omit them here. The same also applies to "Component.js".

Detail View

For this example, we would simply assume that the detail view is a simple ObjectPageLayout. Then, we embed additional modules in the individual sections, each within a ComponentContainer. For simplicity, I will only cover the Comments module.

The ComponentContainer automatically renders the Comments module which we specify in the usage property (“moduleComments” is the componentUsages name from the manifest).

<mvc:View
	controllerName="app.detail.controller.Detail"
	xmlns="sap.m"
	displayBlock="true"
	xmlns:mvc="sap.ui.core.mvc"
	xmlns:uxap="sap.uxap"
	xmlns:core="sap.ui.core">
	<uxap:ObjectPageLayout
        id="objectPageLayout"
		showTitleInHeaderContent="true"		
		showFooter="true"
		alwaysShowContentHeader="true"
		preserveHeaderStateOnScroll="false"
		headerContentPinnable="true"
		isChildPage="true"
		upperCaseAnchorBar="false"
		useIconTabBar="true"
		enableLazyLoading="true">
		<uxap:headerTitle>
			<uxap:ObjectPageDynamicHeaderTitle
				id="objectPageDynamicHeaderTitle"
				areaShrinkRatio="5:1:1">
				<uxap:heading>
						<Title
							id="objectPageDynamicHeaderTitleHeadingTitle"
							wrapping="true"
							text="Feature Detail: {detailView>/id}"/>
				</uxap:heading>
			</uxap:ObjectPageDynamicHeaderTitle>
		</uxap:headerTitle>
		<uxap:headerContent>
		</uxap:headerContent>
		<uxap:sections>
		
			<!-- Modul Comments -->
			<uxap:ObjectPageSection title="Comments">
				<uxap:subSections>
					<uxap:ObjectPageSubSection mode="Expanded">
						<core:ComponentContainer
							id="moduleCommentsContainer"
							usage="moduleComments"
							async="true"
							componentCreated="onComponentCommentsCreated"/>
					</uxap:ObjectPageSubSection>
				</uxap:subSections>
			</uxap:ObjectPageSection>

			<!-- Modul History -->
			<uxap:ObjectPageSection title="History">
				<uxap:subSections>
					<uxap:ObjectPageSubSection mode="Expanded">
						<!-- <core:ComponentContainer
							id="moduleHistoryContainer"
							usage="moduleHistory"
							async="true"
							componentCreated="onComponentHistoryCreated"/> -->
							<Text text="History"/>
					</uxap:ObjectPageSubSection>
				</uxap:subSections>
			</uxap:ObjectPageSection>

			<!-- Modul Flow -->
			<uxap:ObjectPageSection title="Flow">
				<uxap:subSections>
					<uxap:ObjectPageSubSection mode="Expanded">
						<!-- <core:ComponentContainer
							id="moduleFlowContainer"
							usage="moduleFlow"
							async="true"
							componentCreated="onComponentFlowCreated"/> -->
							<Text text="Flow"/>
					</uxap:ObjectPageSubSection>
				</uxap:subSections>
			</uxap:ObjectPageSection>
		</uxap:sections>
		<uxap:footer>
			<OverflowToolbar>
			</OverflowToolbar>
		</uxap:footer>
	</uxap:ObjectPageLayout>
</mvc:View>

Detail Controller

There are multiple points here.

First, we implement a simple view binding (_bindView) where we bind the ID passed from List to our view.

Second, some functions for chapter 5 (communication) are already defined which trigger events in other components.
For example, setting or clearing the busy state in the Shell (_setShellBusy and _hideShellBusy) or controlling the layout in List (_listSetLayout).

And lastly, there are handler functions for the componentCreated event of the module ComponentContainer.

Here I am already anticipating something:

As soon as the Comments module was created, the function onComponentCommentsCreated is triggered. In this function, we pass an API created by "_createCommentsApi" to the module, through which "Comments" can call functions of the parent component.

This is one possible way of communication between components.

Another way, which I will discuss later, is accessing the required component via a defined ID. In our example we use a helper function called "getComponent". More on this later.

sap.ui.define([
	"app/utility/controller/BaseController",
	"sap/ui/model/json/JSONModel"
], function(
	BaseController,	
	JSONModel
) {
	"use strict";

	return BaseController.extend("app.detail.controller.Detail", {

		onInit: function() {
			const oViewModel = this._createViewModel();
			this.setModel(oViewModel, "detailView");

			this.getRouter().getRoute("detail").attachPatternMatched(this._handleRouteMatched, this);
		},	
		
		getEntityId: function() {
			return this.getModel("detailView").getProperty("/id");
		},

		_createViewModel: function() {
			const oViewModel = new JSONModel({
				busy: false,
				delay: 50,
				id: ""
			});
			return oViewModel;
		},

		_handleRouteMatched: async function(oEvent) {				
			try {
				this._listSetLayout("TwoColumnsMidExpanded");
				const sId = oEvent.getParameter("arguments").id;
				this._setShellBusy();
				await this._bindView(sId);
				await this._bindCommentsModule(sId);
			} catch(oError) {
				// Error handling
			} finally {
				this._hideShellBusy();
			}
		},

		_createCommentsApi: function () {
			return {
				getEntityId: this.getEntityId.bind(this),
				setShellBusy: this._setShellBusy.bind(this),
				hideShellBusy: this._hideShellBusy.bind(this)
			};
		},
		
		_bindView: async function(sId) {
			this.getModel("detailView").setProperty("/id", sId);
			return Promise.resolve(true);
		},
		
		_bindCommentsModule: async function(sId) {
			const oComponent = this.byId("moduleCommentsContainer").getComponentInstance();
			if(oComponent) {
				oComponent.fireSetEntityId({id: sId});									
			}
			return Promise.resolve(true);			
		},

		/* --- Shell Busy State Handling --- */
		_setShellBusy: function() {
			try {
				this.getComponent("app.shell").fireSetShellBusy();
			} catch (oError) {
				//error handling
			}
		},

		_hideShellBusy: function() {
			try {
				this.getComponent("app.shell").fireHideShellBusy();
			} catch (oError) {
				//error handling
			}
		},

		/* --- Feature List Events ---  */
		_listSetLayout: function(sLayout) {
			try {
				this.getComponent("app.list").fireSetLayout({layout: sLayout});
			} catch (oError) {
				//error handling
			}
		},

		/* --- Module Creation Handling --- */
		onComponentCommentsCreated: function(oEvent) {
			const oComponent = oEvent.getParameter("component");
			oComponent.oAPI = this._createCommentsApi();
			
			this._bindCommentsModule(this.getModel("detailView").getProperty("/id"));
		},		

		onComponentHistoryCreated: function(oEvent) {
		},

		onComponentFlowCreated: function() {
		}
	});
});

Detail Manifest

The Detail feature can contain additional modules that are used within this feature.

In our example, these would be Comments, History, and Flow, for which we have already created the appropriate ComponentContainers in the view. The modules are automatically loaded into the respective ComponentContainers when opening the Detail feature.

{
  "_version": "1.32.0",
  "sap.app": {
    "id": "app.detail",
    "type": "application",
    "i18n": "i18n/i18n.properties",
    "applicationVersion": {
      "version": "0.0.1"
    },
    "title": "{{appTitle}}",
    "description": "{{appDescription}}",
    "dataSources": {
      "mainService": {
        "uri": "/sap/opu/odata/SERVICE",
        "type": "OData",
        "settings": {
          "localUri": "localService/metadata.xml",
          "odataVersion": "2.0"
        }
      }
    }
  },
  "sap.ui": {
    "technology": "UI5",
    "fullWidth": true,
    "icons": {
      "icon": "",
      "favIcon": "",
      "phone": "",
      "phone@2": "",
      "tablet": "",
      "tablet@2": ""
    },
    "deviceTypes": {
      "desktop": true,
      "tablet": true,
      "phone": true
    }
  },
  "sap.ui5": {
    "flexEnabled": true,
    "dependencies": {
      "minUI5Version": "1.97.0",
      "libs": {
        "sap.ui.core": {},
        "sap.m": {},
        "sap.f": {},
        "sap.uxap": {}
      }
    },
    "componentUsages": {
      "moduleComments": {
        "name": "app.module.comments",
        "lazy": true
      },
      "moduleHistory": {
        "name": "app.module.history",
        "lazy": true
      },
      "moduleFlow": {
        "name": "app.module.flow",
        "lazy": true
      }
    },
    "contentDensities": {
      "compact": true,
      "cozy": true
    },
    "models": {
      "i18n": {
        "type": "sap.ui.model.resource.ResourceModel",
        "settings": {
          "bundleName": "app.detail.i18n.i18n"
        }
      }
    },
    "routing": {
      "config": {
        "routerClass": "sap.f.routing.Router",
        "viewType": "XML",
        "viewPath": "app.detail.view",
        "controlId": "app",
        "controlAggregation": "pages",        
        "async": true
      },
      "routes": [
        {
          "pattern": "Detail/{id}",
          "name": "detail",
          "target": [
            "detail"
          ]
        }
      ],
      "targets": {
        "detail": {
          "viewName": "Detail",
          "viewLevel": 1,
          "viewId": "detail"
        }
      }
    },
    "rootView": {
      "viewName": "app.detail.view.App",
      "type": "XML",
      "async": true,
      "id": "app"
    }
  }
}

4. Modules

Modules are smaller, reusable components that are used exclusively (at least in our example) within a feature.

What is defined as a module depends on the specific use case.

In our projects, modules are small functional components that are embedded in multiple features.

For example, a UI5 application with a table for change history, notes (in our example Comments), etc.

The module takes care of its own UI and data retrieval.

The structure can remain very clear and compact:

image-20260121-121142.png

 

 

 

 

 

5. Communication between Components

This is the most important part.

For communication between components (e.g. passing data or triggering functions), there are multiple possibilities.

Some use the EventBus, others build an additional service class.

Here I want to explain two possibilities that we have used in our projects.

Event handling with sap.ui.core.ComponentRegistry

We define custom events in Component.js for functions that we want to use in the respective component.

The following scenario:

image-20260121-141024.png

 

 

 

 

 

 

As soon as we click an entry in the list of Feature List, we want to see the details in Feature Detail on the right side.
For that, we need to set the layout of the FlexibleColumnLayout in the App.controller.js of Feature List to "TwoColumnsMidExpanded".

We already prepared the necessary steps.

1. Define the event "setLayout" in the List component.

metadata: {
  manifest: "json",
  events: {
	setLayout: {
	  parameters: {
		layout: {type: "string"}
	  }
	}
  }
}

2. Define the event handler in App.controller.js (List).

onInit: function () {
    //...
	this.getOwnerComponent().attachSetLayout(this._onSetLayout, this);
    //...
},

_onSetLayout: function (oEvent) {
	const sLayout = oEvent.getParameter("layout");
	this.getModel("appView").setProperty("/layout", sLayout);
},

To be able to use this event in Feature Detail, we created a helper function getComponent in our example.

getComponent: function (sAppId) {
  let oResultComponent;

  const aComponentNames = Object.keys(sap.ui.core.ComponentRegistry.all());
  aComponentNames.forEach((sComponentName) => {
    const oComponent = sap.ui.core.ComponentRegistry.all()[sComponentName];
    if (oComponent.getManifestEntry("sap.app").id === sAppId) {
      oResultComponent = oComponent;
    }
  });

  return oResultComponent;
}

We pass the app ID to this function, which we previously defined in the component’s manifest.json, and which identifies the component we want to access.

This function is reliable as long as app IDs are maintained properly and kept unique.

This helper can also be centralized. In our example, the function is located in the BaseController, which is outsourced into a separate Utility library.

Utility/
└─ src/
   └─ app/
      └─ utility/
         ├─ controller/
         │  └─ BaseController.js
         ├─ library.js
         ├─ manifest.json
         ├─ package.json
         └─ ui5.yaml

Since each feature inherits from the BaseController, Feature Detail can now trigger the layout event, for example.

_listSetLayout: function(sLayout) {
	try {
		this.getComponent("app.list").fireSetLayout({layout: sLayout});
	} catch (oError) {
		//error handling
	}
},

The ID "app.list" is the ID that we defined in the manifest.json of Feature List.

 "sap.app": {
    "id": "app.list"

As soon as the event is triggered, Feature Detail opens in the right area of the FlexibleColumnLayout.

image-20260121-140859.png

 

 

 

 

 

 

 

Passing an API

Another possibility that we also like to use is passing an “API” to another component.

Here we create an object with callbacks and pass it to a newly created or rendered component.

We have already prepared this in Feature Detail as well:

_createCommentsApi: function () {
    return {
        getEntityId: this.getEntityId.bind(this),
        setShellBusy: this._setShellBusy.bind(this),
        hideShellBusy: this._hideShellBusy.bind(this)
    };
},

As soon as the Comments module was created by the ComponentContainer, we pass the API with the callbacks.

onComponentCommentsCreated: function(oEvent) {
    const oComponent = oEvent.getParameter("component");
    oComponent.oAPI = this._createCommentsApi();
     
    this._bindCommentsModule(this.getModel("detailView").getProperty("/id"));
},	

This way, the Comments module is able to execute functions of the parent component.

A very rough example:

We set the busy state of the Shell before the view binding via the Feature Detail API in the function "_setEntityId" and clear it again afterwards.

Comments Controller:

sap.ui.define([
  "app/utility/controller/BaseController",
	"sap/ui/model/json/JSONModel"
], function (
    BaseController, 
    JSONModel
) {
    "use strict";

    return BaseController.extend("app.module.comments.controller.Comments", {

        onInit: function () {
            const oViewModel = this._createViewModel();
			this.setModel(oViewModel, "moduleView");

            this.getOwnerComponent().attachSetEntityId(this._setEntityId, this);
        },

        _setEntityId: function(oEvent) {
            this.getOwnerComponent().oAPI.setShellBusy();
            this.getModel("moduleView").setProperty("/id", oEvent.getParameter("id"));
            this.getOwnerComponent().oAPI.hideShellBusy();
        },

        _createViewModel: function() {
			const oViewModel = new JSONModel({
				busy: false,
				delay: 50,
				id: ""
			});
			return oViewModel;
		}
    });
});

6. Conclusion

The examples shown here are intended only as an inspiration, and I therefore kept them intentionally rough and simple.

If you notice early on (or already know) that the application will grow significantly in the future, it can be worth thinking about a larger architecture already at this stage.

If you wait too long, dependencies quickly arise and the restructuring becomes unnecessarily complex and time-consuming.

A clean structure does not only pay off in readability, but also makes later maintenance easier.

 

4 Comments
Labels in this area