- 1. Shell
- App View
- App Controller
- Shell View
- Shell Controller
- Shell Manifest
- Shell Component
- 2. Feature List
- App View
- App Controller
- List View
- List Controller
- List Manifest
- 3. Feature Detail
- Detail View
- Detail Controller
- Detail Manifest
- 4. Modules
- 5. Communication between Components
- Event handling with sap.ui.core.ComponentRegistry
- Passing an API
- 6. Conclusion
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.
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:
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:
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.yamlSince 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.
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.