Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
This is the ninth part of a tutorial series about how to build your own SAP Fiori Approve Purchase Order app.

The purpose of this tutorial is to show you step-by-step how to build your own SAP Fiori Approve Purchase Orders app and provides additional insights into why certain aspects have been developed as they are.

Please see the introductory post (Tutorial: Build Your Own SAP Fiori Approve Purchase Order App) for background information regarding this tutorial.

Previously posted chapters can be find here:

In this chapter, we set up the app using the SAP Web IDE and ran the app using mock data.

In this chapter, we adapted the list screen of the app.

In this chapter, we adapted the detail screen of the app.

In this chapter, we enabled approve and reject buttons.

In this sixth chapter, we set up a new model that stores the global app state.

In this seventh chapter,  we encapsulated approve / reject service calls.

  • Part 8: (Tutorial: Build Your Own SAP Fiori Approve Purchase Order App - Part 😎


In this eighth chapter, we mimicked the backend logic.

In this ninth chapter, we're going to refresh the master and detail screen

Refresh the master and detail screen
We’ll now define the order of purchase orders to be displayed on the detail screen after a purchase order has been successfully approved / rejected. There are a few factors and scenarios to consider when deciding which purchase order is to be selected next. On devices where the list and detail are displayed together on the same screen, we need to ensure that the selection of the list corresponds to the order that is shown in the details view. A very straight forward solution would be to refresh the master list and select the first available entry. A brute force approach would be to always refresh the list, independently of whether the user cancels the approval dialog or whether the business logic is successfully executed. Of course, this could easily be optimized if we only refresh the list in case the business logic is successfully executed. However, if a user approves the tenth entry in the list, he/she already decided not to approve entries 1 to 9. So selecting, for example, the eleventh entry would make sense. We’ll implement a defaulting mechanism to select the next purchase order in the given order. When the approval or rejection is successful, the next purchase order is selected. In case the operation isn’t successful or the user cancelled the operation, the selection is kept stable. As for the approval logic, we’ll prepare the coding already for mass selection.

The logic affects the master list and the detail screens so it needs to be “global”. We already defined two properties (preferredIDs and currentPOId) for this in our global model in model.js without using / explaining them in detail.



File: model/model.js

var oModel= new JSONModel({

isBusyApproving: false,

isSwipeRunning: false,

preferredIds: [],

currentPOId: null

});

The array preferredIDs[] stores a list of IDs currently selected for approval. This is required because we don’t know upfront whether a purchase order operation will be successful in the backend. The property currentPOId stores the ID that is currently shown in the detail screen.

Since we need to store the current purchase order ID, the _onObjectMatched function in the detail controller is a good place. _onObjectMatched is a function registered in onInit() and called by the router whenever a new purchase item is requested by a URL parameter.



File: controller/Detail.controller.js

 

_onObjectMatched : function (oEvent) {

var sObjectId =  oEvent.getParameter("arguments").objectId;

this.getModel().metadataLoaded().then( function() {

var sObjectPath = this.getModel().createKey("Z_C_Purchaseorder", {

POId :  sObjectId

});

this.getModel("globalProperties").setProperty("/currentPOId", sObjectId);        this._bindView("/" + sObjectPath);

}.bind(this));

},

Once an item has successfully been completed, we need to refresh the master list. This is done via function refreshList().



File: controller/ListSelector.js

clearMasterListSelection: function() {

[…]

},

 

/**

* Trigger a refresh of the master list without changing the scroll position

*/

refreshList: function() {

this._oList.attachEventOnce("updateFinished", this._oList.focus, this._oList);

this._oList.getBinding("items").refresh();

}

});

RefreshList triggers an update on the binding. Once the refresh is finished, an updateFinished event is fired. We don’t want the focus function to be called every time the list refreshes but only when the refresh is triggered via this function. So we use attachEventOnce. This ensures that once the eventhandler is called, it’ll be removed.

Now we can wire the refresh function to the approval logic. The function is to be called every time a purchase order is successfully approved / rejected.



File: model/Approver.js

_callEnded: function(bSuccess, oGlobalModel) {

this._iOpenCallsCount--;

this._bOneWaitingSuccess = bSuccess || this._bOneWaitingSuccess;

if (this._iOpenCallsCount === 0) { // When we are not waiting for another call

this._mRunningSwipes = {}; // start with a new round

oGlobalModel.setProperty("/isSwipeRunning", false);

oGlobalModel.setProperty("/isBusyApproving", false);

if (this._bOneWaitingSuccess) {

this._bOneWaitingSuccess = false;

this._oListSelector.refreshList();

}

}

}

A user might expect the application to select an item close to the previously selected one. For example, if a user approves the tenth item, he or she doesn’t want the application to select the first item in the list, but rather the ninth or eleventh one. So let’s implement a function to calculate the next item to be displayed.



File: controller/ListSelector.js

/**

* Trigger a refresh of the master list without changing the scroll position

*/

refreshList: function() {

this._oList.attachEventOnce("updateFinished", this._oList.focus, this._oList);

this._oList.getBinding("items").refresh();

},

 

// Prepare for the removal of some items from the list (due to

// approvals/rejections).

// This is done by setting the IDs currently in the list to

// preferredIds. Thereby we

// start with the item currently displayed. Then the IDs following

// this element are added

// in their current order. Finally, we add those items listed in

// front of the current item in reverse order.

prepareResetOfList: function(oGlobalModel) {

var aListItems = this._oList.getItems(),

sCurrentPOId = oGlobalModel.getProperty("/currentPOId");

// Get list of POIds from the items in the list

var aPreferredIds = aListItems.map(function(oItem) {

return oItem.getBindingContext().getProperty("POId");

});

// Split into pre- and post-found

var aTail = aPreferredIds.splice(0, aPreferredIds.indexOf(sCurrentPOId)).reverse();

aPreferredIds = aPreferredIds.concat(aTail);

oGlobalModel.setProperty("/preferredIds", aPreferredIds);

// Reset the current ID (we only have preferences now)

oGlobalModel.setProperty("/currentPOId", null);

}

});

The new selection is done when a list refresh is triggered. There is already an onUpdateFinished() event handler generated by the Template Wizard in Master.controller.js. We’ll extend it to handle the new selection. On a phone device, the master and detail screens aren't shown on one screen. Once an order is approved on the detail screen, the master list is displayed. That’s why we won’t implement this selection behavior for phones. There’ll be an item to display as long as /currentPOId isn’t null.



File: controller/Master.controller.js


onUpdateFinished: function(oEvent) {

// update the master list object counter after new data is loaded

this._updateListItemCount(oEvent.getParameter("total"));

// hide pull to refresh if necessary

this.byId("pullToRefresh").hide();

 

var oGlobalModel = this.getModel("globalProperties");

if (Device.system.phone || oGlobalModel.getProperty("/currentPOId")) {

oGlobalModel.setProperty("/isBusyApproving", false);

} else {

this._findItemToDisplay();

}

},

In Master.controller.js, we’ll also implement the logic to find the item to be displayed based on the list of preferred ID’s. This function basically checks for ID’s in the list of preferred ID’s (remembers the sequence) and selectes the first one that is also shown in the list. If no item is found, an empty page is displayed:



File: controller/Master.controller.js

onSwipeApprove: function(oEvent) {

},

 

_findItemToDisplay: function() {

var oGlobalModel = this.getModel("globalProperties"),

aPreferredIds = oGlobalModel.getProperty("/preferredIds"),

oNextItem = null;

oGlobalModel.setProperty("/preferredIds", []);

for (var i = 0; i < aPreferredIds.length && !oNextItem; i++) {

oNextItem = this._getItemForId(aPreferredIds[i]);

}

oNextItem = oNextItem || this._oList.getItems()[0];

if (oNextItem) {

this._showDetail(oNextItem);

} else {

this.getRouter().getTargets().display("detailNoObjectsAvailable");

}

}

});

Last but not least, we need to implement the helper function _getItemForId, which is used in the code. Again, we put this in Master.controller.js. It'll retrieve the binding context for the purchase order ID.



File: controller/Master.controller.js

_findItemToDisplay: function() {

[…]

},

 

_getItemForId: function(sPOId) {

return this._oList.getItems().find(function(oItem) {

return sPOId === getIdForItem(oItem);

});

}

});

Note that the code above uses new features vom ECMA Script 2015. If your browser does not support this version, here’s an alternative version:

_getItemForId: function(sPOId) {

var aItems = this._oList.getItems();

for (var i = 0; i < aItems.length; i++) {

if (sPOId === getIdForItem(aItems[i])) {

return aItems[i];

}

}

}

 

});

And we need to implement a utility function to derive the purchase order item from a given purchase order ID.



File: controller/Master.controller.js

], function(BaseController, JSONModel, Filter, FilterOperator, GroupHeaderListItem, Device, formatter, grouper, GroupSortState,

MessageToast) {

"use strict";

 

function getIdForItem(oListItem) {

return oListItem.getBindingContext().getProperty("POId");

}

return BaseController.extend("acme.purchaseorder.controller.Master", {

We now need to hook in the calculations of preferred IDs into our existing logic. This is already done in the approve function of Approver.js.



File: model/Approver.js

if (bFromSwipe) {

this._mRunningSwipes[aPOIds[0]] = true;

oGlobalModel.setProperty("/isSwipeRunning", true);

} else {

oGlobalModel.setProperty("/isBusyApproving", true);

}

if (!Device.system.phone) {

var sCurrentPOId = oGlobalModel.getProperty("/currentPOId"),

bIsCurrentApproved = (sCurrentPOId === aPOIds[0]);

if (bIsCurrentApproved) {

this._oListSelector.prepareResetOfList(oGlobalModel);

}

}

this._iOpenCallsCount++;

This function is called to approve or reject a purchase order. As mentioned above:

  • The defaulting logic is not required on phone devices

  • Since we are only approving single purchase orders, currentPOId must be always identical to the first item in the list of purchase orders to be approved.


So far, we have calculated a list of all possible IDs that can be shown in the detail screen once the backend operation succeeds. And, we have implemented a logic that selects the next item based on this list during a refresh of the master list. So, how is refresh triggered?  It is already part of our coding. In Approver.js, the _callEnded method is invoked every time - regardless of success or failure. Upon successful operation, it calls the refreshList() method of the ListSelector to trigger a refresh.

I hope the ninth part of the tutorial has sparked your interest in the chapters to come. Next time, we’ll implement code to block the screen for further input, for example, to prevent a user from approving the same purchase order twice.