How to organize your code in your SAPUI5 project

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

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 createreadupdate 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 the sap.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);
		}

	});

});

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

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.
		}
	});
});

✨ Recommendation

Check out the ui5-validator  by Qualiture for a “real” use case.

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!

Leave a Reply

Your email address will not be published. Required fields are marked *