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: 
0 Kudos
This is the fifth 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 fifth part, we're going to enable approve and reject buttons.

Enable Approve Reject Buttons
As of now, the application is nothing more than an open purchase order browser. So far, we enriched the information displayed in our app, but it’s still read only with no data changes possible. Let’s now implement the transactional behavior. To do so, we need a number of sub-steps, because besides the pure controller logic, we need to consider how our mock-server can support changes, and what happens to our UI in case an approval is pending.

First, let’s define two new buttons for approval / reject on our details page. When we're done, the buttons will like the following:



The definition is done in view/Detail.view.xml:



File: view/Detail.view.xml

<semantic:DetailPage

id="page"

navButtonPress="onNavBack"

showNavButton="{device>/system/phone}"

title="{i18n>detailTitle}"

busy="{detailView>/busy}"

busyIndicatorDelay="{detailView>/delay}">

<semantic:positiveAction>

<semantic:PositiveAction id="approveButton" press="onApprove" text="{i18n>xbut.approve}"/>

</semantic:positiveAction>

<semantic:negativeAction>

<semantic:NegativeAction id="rejectButton" press="onReject" text="{i18n>xbut.reject}"/>

</semantic:negativeAction>

<semantic:content>

 

We use different semantics - positive and negative to indicate the different semantics of these buttons. For each button, we need to define a text to be displayed and an event handler that implements the behavior. Both texts are defined in the i18n.properties file.


File: i18n/i18n.properties

 

#~~~ Detail View ~~~~~~~~~~~~~~~~~~~~~~~~~~

#XBUT

xbut.approve=Approve

#XBUT

xbut.reject=Reject

Next, we’ll define two event handlers in the Detail controller class to handle approve and reject events. They use a simple MessageToast to display the text of the corresponding button. Later in this tutorial, we’ll wire the business logic into these event handlers.


File: controller/Detail.controller.js

sap.ui.define([

"acme/purchaseorder/controller/BaseController",

"sap/ui/model/json/JSONModel",

"acme/purchaseorder/model/formatter",

"sap/m/MessageToast"

], function (BaseController, JSONModel, formatter, MessageToast) {

"use strict";

[…]

_onMetadataLoaded : function () {

[…]

},

 

onApprove: function() {

MessageToast.show(this.getResourceBundle().getText("xbut.approve"));

},

onReject: function() {

MessageToast.show(this.getResourceBundle().getText("xbut.reject"));

}

});

}

);


Clicking on approve should now display a short message like this:



Initiate swipe for approval


Especially on iOS, users are used to triggering default actions on lists using swipe actions instead of navigating to a detail view. This should also be possible in our approval app. So, we'll need to implement a swipe event on the list.


File: view/Master.view.xml


<List

id="list"

[…]

updateFinished="onUpdateFinished"

swipe="onSwipe"

selectionChange="onSelectionChange">

[…]

<swipeContent>

<Button id="swipeButton" press="onSwipeApprove" text="{i18n>xbut.approve}" type="Accept"/>

</swipeContent>

</List>

Again, we’ll add an event handler in the corresponding view controller just to test the new button.


File: 
controller/Master.controller.js

 

sap.ui.define([

"acme/purchaseorder/controller/BaseController",

"sap/ui/model/json/JSONModel",

"sap/ui/model/Filter",

"sap/ui/model/FilterOperator",

"sap/m/GroupHeaderListItem",

"sap/ui/Device",

"acme/purchaseorder/model/formatter",

"acme/purchaseorder/model/grouper",

"acme/purchaseorder/model/GroupSortState",

"sap/m/MessageToast"

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

[…]

_updateFilterBar : function (sFilterBarText) {

[…]

},

 

onSwipeApprove: function(oEvent){

MessageToast.show(this.getResourceBundle().getText("xbut.approve"));

},

 

onSwipe: function(oEvent) {

// intentionally left empty

},

 

Define the approve/reject popup


Once the user clicks either approve or reject, we’d like to have a dialog in both cases so that the user can add optional comments. First, we need to define the dialog that prompts the user for an optional comment. For this, we’ll create a new fragment identical to the one shown in the SAPUI5 walkthrough.

We'll create a subfolder called fragment in the view folder.

We'll also create a new file ApprovalDialog.fragment.xml in the fragment folder.



 

Paste the following definition in the ApprovalDialog.fragment.xml file.


File: view/fragment/ApprovalDialog.fragment.xml


<core:FragmentDefinition xmlns:core="sap.ui.core" xmlns:l="sap.ui.layout" xmlns="sap.m">

<Dialog id="approvalDialog" class="sapUiContentPadding"  title="{approvalProperties>/approvalTitle}">

<content>

<l:VerticalLayout id="textLayout" width="100%">

<l:content>

<Text id="approvalText" text="{approvalProperties>/approvalText}"/>

<TextArea id="noteTextArea" placeholder="{i18n>xfld.approvalNote}" rows="5" value="{approvalProperties>/approvalNote}" width="100%"/>

</l:content>

</l:VerticalLayout>

</content>

<beginButton>

<Button id="confirmButton" press="onConfirmAction" text="{i18n>xbut.buttonOK}"/>

</beginButton>

<endButton>

<Button id="cancelButton" press="onCancelAction" text="{i18n>xbut.buttonCancel}"/>

</endButton>

</Dialog>

</core:FragmentDefinition>

 

To pass and retrieve information to and from this dialog, the approvalProperties model is used. The model contains the properties. approvalTitle and approvalText are passed to this dialog whereas approvalNote is initially empty and retrieved from this dialog.

The properties correspond to the following parts of the Approve popup.



The dialog definition also contains translatable text that need to be stored in i18n.properties:


File: i18n/i18n.properties

#~~~ Approve and Reject Dialog ~~~~~~~~~~~~~~

#XFLD: Approval note

xfld.approvalNote=Add note (optional)

#XBUT: OK button

xbut.buttonOK=OK

#XBUT: Cancel button

xbut.buttonCancel=Cancel

#XFLD: Approval text

xfld.approvalTextWithSupplier=The purchase order for supplier {0} will be approved

#XFLD: Rejection text

xfld.rejectionTextWithSupplier=The purchase order for supplier {0} will be rejected

#XTIT: ActionSheet header for approval

xtit.approvalTitleForDialog=Approve

#XTIT: ActionSheet header for rejection

xtit.rejectionTitleForDialog=Reject

Create a dialog logic class

 

Now we can implement the dialog logic itself. We could do this in the detail controller class, but since the dialog is a fragment, we’ll create a dedicated class to implement the dialog logic.



Right now, this application supports only single approve / reject requests. As preparation for multi selection, the dialog and request logic already supports sets of purchase orders to be rejected / approved. So instead of dealing with single instances, these classes are already mass-enabled.

We'll create a new file called SubControllerForApproval.js in controller. In contrast the master and detail controller, this controller is not derived from BaseController but from the base object. This is because it’s not associated with a view; it only manages a page fragment.

Create an empty stub for this class, we’ll extend it step-by-step. If you get stuck on the way, there’s the complete coding of this file at the end.

Create a constructor


First, we need to implement a constructor inside the object. Since JavaScript is a scripting language, there’s no need for explicit object initialization. This is only done to introduce the properties and discuss their usage.


File: controller/SubControllerForApproval.js


sap.ui.define([

"sap/ui/base/Object",

"sap/ui/model/json/JSONModel",

"sap/m/MessageToast"

], function(Object, JSONModel, MessageToast) {

"use strict";

 

return Object.extend("acme.purchaseorder.controller.SubControllerForApproval", {

constructor: function(oParentView) {

this._oParentView = oParentView;

this._oResourceBundle = oParentView.getController().getResourceBundle();

this._aPurchaseOrders = [];

this._bApprove = false;

this._fnApproveActionFinished = null;

this._fnApproveFailed = null;

this._oApprovalDialog = null;

this._oApprovalProperties = null;

}

});

});

 

Since the class is derived from object, it uses its parent view to access the required models, such as the globalModel and ResourceBundle. Properties are used for the following purposes:

Properties







































Property Description
_oParentView Hosts the dialog
_oResourceBundle Provides access to i18n texts
_aPurchaseOrders Array of purchase order IDs to be accepted or rejected
_bApprove Indicates whether the POs to be processed are to be approved or rejected
_fnApproveActionFinished To be called once the purchase order is approved or rejected
_fnApproveFailed To be called if the purchase order approval / rejection failed
_oApprovalDialog The approval dialog
_oApprovalProperties A JSON model used in the dialog. It contains the text that is shown on the UI

Initialize the class

We need to initialize the dialog class and register it to the parent view. We only need to do this once. We use the _underscore to indicate that this function is to be used only in this class. Please also note that we need to sync the style classes with the parent view explicitly. More information about popups is available in the UI5 walkthrough.


File: controller/SubControllerForApproval.js


constructor: function(oParentView) {

[...]

},

 

_initializeApprovalDialog: function() {

this._oApprovalDialog = sap.ui.xmlfragment(this._oParentView.getId(), "acme.purchaseorder.view.fragment.ApprovalDialog", this);

jQuery.sap.syncStyleClass(this._oParentView.getController().getOwnerComponent().getContentDensityClass(), this._oParentView, this._oApprovalDialog);

this._oParentView.addDependent(this._oApprovalDialog);

this._oApprovalProperties = new JSONModel();

this._oApprovalDialog.setModel(this._oApprovalProperties, "approvalProperties");

}

Open the accept/reject dialog

Next, we’ll implement the opening of the dialog. This is encapsulated by a promise object. The usage of promise objects for handling dialog is not required. This is more a matter of style. Find more information about the usage of promise objects in JavaScript e.g. here.


File: controller/SubControllerForApproval.js


_initializeApprovalDialog: function() {

[..]

},

 

openDialog: function(bApprove, aPurchaseOrders) {

var fnPromise = function(fnResolve, fnFailed) {

var sApprovalText, sTitle;

this._fnApproveActionFinished = fnResolve;

this._fnApproveFailed = fnFailed;

this._bApprove = bApprove;

this._aPurchaseOrders = aPurchaseOrders;

if (!this._oApprovalDialog) {

this._initializeApprovalDialog();

}

 

if (aPurchaseOrders.length === 1) {

sApprovalText = this._oResourceBundle.getText(this._bApprove ? "xfld.approvalTextWithSupplier" : "xfld.rejectionTextWithSupplier", [aPurchaseOrders[0].SupplierName]);

sTitle = this._oResourceBundle.getText(this._bApprove ? "xtit.approvalTitleForDialog" : "xtit.rejectionTitleForDialog");

} else {

return;

}

this._oApprovalProperties.setProperty("/approvalText", sApprovalText);

this._oApprovalProperties.setProperty("/approvalTitle", sTitle);

this._oApprovalProperties.setProperty("/approvalNote", "");

this._oApprovalDialog.open();

}.bind(this);

return new Promise(fnPromise);

}

We’ll register the resolve and reject functions that are passed to the promise object. If the dialog object hasn’t been initialized yet, the initialization function is called.

Since the dialog is used to approve and reject, we need to know what constitutes approve and reject: bApprove is true for approve, and false for reject. This information is required, for example, to retrieve the corresponding text from the resource bundle.

Next, we’ll store all purchase orders to be approved/rejected in the _aPurchaseOrder array. This will be passed to the function doing the action.

Finally, we’ll fill the model that is bound in the dialog with text to be displayed. The text depends on the action (approve or reject) and how many items are processed.

Implement ok and cancel event handlers

 

The view fragment of the popup contains handlers to confirm and cancel actions.
These are implemented using the begin and end buttons.



<beginButton>

<Button id="confirmButton" press="onConfirmAction" .../>

</beginButton>

<endButton>

<Button id="cancelButton" press="onCancelAction"  .../>

</endButton>

 

Since we don’t have any business logic so far, we’ll only use the message toast technique from the last step to verify that our popups works. Additionally, as a clean up, we’ll implement a _cleanup function to do the housekeeping.


File: controller/SubControllerForApproval.js


openDialog: function(bApprove, aPurchaseOrders) {

[…]

},

 

_cleanup : function() {

this._oApprovalDialog.close();

this._fnApproveFailed = null;

this._aPurchaseOrders = null;

this._fnApproveActionFinished = null;

},

onConfirmAction: function() {

this._fnApproveActionFinished(true);

this._cleanup();

MessageToast.show(this._oResourceBundle.getText("xbut.buttonOK"));

},

onCancelAction: function() {

this._fnApproveActionFinished(false);

this._cleanup();

MessageToast.show(this._oResourceBundle.getText("xbut.buttonCancel"));

}

We use a boolean parameter in the _fnApproveActionFinish function to signal whether the popup is confirmed or canceled.

We’re now prepared to replace the simple message toast in our approve and reject handlers with the popup we’ve created. We need to create a SubControllerForApproval instance in the detail controller at the end of the init function. Please note that we also require the device in


File: controller/Detail.controller.js


/*global location */

sap.ui.define([

"acme/purchaseorder/controller/BaseController",

"sap/ui/model/json/JSONModel",

"acme/purchaseorder/model/formatter",

"sap/m/MessageToast",

"acme/purchaseorder/controller/SubControllerForApproval",

"sap/ui/Device"

], function (BaseController, JSONModel, formatter, MessageToast, , SubControllerForApproval, Device) {

"use strict";

[…]

 

onInit : function () {

// Model used to manipulate control states. The chosen values make sure,

// detail page is busy indication immediately so there is no break in

// between the busy indication for loading the view's meta data

var oViewModel = new JSONModel({

busy : false,

delay : 0,

lineItemListTitle : this.getResourceBundle().getText("detailLineItemTableHeading")

});

 

this.getRouter().getRoute("object").attachPatternMatched(this._onObjectMatched, this);

this.setModel(oViewModel, "detailView");

this.oSubControllerForApproval = new SubControllerForApproval(this.getView());

this.getOwnerComponent().getModel().metadataLoaded().then(this._onMetadataLoaded.bind(this));

},

Approve and reject both work in a similar manner. In fact, in both cases our popup appears but with different texts. So, we refactor the complete code into a new function _onOpenApprovalDialog and pass true for approve or false for reject. This function passes the selected purchase order to the dialog controller and opens the dialog. A mobile device displays either the master list or the detail screen. When the user approves or rejects a purchase order, the purchase order will be removed from the list and the detail screen must be updated with the next purchase order (if any) or navigate from the detail screen back to the list. The latter is what our app does.


File: controller/Detail.controller.js


onApprove: function() {

MessageToast.show(this.getResourceBundle().getText("xbut.approve"));

this._onOpenApprovalDialog(true);

},

 

onReject: function() {

MessageToast.show(this.getResourceBundle().getText("xbut.reject"));

this._onOpenApprovalDialog(false);

},

 

_onOpenApprovalDialog: function(bApprove) {

var _sContext = this.getView().getElementBinding().getPath();

var oWhenApproved = this.oSubControllerForApproval.openDialog(bApprove, [this.getModel().getProperty(_sContext)]);

if (Device.system.phone) {

// On a phone, when the approval is really completed: go to master list

oWhenApproved.then(function(bProcessed) {

// bProcessed indicates whether the approval was really completed

if (bProcessed) {

this.onNavBack();

}

}.bind(this));

}

}

You may be wondering what happens to the swipe event handler. In our use case, swipe is a shortcut for approval without providing a note.

I hope the fifth part to the tutorial has sparked your interest in the chapters to come. Next time, we’ll set up a new model that stores the global app state.