This blog post is a big one 🚀. It’s about how to organize your code in your SAPUI5 project. It doesn’t matter whether you look at official SAP Standard apps or at custom apps. As soon as a app reaches a certain level of complexity, the main view controller contains over a thousand lines of code.
If you outsource recurring coding in own functions your lines of coding will decrease. However, this is not enough to ensure order if the application is complex. It all becomes a bit messy.
The solution? Organize your code!
Let’s begin. In the following i will show you how i organize my code in SAPUI5 projects via the following controls:
- sap.ui.core.mvc.Controller
sap.ui.model.json.JSONModel
sap.ui.base.Object
sap.ui.base.ManagedObject
sap.ui.define
sap.ui.core.mvc.Controller
Every view has a controller. The class sap.ui.core.mvc.Controller
is used as the base class for every controller.
This way you can access lifecycle event functions as onInit
, onAfterRendering
or functions like getOwnerComponent
out of the box.
sap.ui.define([ "sap/ui/core/mvc/Controller" ], function(Controller) { "use strict"; return Controller.extend("<PROJECT_PATH>.<NAME>", { onInit: function() { //inherited by sap.ui.core.mvc.Controller //this.getOwnerComponent(); //... } }); });
BaseController.js
When generating a project from template different files are created by SAP automatically. One of them is the BaseController.js
.
Instead the view controller extends the sap.ui.core.mvc.Controller
it now extends the BaseController.js
– which at the end extends the sap.ui.core.mvc.Controller
.
This is a great way to outsource coding you use in every controller – Put it in one place.
/controller/BaseController.js
sap.ui.define([ "sap/ui/core/mvc/Controller" ], function(Controller) { "use strict"; return Controller.extend("<PROJECT_PATH>.controller.BaseController", { getResourceBundle: function() { return this.getOwnerComponent().getModel("i18n").getResourceBundle(); } }); });
/controller/View.controller.js
sap.ui.define([ "<PROJECT_PATH>/controller/BaseController" ], function(BaseController) { "use strict"; return BaseController.extend("<PROJECT_PATH>.View", { onInit: function() { //inherited by sap.ui.core.mvc.Controller //this.getView(); //... //inherited by <PROJECT_PATH>.controller.BaseController //this.getResourceBundle(); } }); });
Organize code via the sap.ui.model.json.JSONModel
The sap.ui.model.json.JSONModel
can be used to bind UI5 controls to JavaScript object data. By extending the JSONModel you have a clean way to organize specific code that is responsible for managing specific object data.
Personally, as soon as there is a need for an deep insert create request i say goodbye to the most functionality of the sap.ui.model.odata.v2.ODataModel
and go for a “custom” JSONModel.
I then only use the basic functionality create
, read
, update
and remove
aswell as helper methods like createKey
. That’s it. In the following there is an example of an “custom” JSONModel for an survey object.
/model/Survey.js
sap.ui.define([ "sap/ui/model/json/JSONModel", "sap/base/util/deepClone", "sap/base/util/deepEqual" ], function(JSONModel, deepClone, deepEqual) { "use strict"; return JSONModel.extend("<PROJECT_PATH>.model.Survey", { /** * @class Survey * @summary Example of "custom" JSONModel * @extends sap.ui.model.json.JSONModel * @param {sap.ui.core.mvc.Controller} oController - View Controller * @param {string} sSurveyId - SurveyId */ constructor: function(oController, sSurveyId) { JSONModel.prototype.constructor.call(this, []); this._oServiceModel = oController.getOwnerComponent().getModel(); this._sSurveyId = sSurveyId; }, /** * @memberOf Survey * @description Read the survey * <ul> * <li>Read the survey via {@link Survey._read}</li> * <li>Sets the response to the model</li> * <li>Create undo object via {@link Survey._createUndoObject}</li> * </ul> * @returns {promise} oData Service Call */ read: function() { return new Promise(function(resolve, reject) { this._read().then(function(oResponse) { this.setData(oResponse); this._createUndoObject(); resolve(oResponse); }.bind(this)).catch(function(oError) { reject(oError); }); }.bind(this)); }, /** * @memberOf Survey * @description oData Service Read Request * @returns {promise} oData Service Call * @private */ _read: function() { var oServiceModel = this._oServiceModel, sSurveyId = this._sSurveyId; return new Promise(function(fnResolve, fnReject) { var sPath = oServiceModel.createKey("/SurveySet", { Pernr: sSurveyId }); var oParameters = { urlParameters: { "$expand": "toQuestions,toAnswers" }, success: fnResolve, error: fnReject }; oServiceModel.read(sPath, oParameters); }); }, /** * @memberOf Survey * @description Save the survey * <ul> * <li>Save the survey via {@link Survey._save}</li> * <li>Sets the response to the model</li> * <li>Create undo object via {@link Survey._createUndoObject}</li> * </ul> * @returns {promise} oData Service Call */ save: function() { var oDeepEntity = this.getData(); return new Promise(function(resolve, reject) { this._save(oDeepEntity).then(function(oResponse) { this.setData(oResponse); this._createUndoObject(); resolve(oResponse); }.bind(this)).catch(function(oError) { reject(oError); }); }.bind(this)); }, /** * @memberOf Survey * @description OData Service Create Deep Entity Call * @param {object} oDeepEntity - Data to be saved * @returns {promise} oData Service Call * @private */ _save: function(oDeepEntity) { var oServiceModel = this._oServiceModel; return new Promise(function(fnResolve, fnReject) { var sPath = "/SurveySet"; var oParameters = { success: fnResolve, error: fnReject }; oServiceModel.create(sPath, oDeepEntity, oParameters); }); }, /** * @memberOf Survey * @description creating a deep clone of the model data to be able to determine changes afterwards */ _createUndoObject: function() { this.oUndoObject = deepClone(this.getData()); //maybe adjust maxDepth parameter }, /** * @memberOf Survey * @description undo changes */ undo: function() { this.setData(this.oUndoObject); }, /** * @memberOf Survey * @description Check if there are changes */ hasChanges: function() { var bHasChanges = !deepEqual(this.oUndoObject, this.getData()); return bHasChanges; } }); });
/model/models.js
sap.ui.define([ "sap/ui/model/json/JSONModel", "sap/ui/Device", "<PROJECT_PATH>/model/Survey" ], function(JSONModel, Device, Survey) { "use strict"; return { /** * @description Creation of survey model * @param {sap.ui.core.mvc.Controller} oController - Controller * @param {string} sId - SurveyId * @returns {<PROJECT_PATH>.model.Survey} Survey model */ createSurveyModel: function(oController, sId) { var oModel = new Survey(oController, sId); oModel.setDefaultBindingMode("TwoWay"); return oModel; } }; });
/controller/View.controller.js
sap.ui.define([ "sap/ui/core/mvc/Controller", "<PROJECT_PATH>/model/models" ], function(Controller, models) { "use strict"; return Controller.extend("<PROJECT_PATH>.controller.View", { /** * @description Instantiation of the survey model and reading an entry */ onInit: function() { var sSurveyId = "999999"; this.oSurveyModel = models.createSurveyModel(this, sSurveyId); this.getView().setModel(this.oSurveyModel, "surveyModel"); this.getOwnerComponent().getModel().metadataLoaded().then(function() { this.oSurveyModel.read().then(function(oResponse) { //do sth. (busy states ...) }.bind(this)).catch(function(oErr) { //do sth. (busy states ...) }.bind(this)); }.bind(this)); }, /** * @description save changes if there are any */ save: function() { if (this.oSurveyModel.hasChanges()) { this.oSurveyModel.save().then(function(oResponse) { //do sth. (busy states ...) }.bind(this)).catch(function(oErr) { //do sth. (busy states ...) }.bind(this)); } } }); });
/view/View.view.xml
... <Input value="{ path: 'surveyModel>/PropertyA', type: 'sap.ui.model.type.String'}"/> ...
Related Information
📢 Advertisement for the sap.ui.model.odata.v2.ODataModel
As long as the project requirement is not to complex theÂsap.ui.model.odata.v2.ODataModel
 does an awesome job and there is no need for a “custom” JSONModel. You can check pending changes viaÂhasPendingChanges
. You can set changegroups viaÂsetChangeGroupsÂ
that allow you to submit only changes of a specific entitytype. In short, you can create applications with ease, and if used right you do not really have to think about the management of the application data. TheÂsap.ui.model.odata.v2.ODataModel
 does it for you. For example check out the following blog post, An Recommendation for creating Data Form Dialogs in SAPUI5, and see how to create Data Form Dialogs with ease by using the standard functionality of thesap.ui.model.odata.v2.ODataModel
.
Organize code via the sap.ui.base.Object
The sap.ui.base.Object
is the base class for all SAPUI5 objects. By extending it you have a clean way to organize specific code in your SAPUI5 project.
If many functions in your code belong to a specific topic, it makes sense to outsource them into a “custom” Object.
As an example take the “official” ErrorHandler.js
from SAP. The ErrorHandler includes all functions that are needed to handle application errors automatically and display errors when needed.
The ErrorHandler is instantiated in the Component.js
and takes up 3 lines of code. Outsourcing all the functions in a “custom” Object makes the Component.js
more readable and the ErrorHandler reusable.
/controller/ErrorHandler.js
sap.ui.define([ "sap/ui/base/Object", "sap/m/MessageBox" ], function(UI5Object, MessageBox) { "use strict"; return UI5Object.extend("<PROJECT_PATH>.controller.ErrorHandler", { /** * Handles application errors by automatically attaching to the model events and displaying errors when needed. * @class * @param {sap.ui.core.UIComponent} oComponent reference to the app's component * @public * @alias <PROJECT_PATH>.controller.ErrorHandler */ constructor: function(oComponent) { ... }, /** * Shows a {@link sap.m.MessageBox} when a service call has failed. * Only the first error message will be display. * @param {string} sDetails a technical error to be displayed on request * @private */ _showServiceError: function(sDetails) { ... }, /** * @description try to read the error message value * @param {string} sDetails a technical error to be displayed on request * @returns {string} sDetails */ tryToResolveError: function(sDetails) { ... } }); });
Component.js
sap.ui.define([ "sap/ui/core/UIComponent", "<PROJECT_PATH>/controller/ErrorHandler" ], function(UIComponent, ErrorHandler) { "use strict"; return UIComponent.extend("<PROJECT_PATH>.Component", { metadata: { manifest: "json" }, /** * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. * @public * @override */ init: function() { // call the base component's init function UIComponent.prototype.init.apply(this, arguments); // initialize the error handler with the component this._oErrorHandler = new ErrorHandler(this); ... }, /** * The component is destroyed by UI5 automatically. * In this method, the ErrorHandler is destroyed. * @public * @override */ destroy: function() { this._oErrorHandler.destroy(); // call the base component's destroy function UIComponent.prototype.destroy.apply(this, arguments); } }); });
Related Information
✨ Recommendation
With the sap.ui.base.Object you can kinda simulate classes and go for a object oriented programming concept in your SAPUI5 project. Wouter Lemaire aswell as Ilja Postnovs written really good blog posts about that topic. Check them out!
- Code Template: sap.ui.base.Object
- UI5 Classes and objects by Wouter Lemaire
- UI5 Classes and objects – Inheritance by Wouter Lemaire
- UI5 Classes and Objects – Putting it all together by Wouter Lemaire
- Object Oriented Programming in UI5, part 1 by Ilja Postnovs
- Object Oriented Programming in UI5, part 2 by Ilja Postnovs
Organize code via sap.ui.base.ManagedObject
If the functionality of the sap.ui.base.Object
is not enough and there is a need for basic concepts, such as state management and data binding – the sap.ui.base.ManageObject
may be what you looking for.
The ManagedObject offers many functionality out of the box like state management, data binding, managed properties, managed events and more.
Personally i like to use the sap.ui.base.ManagedObject
instead of the sap.ui.base.Object
whenever i need managed events.
In the following there is an example of an “custom” ManagedObject with managed properties aswell as managed events.
/control/AddressCheck.js
sap.ui.define([ "sap/ui/base/ManagedObject", "sap/ui/model/resource/ResourceModel", "sap/m/MessageBox" ], function(UI5ManagedObject, ResourceModel, MessageBox) { "use strict"; return UI5ManagedObject.extend("<PROJECT_PATH>.control.AddressCheck", { metadata: { properties: { address: { type: "object" } }, events: { complete: { parameters: { address: { type: 'object' }, valid: { type: 'boolean' } } }, exception: { parameters: { message: { type: 'string' } } } } }, /** * @class AddressCheck * @summary Example of a "custom" ManagedObject * @extends sap.ui.base.ManagedObject */ constructor: function(oAddress) { UI5ManagedObject.apply(this); this.setAddress(oAddress); //Just an example to show that you can define a specific resourcemodel for the ManagedObject // this._oi18nModel = new ResourceModel({ // bundleName: "<PATH>.i18n.i18n", // supportedLocales: ["en"] // }); // this.setModel(this._oi18nModel, "i18n"); }, /** * @memberOf AddressCheck * @description validate the given address */ runAddressCheck: function() { var oAddress = this.getAddress(); //error event if (oAddress.postCode > 99999) { this.fireException({ message: "Postcode is invalid." }); return; } MessageBox.confirm("Street is invalid, let us fix it?", { onClose: function(sAction) { var bValid = false; if (sAction === MessageBox.Action.OK) { oAddress.street = "Valid Street"; bValid = true; } //complete event this.fireComplete({ address: oAddress, valid: bValid }); }.bind(this) }); } }); });
/controller/View.controller.js
sap.ui.define([ "sap/ui/core/mvc/Controller", "<PROJECT_PATH>/control/AddressCheck" ], function(Controller, AddressCheck) { "use strict"; return Controller.extend("<PROJECT_PATH>.controller.View", { onInit: function() { this.oAddressCheck = new AddressCheck({ street: "Invalid Street", postCode: 123, //9999999, city: "Somewhere", houseNumber: "123" }); this.oAddressCheck.getAddress(); //Expected output: {street: 'Invalid Street', postCode: '999999', city: 'Somewhere', houseNumber: '123'} //example of attachEventOnce this.oAddressCheck.attachEventOnce("complete", function(oEvt) { oEvt.getParameter("valid"); //Expected output: true/false oEvt.getParameter("address"); //Expected output: {street: 'Valid Street', postCode: '999999', city: 'Somewhere', houseNumber: '123'} //if valid === true }); //example of attachEvent this.oAddressCheck.attachException(function(oEvt) { oEvt.getParameters(); //Expected output: {message: 'Postcode is invalid.'} }); //execute addrcheck after registration of events this.oAddressCheck.runAddressCheck(); } }); });
Related Information
Organize code via sap.ui.define
sap.ui.define
defines a JavaScript module.
A module allows you to split your large bundle of JavaScript code into smaller parts. These parts then can be loaded at runtime when they are needed.
As an example take the “official” formatter.js
from SAP. The formatter.js
bundles all custom formatting functions you need in your application and can be loaded in specific controllers when needed.
/model/formatter.js
sap.ui.define([], function() { "use strict"; return { /** * @description convert hyphened UUID to raw UUID * @param {string} sHyphendedUUID * @returns {string} sRawUUID */ convertHyphenedUUIDToRawUUID: function(sHyphendedUUID) { return sHyphendedUUID.replaceAll("-", "").toUpperCase(); } }; });
/controller/View.controller.js
sap.ui.define([ "sap/ui/core/mvc/Controller", "<PROJECT_PATH>/model/formatter" ], function(Controller, formatter) { "use strict"; return Controller.extend("<PROJECT_PATH>.controller.View1", { formatter: formatter, onInit: function() { formatter.convertHyphenedUUIDToRawUUID("0050569f-176f-1edd-a39f-10064a8f52f1"); //Expected output: 0050569F176F1EDDA39F10064A8F52F1 } }); });
sap.ui.define
also let’s you create a reusable module with a constructor and methods. To use the module just load it into your controller and create an instance of it.
…/MyModule.js
sap.ui.define([ "sap/m/MessageToast", "sap/m/MessageBox" ], function(MessageToast, MessageBox) { // create a new class (constructor) var MyModule = function(oArguments) { this._oArguments = oArguments; }; // add methods to its prototype MyModule.prototype.doSth = function() { MessageToast.show(this._oArguments.ParameterA); }; // add methods to its prototype MyModule.prototype.doSthMore = function() { return MessageBox.confirm(this._oArguments.ParameterA, { onClose: function(sAction) { this._oArguments.FunctionA(sAction); }.bind(this) }); }; // return the class as module value return MyModule; });
/controller/View.controller.js
sap.ui.define([ "sap/ui/core/mvc/Controller", ".../MyModule" ], function(Controller, MyModule) { "use strict"; return Controller.extend("<PROJECT_PATH>.controller.View1", { onInit: function() { var oMyModule = new MyModule({ ParameterA: "ParamterA", FunctionA: this.functionA.bind(this) }); oMyModule.doSth(); oMyModule.doSthMore(); }, functionA: function(sAction) { //do sth. } }); });
Related Information
✨ Recommendation
Check out the ui5-validator by Qualiture for a “real” use case.
- Modules and Dependencies
- sap.ui.define
- What is the best practice or storing constants in SAPUI5? (Example of how to store and access constants via
sap.ui.define
)
Final Thoughts
That’s it. Those were all the common options for organizing the code in an SAPUI5 project, that i use on a daily basis. Of course, all the options listed, can be easily outsourced to a library. – If you are working as an in-house developer, go for it.
Finally, of course, it must be said… this blog post is very opinion based.🤯 I have found these options to be the most fitting ones for me. There are certainly many other approaches, some better, some worse.
So if you have other approaches, tell us!