SAPUI5 Getting Started (SAP HANA): Unterschied zwischen den Versionen
Matt (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
Matt (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung |
||
(58 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
Zeile 17: | Zeile 17: | ||
|- | |- | ||
| ./ || index.html || | | ./ || index.html || | ||
* Load SAPUI5 bootstrap | * Load SAPUI5 bootstrap ''sap-ui-core.js'' | ||
* Load error handler | * Load error handler | ||
* Load startup.js for session info, unified shell | * Load startup.js for session info, unified shell | ||
* Specifies async loading of UI | |||
|- | |- | ||
| ./ || Component.js || | | ./ || Component.js || | ||
* Represents the real start of the application | * Represents the real start of the application | ||
* Initialize SAPUI5 component | * Initialize SAPUI5 component | ||
* Define manifest | * Define metadata and load it from manifest JSON | ||
* | * Initialize Router | ||
* Initialize additional models, that are not defined in the manifest, i.e. the device model | |||
* Load first view | * Load first view | ||
|- | |- | ||
| ./ || manifest.json || | | ./ || manifest.json || | ||
Also called: Application descriptor | |||
Specifies: | Specifies: | ||
* component configuration | |||
* libraries and versions | * libraries and versions | ||
* device types and supported themes | * device types and supported themes | ||
Zeile 37: | Zeile 42: | ||
* odata models (Binding data source to model) | * odata models (Binding data source to model) | ||
* text bundles | * text bundles | ||
* routes | |||
* asnyc loading of root view | |||
|- | |- | ||
| ./i18n/ || i18n_en.properties || | | ./i18n/ || i18n_en.properties || | ||
Zeile 63: | Zeile 70: | ||
* callMultiService: Handles call of odata service to dynamically create columns in table controls | * callMultiService: Handles call of odata service to dynamically create columns in table controls | ||
* callExcel: Calls download of PO data in Excel format | * callExcel: Calls download of PO data in Excel format | ||
sap.ui.define is used for asynchronous loading | |||
|- | |- | ||
| ./controller/ || Base.controller.js || | | ./controller/ || Base.controller.js || | ||
Zeile 116: | Zeile 125: | ||
| ./i18n/ || messagebundle_de.properties || Language DE message bundle | | ./i18n/ || messagebundle_de.properties || Language DE message bundle | ||
|} | |} | ||
A language can be selected by changing browser language or adding the URL parameter ''sap-ui-langauge=DE_de'' to tue URL of your app. | |||
Add code: | Add code: | ||
Zeile 159: | Zeile 170: | ||
|- | |- | ||
| /web/ || [https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/index.html index.html] || Fetch bootstrap from resource by utilizing variable | | /web/ || [https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/index.html index.html] || Fetch bootstrap from resource by utilizing variable | ||
|} | |||
== UI Controls and Libraries == | |||
{| class="wikitable" | |||
! Control !! Library !! Function !! Properties | |||
|- | |||
| Image || sap.m || Show image || src, width, tooltip | |||
|- | |||
| Page || sap.m || Create a page with footer || title | |||
|- | |||
| Link || sap.m || Show link || emphasized, text, target, href | |||
|- | |||
| SearchField || sap.m || Input field for search || placeholder | |||
|- | |||
| Select || sap.m || List of items for selection || | |||
|- | |||
| Button || sap.m || Button for triggering actions || text | |||
|- | |||
| Label || sap.m || Text label for other controls || labelFor | |||
|- | |||
| SimpleForm || sap.ui.layout.form || Create nicely arranged simple forms || layout, columnsM, columnsL, columnsXL | |||
|- | |||
| MessageToast || sap.m || Display a message || text | |||
|- | |||
| PlanningCalendar || sap.ui.unified || Display rows with appointments || startDate, rows, Aggregation PlanningCalendarRow | |||
|} | |||
''Aggregations'' of controls are used to nest other controls to be displayed inside them. | |||
''Association'' to the Select control is the currently selected item. | |||
Examples: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/view/App.view.xml | |||
== Events == | |||
{| class="wikitable" | |||
! Event !! Belongs to !! Description | |||
|- | |||
| press || Button, Image || Fires when pressing on a button | |||
|- | |||
| onInit || Controller || After the controller is initialized | |||
|- | |||
| onBeforeRendering || Controller || Before rendering, i.e. for checking device parameters | |||
|- | |||
| onAfterRendering || Controller || After rendering, i.e. for checking contents of DOM | |||
|- | |||
| onExit || Controller || Before destroying the App | |||
|} | |||
Example Listing: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/controller/App.controller.js | |||
== URL Parameters == | |||
Parameters are specified by adding parameter-value-pairs to the URL. Example: | |||
index.html?<parameter>=<value> | |||
index.html?sap-ui-language=de_DE | |||
{| class="wikitable" | |||
! Parameter !! Description !! Example Values | |||
|- | |||
| sap-ui-language || Set language in as <Language>_<Region> || | |||
German in Germany: de_DE<br> | |||
Chinese in China: zh_CN<br> | |||
Arabic in Saudi Arabia: ar_SA<br> | |||
|- | |||
| sap-ui-theme || Set theme || | |||
High Contrast White: sap_belize_hcw<br> | |||
High Contrast Black: sap_belize_hcb | |||
|} | |} | ||
Zeile 255: | Zeile 336: | ||
=== OData V2 Deep Insert with Content-ID and Links === | === OData V2 Deep Insert with Content-ID and Links === | ||
Example Application: | |||
/web/resource/odataDeep | |||
Deep inserts allow to insert data into tables with parent-child relationship and ensure that all the inserted records of both tables are correctly related to each other. | Deep inserts allow to insert data into tables with parent-child relationship and ensure that all the inserted records of both tables are correctly related to each other. | ||
In the example below this actually happens by inserting records into both tables seperately using content ID and then updating the second table to have the same primary keys as the first table. | In the example below this actually happens by inserting records into both tables seperately using content ID and then updating the second table to have the same primary keys as the first table. | ||
Definition of OData Service '''businessPartnersAddresses.xsodata''': | |||
service { | |||
"MD.BusinessPartner" | |||
as "BusinessPartners" | |||
navigates ("ToAddresses" as "AddRef") | |||
create events(before | |||
"sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::bp_create_before_exit"); | |||
"MD.Addresses" | |||
as "Addresses" | |||
without ("POINT") | |||
create events(before | |||
"sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::address_create_before_exit"); | |||
association "ToAddresses" principal "BusinessPartners"("ADDRESSES.ADDRESSID") | |||
multiplicity "1" | |||
dependent "Addresses"("ADDRESSID") multiplicity "1" | |||
over "MD.Addresses" | |||
principal("ADDRESSID") dependent ("ADDRESSID") | |||
update using | |||
"sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::assocation_create_exit"; | |||
} | |||
The service has three user exits: | |||
* '''bp_create_before_exit:''' Generates partnerID from sequence and fills some fields before create | |||
* '''address_create_before_exit:''' Generates addressID from sequence and fills some fields before create | |||
* '''association_create_exit:''' Fetches both previously created records and updates the business partner record with the addressID from the address record. | |||
Listing of '''businessPartnersAddresses.xsjslib''' containing the three exits: | |||
https://github.com/mattxdev/opensap-hana7/blob/master/xsjs/lib/sap/hana/democontent/epm/services/businessPartnersAddresses.xsjslib | |||
In order to use batch with content-ids the SAPUI5 odata object cannot be used. Instead the request will be build by the controller during run time. Calling of the services utilizes jQuery.ajax and must also handle CSRF tokens. | |||
index.html | |||
... | |||
<script type="text/javascript" src="/common/csrf.js" ></script> | |||
... | |||
Listing of app controller which builds the complete OData batch request (unlike earlier SAPUI5 exercises where the framework did all the work): | |||
Notice the Content-ID option. It works as a place holder ($1 and $2) to represent the keys which will not be generated until the server side has processed the work. The $1 and $2 placeholders can be used in the association URL and work as placeholders for actual values. XSODATA framework on the server side will replace this values during processing. | |||
=== Fiori Annotations with OData V4 and CDS === | |||
Example Application: | |||
/web/resource/poListV4 | |||
OData V4 and CDS enable a large range of metadata definitions for Fiori applications to allow nearly everything about the UI like: | |||
* Value helps | |||
* Sort Options | |||
* Output columns | |||
The metadata can be placed at different levels of the CDS like: | |||
# Lowest level: At the entity definitions | |||
# At the service level or UI page | |||
{| class="wikitable" | |||
|+ style="text-align: left" | Types of annotations | |||
! Type !! Description | |||
|- | |||
| title || Text description used as field label and column headers. Can be used at reusable type level or within entity or view definition | |||
|- | |||
| FieldControl || Set state of input field to i.e. Read Only or Mandatory | |||
|- | |||
| Measures || i.e. amount of fields gets measure from currency column | |||
|- | |||
| ValueList || i.e. currency column which points to foreign entity CURRENCY. This allows to pupulate drop down list boxes or fill value helps within the UI from this definition. | |||
|} | |||
Code excerpts for entity level annotations from ''/db/PurchaseOrder.cds'': | |||
type HistoryT { | |||
CREATEDBY : BusinessKey @(title: '{i18n>CreatedBy}', Common.FieldControl: #ReadOnly); | |||
CREATEDAT : SDate @(title: '{i18n>CreatedAt}', Common.FieldControl: #ReadOnly); | |||
CHANGEDBY : BusinessKey @(title: '{i18n>ChangedBy}', Common.FieldControl: #ReadOnly); | |||
CHANGEDAT : SDate @(title: '{i18n>ChangedAt}', Common.FieldControl: #ReadOnly); | |||
}; | |||
entity Headers { | |||
key PURCHASEORDERID : Integer @( | |||
title: '{i18n>po_id}', | |||
Common.FieldControl: #Mandatory, | |||
Search.defaultSearchElement, | |||
Common.Label: '{i18n>po_id}' | |||
); | |||
ITEMS : association to many Items on ITEMS.POHeader = $self @( | |||
title: '{i18n>po_items}', | |||
Common: { Text: {$value: ITEMS.PRODUCT, "@UI.TextArrangement": #TextOnly }} | |||
); | |||
HISTORY : HistoryT; | |||
NOTEID : BusinessKey null @title: '{i18n>notes}'; | |||
PARTNER : BusinessKey @title: '{i18n>partner_id}'; | |||
CURRENCY : CurrencyT @( | |||
Common: { | |||
Text: {$value: CURRENCY.CURRENCY, "@UI.TextArrangement": #TextOnly}, | |||
ValueList: {entity: 'CURRENCY', type: #fixed}, | |||
ValueListWithFixedValues | |||
} | |||
); | |||
GROSSAMOUNT : AmountT @( title: '{i18n>grossAmount}', Measures.ISOCurrency: currency); | |||
NETAMOUNT : AmountT @( title: '{i18n>netAmount}', Measures.ISOCurrency: currency); | |||
TAXAMOUNT : AmountT @( title: '{i18n>taxAmount}', Measures.ISOCurrency: currency); | |||
LIFECYCLESTATUS : StatusT @(title: '{i18n>lifecycle}', Common.FieldControl: #ReadOnly); | |||
APPROVALSTATUS : StatusT @(title: '{i18n>approval}', Common.FieldControl: #ReadOnly); | |||
CONFIRMSTATUS : StatusT @(title: '{i18n>confirmation}', Common.FieldControl: #ReadOnly); | |||
ORDERINGSTATUS : StatusT @(title: '{i18n>ordering}', Common.FieldControl: #ReadOnly); | |||
INVOICINGSTATUS : StatusT @(title: '{i18n>invoicing}', Common.FieldControl: #ReadOnly); | |||
} | |||
Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/db/PurchaseOrder.cds | |||
Code excerpt from ''/srv/fiori-annotations-my-service.cds'' for service level UI annotations: | |||
using CatalogService as cats from './my-service'; | |||
annotate cats.POItemsView with @( // header-level annotations | |||
// --------------------------------------------------------------------------- | |||
// List Report | |||
// --------------------------------------------------------------------------- | |||
// Product List | |||
UI: { | |||
LineItem: [ | |||
{$Type: 'UI.DataField', Value: PO_ITEM_ID, "@UI.Importance":#High}, | |||
{$Type: 'UI.DataField', Value: PRODUCT_ID, "@UI.Importance": #High}, | |||
{$Type: 'UI.DataField', Value: PARTNER_ID, "@UI.Importance": #High}, | |||
{$Type: 'UI.DataField', Value: PARTNERS.COMPANYNAME, "@UI.Importance": #Medium}, | |||
{$Type: 'UI.DataField', Value: AMOUNT, "@UI.Importance": #High}, | |||
{$Type: 'UI.DataField', Value: CURRENCY_CODE, "@UI.Importance": #Medium}, | |||
], | |||
PresentationVariant: { | |||
SortOrder: [ {$Type: 'Common.SortOrderType', Property: PO_ITEM_ID, Descending: false}, | |||
{$Type: 'Common.SortOrderType', Property: PRODUCT_ID, Descending: false} ] | |||
} | |||
} | |||
); | |||
Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/srv/fiori-annotations-my-service.cds | |||
Fiori allows '''XML templating'''. It will be implemented in the Main.view.xml by making use of ''template:with'' and ''template:repeat'' elements. This allow iterating over OData Annotations in order to control the UI output dynamically. | |||
Example code excerpts from ''/web/resource/poListV4/view/Main.view.xml'': | |||
<syntaxhighlight lang="XML"> | |||
<template:with path="entityType>@com.sap.vocabularies.UI.v1.LineItem" var="lineItem"> | |||
<!-- Note: this limits the data shown to 5 rows! --> | |||
<Table headerText="Purchase Orders" items="{path:'', parameters : {$expand : 'PARTNERS'}}" | |||
growingThreshold="10" growing="true"> | |||
<columns> | |||
<template:repeat list="{lineItem>}" var="field"> | |||
<template:if test="{= ${field>@com.sap.vocabularies.UI.v1.Importance/$EnumMember} !== | |||
'com.sap.vocabularies.UI.v1.ImportanceType/Low' }"> | |||
<template:then> | |||
<Column> | |||
<Label | |||
design="{:= ${field>@com.sap.vocabularies.UI.v1.Importance/$EnumMember} === | |||
'com.sap.vocabularies.UI.v1.ImportanceType/High' ? 'Bold' : 'Standard'}" | |||
text="{field>@@label}"/> | |||
</Column> | |||
</template:then> | |||
</template:if> | |||
</template:repeat> | |||
</columns> | |||
<ColumnListItem> | |||
<template:repeat list="{lineItem>}" var="field"> | |||
<Text text="{field>Value/@@value}"/> | |||
</template:repeat> | |||
</ColumnListItem> | |||
</Table> | |||
</template:with> | |||
</syntaxhighlight> | |||
Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/poListV4/view/Main.view.xml | |||
=== Consume OData V4 Service with Create Option === | |||
Example Application: | |||
/web/resource/odataCRUDV4 | |||
In order to use OData V4 appropriate settings for data source and model are necessary in ''manifest.json'', i.e.: | |||
... | |||
"sap.app": { | |||
... | |||
"dataSources": { | |||
"userService": { | |||
"uri": "/odata/v4/opensap.hana.CatalogService/", | |||
"type": "OData", | |||
"settings": { | |||
"odataVersion": "4.0" | |||
} | |||
} | |||
} | |||
... | |||
... | |||
"sap.ui5": { | |||
... | |||
"models": { | |||
"": { | |||
"type": "sap.ui.model.json.JSONModel", | |||
"settings": { | |||
"defaultBindingMode": "TwoWay" | |||
} | |||
}, | |||
"userModel": { | |||
"dataSource": "userService", | |||
"type": "sap.ui.model.odata.v4.ODataModel", | |||
"preload": false, | |||
"settings": { | |||
"synchronizationMode": "None", | |||
"operationMode": "Server", | |||
"autoExpandSelect": true | |||
} | |||
}, | |||
... | |||
} | |||
} | |||
Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/manifest.json | |||
As the SmartTable control does not yet allow OData V4 model (in early 2019) the ''m.table'' control will be used in ''App.view.xml'': | |||
<syntaxhighlight lang="XML"> | |||
<Table tableId="userTable" id="userTable" growingThreshold="10" growing="true" | |||
items="{ | |||
path: 'userModel>/User', | |||
sorter: [{ path: 'USERID', descending: false}], | |||
events : {dataReceived : '.onDataEvents' } }"> | |||
<columns> | |||
<Column> | |||
<header><Label text="User ID"/></header> | |||
</Column> | |||
<Column> | |||
<header><Label text="First Name"/></header> | |||
</Column> | |||
<Column> | |||
<header><Label text="Last Name"/></header> | |||
</Column> | |||
<Column> | |||
<header><Label text="Email"/></header> | |||
</Column> | |||
</columns> | |||
<items> | |||
<ColumnListItem> | |||
<cells> | |||
<Input value="{path: 'userModel>USERID'}" name="PERS_NO" editable="false"/> | |||
<Input value="{path: 'userModel>FIRSTNAME'}" name="FIRSTNAME"/> | |||
<Input value="{path: 'userModel>LASTNAME'}" name="LASTNAME"/> | |||
<Input value="{path: 'userModel>EMAIL'}" name="E_MAIL"/> | |||
</cells> | |||
</ColumnListItem> | |||
</items> | |||
</Table> | |||
</syntaxhighlight> | |||
Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/view/App.view.xml | |||
In order to use OData V4 the ''App.controller.js'' has to account for that. This happens by creating a binding the new data to the OData V4 model. Besides that no special model functions have to be called: | |||
... | |||
callUserService: function () { | |||
try { | |||
var oModel = this.getOwnerComponent().getModel("userModel"); | |||
var result = this.getView().getModel().getData(); | |||
var oList = this.byId("userTable"), | |||
oBinding = oList.getBinding("items"), | |||
// Create a new entry through the table's list binding | |||
oContext = oBinding.create({ | |||
"USERID": 0, | |||
"FIRSTNAME": result.FirstName, | |||
"LASTNAME": result.LastName, | |||
"EMAIL": result.Email | |||
}); | |||
// Note: this promise fails only if the transient entity is deleted | |||
oContext.created().then(function () { | |||
sap.m.MessageBox.alert("User created: " + oContext.getProperty("USERID")); | |||
}, function (oError) { | |||
sap.m.MessageBox.alert(oError.toString()); | |||
}); | |||
} catch (err) { | |||
sap.m.MessageBox.alert(err.toString()); | |||
} | |||
}, | |||
callUserUpdate: function () { | |||
var oModel = this.getOwnerComponent().getModel("userModel"); | |||
var mParams = {}; | |||
mParams.error = function () { | |||
sap.m.MessageToast.show("Update failed"); | |||
}; | |||
mParams.success = function () { | |||
sap.m.MessageToast.show("Update successful"); | |||
}; | |||
oModel.submitChanges(mParams); | |||
} , | |||
... | |||
Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/controller/App.controller.js | |||
=== Asynchronous Loading of Controls in Code === | |||
Loading of controls asynchronously is done by the <code>sap.ui.require</code> function. | |||
This improves initial loading time of the application and reduces its size. | |||
Example: | |||
<syntaxhighlight lang="JavaScript"> | |||
onPress: function (oEvent) { | |||
sap.ui.require(["sap/m/MessageToast"], function (oMessage) { | |||
oMessage.show("Searching..."); | |||
} ); | |||
} | |||
</syntaxhighlight> | |||
=== Logging Events to Console === | |||
Add ''data-sap-ui-logLevel'' to bootstrap: | |||
<syntaxhighlight lang="HTML"> | |||
<script id="sap-ui-bootstrap" | |||
src="resources/sap-ui-core.js" | |||
data-sap-ui-theme="sap_fiori_3" | |||
data-sap-ui-resourceroots='{"opensap.movies": "./"}' | |||
data-sap-ui-compatVersion="edge" | |||
data-sap-ui-logLevel="debug" | |||
data-sap-ui-oninit="module:sap/ui/core/ComponentSupport" | |||
data-sap-ui-async="true" | |||
data-sap-ui-frameOptions="trusted"> | |||
</script> | |||
</syntaxhighlight> | |||
Add logging at controller level by adding ''sap/base/Log'' module: | |||
<syntaxhighlight lang="JavaScript"> | |||
sap.ui.define([ | |||
"sap/ui/core/mvc/Controller", | |||
"sap/base/Log" | |||
], function (Controller, Log) { | |||
"use strict"; | |||
return Controller.extend("opensap.movies.controller.App", { | |||
onInit: function () { | |||
Log.info("Controller has been initialized."); | |||
}, | |||
onExit: function () { | |||
Log.info("Controller will shortly be destroyed."); | |||
}, | |||
onBeforeRendering: function () { | |||
Log.info("The view will shortly be rendered."); | |||
}, | |||
onAfterRendering: function () { | |||
Log.info("The view has been rendered."); | |||
}, | |||
onPress: function (sValue) { | |||
sap.ui.require(["sap/m/MessageToast"], function (oMessage) { | |||
oMessage.show("Searching..." + sValue); | |||
} ); | |||
} | |||
}); | |||
}); | |||
</syntaxhighlight> | |||
=== Route to View on Event === | |||
Specify routes in manifest.json, here for the Detail view: | |||
<syntaxhighlight lang="JSON"> | |||
"routing": { | |||
"config": { | |||
"routerClass": "sap.m.routing.Router", | |||
"viewType": "XML", | |||
"async": true, | |||
"viewPath": "opensap.movies.view", | |||
"controlAggregation": "pages", | |||
"controlId": "app", | |||
"clearControlAggregation": false | |||
}, | |||
"routes": [{ | |||
"name": "Home", | |||
"pattern": "", | |||
"target": ["Home"] | |||
}, { | |||
"name": "Detail", | |||
"pattern": "movies/{movieId}/appointments/{appointmentId}", | |||
"titleTarget": "", | |||
"greedy": false, | |||
"target": ["Detail"] | |||
}], | |||
</syntaxhighlight> | |||
Specify event and eventhandler in view definition, here event ''appointmentSelect'': | |||
<syntaxhighlight lang="HTML"> | |||
<PlanningCalendar id="calendar" startDate="{path: 'movies>/initDate', formatter: '.formatter.formatDate'}" rows="{movies>/movies}" | |||
appointmentsVisualization="Filled" appointmentSelect=".onAppointmentSelect(${$parameters>/appointment})"> | |||
<toolbarContent> | |||
... | |||
</toolbarContent> | |||
<rows> | |||
<PlanningCalendarRow title="{movies>name}" text="{movies>genre}" appointments="{path: 'movies>appointments', templateShareable: 'true'}"> | |||
<appointments> | |||
... | |||
</appointments> | |||
</PlanningCalendarRow> | |||
</rows> | |||
</PlanningCalendar> | |||
</syntaxhighlight> | |||
Full listing: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/view/App.view.xml | |||
Add UIComponent to controller to access the app router: | |||
<syntaxhighlight lang="JavaScript"> | |||
sap.ui.define([ | |||
..., | |||
"sap/ui/core/UIComponent" | |||
], function (Controller, Log, formatter, Filter, FilterOperator, UIComponent) { | |||
"use strict"; | |||
return Controller.extend("opensap.movies.controller.App", { | |||
... | |||
onAppointmentSelect: function (oAppointment) { | |||
var oContext = oAppointment.getBindingContext("movies"), | |||
sPath = oContext.getPath(); | |||
var aParameters = sPath.split("/"); | |||
UIComponent.getRouterFor(this).navTo("Detail", { | |||
movieId: aParameters[2], | |||
appointmentId: aParameters[4] | |||
}); | |||
} | |||
}); | |||
}); | |||
</syntaxhighlight> | |||
Full listing: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/controller/App.controller.js | |||
Now the ''Detail.controller.js'' of the ''Detail'' view has to be enabled to respond to the new route pattern: | |||
<syntaxhighlight lang="JavaScript"> | |||
onInit: function () { | |||
UIComponent.getRouterFor(this).getRoute("Detail").attachPatternMatched(this._onDetailMatched, this); | |||
}, | |||
_onDetailMatched: function (oEvent) { | |||
var oView = this.getView(), | |||
sMovieIndex = oEvent.getParameter("arguments")["movieId"], | |||
sAppointmentIndex = oEvent.getParameter("arguments")["appointmentId"]; | |||
oView.bindElement({ | |||
path: "/movies/" + sMovieIndex + "/appointments/" + sAppointmentIndex, | |||
model: "movies", | |||
events: { | |||
change: this._onBindingChange.bind(this) | |||
} | |||
}); | |||
}, | |||
_onBindingChange: function () { | |||
var oView = this.getView(), | |||
oElementBinding = oView.getElementBinding("movies"), | |||
sPath = oElementBinding.getPath(); | |||
// if the path to the data does not exist we navigate to the not found page | |||
if (!oView.getModel("movies").getObject(sPath)) { | |||
//See Challenge at the end: UIComponent.getRouterFor(this).getTargets().display("NotFound"); | |||
} | |||
} | |||
</syntaxhighlight> | |||
''onDetailsChanged'' binds the view to the model using the path of the appointment the user clicked on. This will display automatically the chosen appointment details on the ''Detail'' view. | |||
''onBindingChange'' transfers the binding contect to the ''Detail'' view. | |||
=== Implement NotFound Page === | |||
Documentation: https://ui5.sap.com/#/topic/e047e0596e8a4a1db50f4a53c11f4276 | |||
Implementation: https://github.com/mattxdev/opensap-ui52-movies/commit/6225043dbe114bf217f570d43db0a1ee395a0013 | |||
=== Implement Feature-Rich Controls === | |||
Feature-Rich controls implemented in exercise: | |||
{| class="wikitable" | |||
! Control !! Library !! Documentation | |||
|- | |||
| Sticky table header || || | |||
|- | |||
| Floating footer || || | |||
|- | |||
| Object Page Layout || sap.uxap.ObjectPageLayout || | |||
* [https://ui5.sap.com/#/topic/d1ffe611194b4c7891772b0cce84648e d1ffe611194b4c7891772b0cce84648e] | |||
* [https://ui5.sap.com/#/api/sap.uxap.ObjectPageLayout sap.uxap.ObjectPageLayout] | |||
|- | |||
| Info Label || sap.tnt.InfoLabel || | |||
* [https://ui5.sap.com/#/api/sap.tnt.InfoLabel sap.tnt.InfoLabel] | |||
|- | |||
| Object Number || sap.m.ObjectNumber || | |||
* [https://ui5.sap.com/#/entity/sap.m.ObjectNumber sap.m.ObjectNumber] | |||
|- | |||
| Status Indicator || sap.suite.ui.commons.statusindicator.StatusIndicator || | |||
* [https://ui5.sap.com/#/topic/8d5664a644f14063aa05cc8d18aa56eb 8d5664a644f14063aa05cc8d18aa56eb] | |||
* [https://ui5.sap.com/#/api/sap.suite.ui.commons.statusindicator.StatusIndicator sap.suite.ui.commons.statusindicator.StatusIndicator] | |||
|} | |||
Exercise from openSAP course UI52: Week 3 Unit 4: Spicing up Your Scenario with Feature-Rich Controls | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/commit/6f8c9aab0a24c735c4309ae0217e4f2e87f055ac | |||
=== Reusing Patterns === | |||
Reuse Patterns: | |||
* Control | |||
* Fragment | |||
* View | |||
* Component | |||
* Library | |||
Control Pattern: | |||
* Encapsulates Rendering and Behavior | |||
* Standardized Control Interface | |||
* Smallest Reusable UI Asset | |||
* Custom or Composite Control | |||
* Bundled in Control Libraries | |||
XML Composite Control | |||
* Declarative UI definition in XML | |||
* Special $this model for binding properties | |||
* Control metadata description | |||
Create XML composite control: | |||
* Create new folder "control" inside "webapp" | |||
* Create .js file for controller and metadata | |||
* Create .xml file for layout | |||
Exercise from openSAP course UI52: Week 3 Unit 5: Scaling Up with UI Reuse patterns | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/commit/3bc3a2ed14f0d4686abec6511063cdb633a145d0 | |||
=== Add CRUD Form to Flexible Column Layout === | |||
Exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input | |||
Form will be called by add-Button ("+") in Detail View. Implementation: | |||
<syntaxhighlight lang="XML"> | |||
<Toolbar> | |||
<Title id="lineItemsTitle" text="{detailView>/lineItemListTitle}" titleStyle="H3" level="H3"/> | |||
<ToolbarSpacer /> | |||
<Button icon="sap-icon://add" tooltip="{i18n>createButtonTooltip}" press=".onCreate"/> | |||
</Toolbar> | |||
</syntaxhighlight> | |||
Create a new XML View and controller. Implementations: | |||
* https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Create.view.xml | |||
* https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js | |||
Add create target and create route to the ''manifest.json'': | |||
<syntaxhighlight lang="JSON"> | |||
"routes": [ | |||
{ | |||
"pattern": "SalesOrderSet/{objectId}/create", | |||
"name": "create", | |||
"target": [ | |||
"master", | |||
"object", | |||
"create" | |||
] | |||
} | |||
], | |||
"targets": { | |||
"create": { | |||
"viewType": "XML", | |||
"viewName": "Create", | |||
"controlAggregation": "endColumnPages", | |||
"viewId": "create" | |||
} | |||
} | |||
</syntaxhighlight> | |||
Full implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/manifest.json | |||
Information about Routing and Navigation: https://ui5.sap.com/#/topic/3d18f20bd2294228acb6910d8e8a5fb5 | |||
Extend existing ''Detail.controller.js'': | |||
https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Detail.controller.js | |||
=== Add Input Validation Client and Server Side === | |||
Add value property to the input fields: | |||
* ProductID | |||
* Note | |||
* Quantity | |||
Set path and type of each value property to the corresponding property from the oData model. This way the app uses backend validation and ensures that the oData properties will receive correct entries regarding their type. | |||
Use contraints to ensure input of correct values at the client side. In the following implementation this happens with regular expressions. Also set minimum value for quantity. | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Create.view.xml | |||
Define ''defaultBindingMode'' to ''TwoWay'' in order to be able to handle the transport of data both from the model to the controls and back from the controls to the model. | |||
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input | |||
=== Add MessageManager for Displaying Messages === | |||
In ''onInit'' a global message manager will be registered and a special ''message'' model will be established to redtieve and display error messages. | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js | |||
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input | |||
Documentation: | |||
* https://ui5.sap.com/#/entity/sap.ui.core.message.MessageManager | |||
* https://ui5.sap.com/#/topic/b8c4e534cdb440e9a5bbff86f9572bd6 | |||
=== Message Popover for Logging Errors === | |||
In general it is a good idea in terms of user experience to store all errors in one place. This helps the user to find and track errors. ''sap.m.MessagePopover'' is a good choice for this. | |||
Add a button to the overflow toolbar for showing the message popover. | |||
Initialize the ''MessageManager'' in ''onInit''. Register the root element ''ObjectPageLayout'' of the view as an object for which errors will be tracked and displayed in the popover. | |||
Ensure to remove all server-side erroors by removing all messages from the ''MessageManager'' ''_onCreateMatched''. | |||
Als remove old messages when the product name is changed and is about to be validated. This is implemented at the ''change'' event of the correspodings input field in the view - here implemented in the ''onNameChange'' function. | |||
In the handler of the popover button ''onOpenMessages'' open the ''MessagePopover''. | |||
Implementations: | |||
* https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Create.view.xml | |||
* https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js | |||
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input | |||
=== Creating Entries with OData === | |||
First, when the route is matched and the view is created an entry is created locally in ''_onCreateMatched'' by utilizing the ''createEntry'' Method, specifying a collection, in our case ''SalesOrderLineItemSet''. Also initial values to the mandatory fields from the model are added. As a result of the ''createEntry'' function you receife a contect that points to the new object. It must be set to the view. | |||
<syntaxhighlight lang="JavaScript"> | |||
_onCreateMatched: function (oEvent) { | |||
var sObjectId = oEvent.getParameter("arguments").objectId; | |||
// create a binding context for a new order item | |||
this.oContext = this.getModel().createEntry("/SalesOrderLineItemSet", { | |||
properties: { | |||
SalesOrderID: sObjectId, | |||
ProductID: "", | |||
Note: "", | |||
Quantity: "1", | |||
DeliveryDate: new Date() | |||
}, | |||
success: this._onCreateSuccess.bind(this) | |||
}); | |||
this.getView().setBindingContext(this.oContext); | |||
... | |||
}, | |||
_onCreateSuccess: function (oContext) { | |||
// show success message | |||
var sMessage = this.getResourceBundle().getText("newItemCreated", [oContext.ProductID]); | |||
MessageToast.show(sMessage, { | |||
closeOnBrowserNavigation : false | |||
}); | |||
// navigate to the new item in display mode | |||
this.getRouter().navTo("Info", { | |||
objectId : oContext.SalesOrderID, | |||
itemPosition : oContext.ItemPosition | |||
}, true); | |||
}, | |||
onCreate: function () { | |||
// send new item to server for processing | |||
this.getModel().submitChanges(); | |||
}, | |||
</syntaxhighlight> | |||
In the ''onCreate'' handler the ''submit'' function of the OData model will be called. It creates a HTTP request against the server. The server completes the creatte action and sends a HTTP response back. | |||
Register to the ''success'' event afterwards via the ''_onCreateSuccess'' callback function. | |||
Afther the _onCreateSuccess function is triggered, the view will be unbound from the created object. A MessageToast shows a success message to the user. Lastly the As a last step, navigate to the created item. | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js | |||
Documentation: https://ui5.sap.com/#/topic/6c47b2b39db9404582994070ec3d57a2.html | |||
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input | |||
=== Enable Drag and Drop in Lists === | |||
The drag and drop feature will be enabled with the ''dragDropConfig'' aggregation and the ''DragInfo'' or ''DropInfo'' element inside of it, depending of if you want it to be a source or a target for drag and drop. | |||
Add dragDropConfig aggregation and define the drag source by adding DragInfo inside of it with the property ''sourceAggregation'': | |||
<syntaxhighlight lang="XML"> | |||
<dragDropConfig> | |||
<dnd:DragInfo sourceAggregation="items"/> | |||
</dragDropConfig> | |||
</syntaxhighlight> | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Detail.view.xml | |||
Create a new custom button and extend its metadata: | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/control/DeleteButton.js | |||
Add XML code to enable drop feature to the custom button: | |||
<syntaxhighlight lang="XML"> | |||
<control:DeleteButton | |||
icon="sap-icon://delete"> | |||
<control:dragDropConfig> | |||
<dnd:DropInfo drop=".onDelete" /> | |||
</control:dragDropConfig> | |||
</control:DeleteButton> | |||
</syntaxhighlight> | |||
Implementation of the drop/press handler in ''Detail.controller.js'': | |||
<syntaxhighlight lang="JavaScript"> | |||
onDelete : function (oEvent) { | |||
// delete the dragged item | |||
var oItemToDelete = oEvent.getParameter("draggedControl"); | |||
// delete the selected item from the list - if nothing selected, remove the first item | |||
if (!oItemToDelete) { | |||
var oList = this.byId("lineItemsList"); | |||
oItemToDelete = oList.getSelectedItem() || oList.getItems()[0]; | |||
} | |||
// delete the item after user confirmation | |||
var sPath = oItemToDelete.getBindingContextPath(), | |||
sTitle = oItemToDelete.getBindingContext().getProperty("ProductID"); | |||
this._confirmDelete(sPath, sTitle); | |||
} | |||
</syntaxhighlight> | |||
In order to provide a better user experience the user should be asked for confirmation before an item is deleted. this will be done with the ''_confirmDelete'' function. | |||
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Detail.controller.js | |||
See also exercise from openSAP course UI52: Week 4 Unit 2 - Exercise: Improving User Experience with Drag and Drop | |||
Documentation: | |||
* Drag and Drop Documentation: https://ui5.sap.com/#/topic/3ddb6cde6a8d416598ac8ced3f5d82d5 | |||
* API reference: https://ui5.sap.com/#/api/sap.ui.core.dnd | |||
* Drag-and-Drop enabled for all UI5 controls: https://blogs.sap.com/2018/07/19/drag-and-drop-is-now-enabled-for-all-ui5-controls/ | |||
== Debugging Tools == | |||
Open diagnostics tool: <code>Ctrl</code> + <code>Shift</code> + <code>Alt</code> + <code>S</code> | |||
Use debug sources: <code>Ctrl</code> + <code>Shift</code> + <code>Alt</code> + <code>P</code> | |||
After that activate the "Use Debug Sources" checkbox. When reloading in the Chrome DevTools files will be loaded with the "-dbg" suffix. These are source code files that include comments and uncompressed code of the OpenUI5 artifacts. | |||
This can be also achieved by adding the parameter to the url: <code>sap-ui-debug=true</code> | |||
Aktuelle Version vom 10. Februar 2023, 16:55 Uhr
This page contains basic knowledge on how to start with SAPUI5 development on SAP HANA development stack and also how the SAPUI5 application looks like.
The repository for the examples can be found here: https://github.com/mattxdev/opensap-hana7
SAPUI5 App Structure
Dynamic odataView App
We create an app odataView. All of its files will be located inside
/web/resources/odataView/
Content:
Relative Path | File | Function |
---|---|---|
./ | index.html |
|
./ | Component.js |
|
./ | manifest.json |
Also called: Application descriptor Specifies:
|
./i18n/ | i18n_en.properties |
|
./view/ | App.view.xml |
Contains:
|
./view/ | MRead.fragment.xml |
Contains:
|
./view/ | MTableHead.fragment.xml |
|
./view/ | MTableItem.fragment.xml |
|
./controller/ | App.controller.js |
Contains methods:
sap.ui.define is used for asynchronous loading |
./controller/ | Base.controller.js |
Base controllers can contain functions and share them to multiple app controllers. |
./model/ | formatter.js, grouper.js, GroupSortState.js, models.js |
Model subdirectory contains reusable functions like formatters |
Basic OData App Skeleton from SAP
Source: https://github.com/SAP-samples/hana-xsa-opensap-hana7/blob/snippets_2.3.2/ex4/odataBasic.zip
The file contains all necessary subfolders and should be imported in your project into this module:
/web/resources
This will create a subfolder for the skeleton app named:
/web/resources/odataBasic
Tasks to be done in order to bind an OData service to the SmartTable defined wihtin the App.view in the skeleton app:
File | Section | Task |
---|---|---|
manifest.json | "sap.app" |
|
manifest.json | "models" |
|
controller/App.controller.js | onInit |
|
Text Bundles
Text bundles are best located in a subfolder of:
/web/resources/
Contents:
Relative Path | File | Function |
---|---|---|
./i18n/ | messagebundle.properties | Default language message bundle |
./i18n/ | messagebundle_de.properties | Language DE message bundle |
A language can be selected by changing browser language or adding the URL parameter sap-ui-langauge=DE_de to tue URL of your app.
Add code:
/* Language Resource Loader */
jQuery.sap.require("jquery.sap.resources");
var sLocale = sap.ui.getCore().getConfiguration().getLanguage();
var oBundle = jQuery.sap.resources({url: "./i18n/messagebundle.properties", locale: sLocale});
// create the button instance
var myButton = new sap.m.Button("btn");
// set properties, e.g. the text (there is also a shorter way of setting several properties)
// myButton.setText("Hello World!");
myButton.setText(oBundle.getText("helloworld"));
Listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/index.html
Local SAPUI5 Micro Service
SAPUI5 libraries can be used from the public SAPUI5 libraries.
Since SAP HANA 2.0 SPS 03 XS Advanced is capable of providing a micro service itself for local consumption.
A local micro service can be created either in XS Advanced Cockpit at
- HANAExpress -> development space -> Services -> Service Marketplace -> sapui5_sb
or via command line with:
xs create-service sapui5_sb sapui5-1.52 openSAPHANA_00-ui5
The service has to be added in:
Path | File | Function |
---|---|---|
/ | mta.yaml | Create new resource and add it to modules |
/web/ | xs-app.json | Use replace capability of app router to insert UI5 service URL dynamically |
/web/ | index.html | Fetch bootstrap from resource by utilizing variable |
UI Controls and Libraries
Control | Library | Function | Properties |
---|---|---|---|
Image | sap.m | Show image | src, width, tooltip |
Page | sap.m | Create a page with footer | title |
Link | sap.m | Show link | emphasized, text, target, href |
SearchField | sap.m | Input field for search | placeholder |
Select | sap.m | List of items for selection | |
Button | sap.m | Button for triggering actions | text |
Label | sap.m | Text label for other controls | labelFor |
SimpleForm | sap.ui.layout.form | Create nicely arranged simple forms | layout, columnsM, columnsL, columnsXL |
MessageToast | sap.m | Display a message | text |
PlanningCalendar | sap.ui.unified | Display rows with appointments | startDate, rows, Aggregation PlanningCalendarRow |
Aggregations of controls are used to nest other controls to be displayed inside them.
Association to the Select control is the currently selected item.
Examples: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/view/App.view.xml
Events
Event | Belongs to | Description |
---|---|---|
press | Button, Image | Fires when pressing on a button |
onInit | Controller | After the controller is initialized |
onBeforeRendering | Controller | Before rendering, i.e. for checking device parameters |
onAfterRendering | Controller | After rendering, i.e. for checking contents of DOM |
onExit | Controller | Before destroying the App |
Example Listing: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/controller/App.controller.js
URL Parameters
Parameters are specified by adding parameter-value-pairs to the URL. Example:
index.html?<parameter>=<value> index.html?sap-ui-language=de_DE
Parameter | Description | Example Values |
---|---|---|
sap-ui-language | Set language in as <Language>_<Region> |
German in Germany: de_DE |
sap-ui-theme | Set theme |
High Contrast White: sap_belize_hcw |
How to Implement
Dynamic Table Field List Based on OData Service Metadata
This example is based on the odataView App from above.
Visible table columns can eiter be hard coded in controller/App.controller.js in OnInit with oTable.setInitiallyVisibleFields:
onInit: function() { this.getView().addStyleClass("sapUiSizeCompact"); // make everything inside this View appear in Compact mode var oConfig = this.getOwnerComponent().getModel("config"); var userName = oConfig.getProperty("/UserName"); var bpModel = this.getOwnerComponent().getModel("bpModel"); var oTable = this.getView().byId("bpTable"); oTable.setModel(bpModel); oTable.setEntitySet("BusinessPartners"); oTable.setInitiallyVisibleFields("PARTNERID,COMPANYNAME,PARTNERROLE"); },
Or they can be made visible dynamically by looking them up in the meta data of the service at dataServices.schema[0].entityType[0].property:
function fnLoadMetadata() { try { oTable.setModel(bpModel); oTable.setEntitySet("BusinessPartners"); var oMeta = bpModel.getServiceMetadata(); var headerFields = ""; for (var i = 0; i < oMeta.dataServices.schema[0].entityType[0].property.length; i++) { var property = oMeta.dataServices.schema[0].entityType[0].property[i]; headerFields += property.name + ","; } oTable.setInitiallyVisibleFields(headerFields); } catch (e) { console.log(e.toString()); } } bpModel.attachMetadataLoaded(bpModel, function() { fnLoadMetadata(); }); fnLoadMetadata();
Hint: If your service metadata has multiple tables then you probably have to loop through the different entityType or even schema.
OData CRUD with Input List and SmartTable
Based on the odataView app from above the following has to be changed:
(The results are implemented in the odataCRUD app.
Model in manifest.json has to be changed:
"userModel": { "dataSource": "userService", "type": "sap.ui.model.odata.v2.ODataModel", "preload": true, "settings": { "useBatch": false, "json": true, "defaultBindingMode": "TwoWay", "defaultUpdateMethod": "PUT" } },
Notably is the defaultBindingMode TwoWay. This means that changes, which happen in the odata Service, are automatically reflected in bound UI controls and vice versa. When changes happen in the UI controls, they are automatically reflected in the model and sent to the server.
New event handlers in app controller for the buttons:
- callUserService -> for create operation -> calls the oModel.create function
- callUserUpdate -> for update operation -> calls the oModel.submitChanges function
Batch Insert with XMLfragment based Dynamically Created Dialog
Batching enables serverside parallelization if the queries are sent to the server as a batch operation.
Example Application: odataCRUD --> See event handler for onBatchDialogPress and the respective buttons and fragments.
Event handler getItem creates dynamically dialog elements in the UI for add and delete icon as well as input fields for first name, last name and email.
Event handler onSubmitBatch handles the batch processing. For that first a JSON object with all users will be created.
Class sap.ui.model.odata.v2.ODataModel parameter oParams.usetBatch = true
overwrites the settings from the manifest.json. Also a new model will be created to use the batch option.
This enables SAPUI5 to communicate with a odata service with different settings.
When batch is used a mParams.groupId will be available. It should be set.
batchModel.create sends the records actually one by one to the server. But as the model was created with the useBatch option all the records will be sent to the server as a batch after all commands are done on the client side.
Documentation: https://sapui5.hana.ondemand.com/#/api/sap.ui.model.odata.v2.ODataModel
OData V2 Deep Insert with Content-ID and Links
Example Application:
/web/resource/odataDeep
Deep inserts allow to insert data into tables with parent-child relationship and ensure that all the inserted records of both tables are correctly related to each other.
In the example below this actually happens by inserting records into both tables seperately using content ID and then updating the second table to have the same primary keys as the first table.
Definition of OData Service businessPartnersAddresses.xsodata:
service { "MD.BusinessPartner" as "BusinessPartners" navigates ("ToAddresses" as "AddRef") create events(before "sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::bp_create_before_exit"); "MD.Addresses" as "Addresses" without ("POINT") create events(before "sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::address_create_before_exit"); association "ToAddresses" principal "BusinessPartners"("ADDRESSES.ADDRESSID") multiplicity "1" dependent "Addresses"("ADDRESSID") multiplicity "1" over "MD.Addresses" principal("ADDRESSID") dependent ("ADDRESSID") update using "sap.hana.democontent.epm.services:businessPartnersAddresses.xsjslib::assocation_create_exit"; }
The service has three user exits:
- bp_create_before_exit: Generates partnerID from sequence and fills some fields before create
- address_create_before_exit: Generates addressID from sequence and fills some fields before create
- association_create_exit: Fetches both previously created records and updates the business partner record with the addressID from the address record.
Listing of businessPartnersAddresses.xsjslib containing the three exits:
In order to use batch with content-ids the SAPUI5 odata object cannot be used. Instead the request will be build by the controller during run time. Calling of the services utilizes jQuery.ajax and must also handle CSRF tokens.
index.html ... <script type="text/javascript" src="/common/csrf.js" ></script> ...
Listing of app controller which builds the complete OData batch request (unlike earlier SAPUI5 exercises where the framework did all the work):
Notice the Content-ID option. It works as a place holder ($1 and $2) to represent the keys which will not be generated until the server side has processed the work. The $1 and $2 placeholders can be used in the association URL and work as placeholders for actual values. XSODATA framework on the server side will replace this values during processing.
Fiori Annotations with OData V4 and CDS
Example Application:
/web/resource/poListV4
OData V4 and CDS enable a large range of metadata definitions for Fiori applications to allow nearly everything about the UI like:
- Value helps
- Sort Options
- Output columns
The metadata can be placed at different levels of the CDS like:
- Lowest level: At the entity definitions
- At the service level or UI page
Type | Description |
---|---|
title | Text description used as field label and column headers. Can be used at reusable type level or within entity or view definition |
FieldControl | Set state of input field to i.e. Read Only or Mandatory |
Measures | i.e. amount of fields gets measure from currency column |
ValueList | i.e. currency column which points to foreign entity CURRENCY. This allows to pupulate drop down list boxes or fill value helps within the UI from this definition. |
Code excerpts for entity level annotations from /db/PurchaseOrder.cds:
type HistoryT { CREATEDBY : BusinessKey @(title: '{i18n>CreatedBy}', Common.FieldControl: #ReadOnly); CREATEDAT : SDate @(title: '{i18n>CreatedAt}', Common.FieldControl: #ReadOnly); CHANGEDBY : BusinessKey @(title: '{i18n>ChangedBy}', Common.FieldControl: #ReadOnly); CHANGEDAT : SDate @(title: '{i18n>ChangedAt}', Common.FieldControl: #ReadOnly); }; entity Headers { key PURCHASEORDERID : Integer @( title: '{i18n>po_id}', Common.FieldControl: #Mandatory, Search.defaultSearchElement, Common.Label: '{i18n>po_id}' ); ITEMS : association to many Items on ITEMS.POHeader = $self @( title: '{i18n>po_items}', Common: { Text: {$value: ITEMS.PRODUCT, "@UI.TextArrangement": #TextOnly }} ); HISTORY : HistoryT; NOTEID : BusinessKey null @title: '{i18n>notes}'; PARTNER : BusinessKey @title: '{i18n>partner_id}'; CURRENCY : CurrencyT @( Common: { Text: {$value: CURRENCY.CURRENCY, "@UI.TextArrangement": #TextOnly}, ValueList: {entity: 'CURRENCY', type: #fixed}, ValueListWithFixedValues } ); GROSSAMOUNT : AmountT @( title: '{i18n>grossAmount}', Measures.ISOCurrency: currency); NETAMOUNT : AmountT @( title: '{i18n>netAmount}', Measures.ISOCurrency: currency); TAXAMOUNT : AmountT @( title: '{i18n>taxAmount}', Measures.ISOCurrency: currency); LIFECYCLESTATUS : StatusT @(title: '{i18n>lifecycle}', Common.FieldControl: #ReadOnly); APPROVALSTATUS : StatusT @(title: '{i18n>approval}', Common.FieldControl: #ReadOnly); CONFIRMSTATUS : StatusT @(title: '{i18n>confirmation}', Common.FieldControl: #ReadOnly); ORDERINGSTATUS : StatusT @(title: '{i18n>ordering}', Common.FieldControl: #ReadOnly); INVOICINGSTATUS : StatusT @(title: '{i18n>invoicing}', Common.FieldControl: #ReadOnly); }
Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/db/PurchaseOrder.cds
Code excerpt from /srv/fiori-annotations-my-service.cds for service level UI annotations:
using CatalogService as cats from './my-service'; annotate cats.POItemsView with @( // header-level annotations // --------------------------------------------------------------------------- // List Report // --------------------------------------------------------------------------- // Product List UI: { LineItem: [ {$Type: 'UI.DataField', Value: PO_ITEM_ID, "@UI.Importance":#High}, {$Type: 'UI.DataField', Value: PRODUCT_ID, "@UI.Importance": #High}, {$Type: 'UI.DataField', Value: PARTNER_ID, "@UI.Importance": #High}, {$Type: 'UI.DataField', Value: PARTNERS.COMPANYNAME, "@UI.Importance": #Medium}, {$Type: 'UI.DataField', Value: AMOUNT, "@UI.Importance": #High}, {$Type: 'UI.DataField', Value: CURRENCY_CODE, "@UI.Importance": #Medium}, ], PresentationVariant: { SortOrder: [ {$Type: 'Common.SortOrderType', Property: PO_ITEM_ID, Descending: false}, {$Type: 'Common.SortOrderType', Property: PRODUCT_ID, Descending: false} ] } } );
Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/srv/fiori-annotations-my-service.cds
Fiori allows XML templating. It will be implemented in the Main.view.xml by making use of template:with and template:repeat elements. This allow iterating over OData Annotations in order to control the UI output dynamically.
Example code excerpts from /web/resource/poListV4/view/Main.view.xml:
<template:with path="entityType>@com.sap.vocabularies.UI.v1.LineItem" var="lineItem">
<!-- Note: this limits the data shown to 5 rows! -->
<Table headerText="Purchase Orders" items="{path:'', parameters : {$expand : 'PARTNERS'}}"
growingThreshold="10" growing="true">
<columns>
<template:repeat list="{lineItem>}" var="field">
<template:if test="{= ${field>@com.sap.vocabularies.UI.v1.Importance/$EnumMember} !==
'com.sap.vocabularies.UI.v1.ImportanceType/Low' }">
<template:then>
<Column>
<Label
design="{:= ${field>@com.sap.vocabularies.UI.v1.Importance/$EnumMember} ===
'com.sap.vocabularies.UI.v1.ImportanceType/High' ? 'Bold' : 'Standard'}"
text="{field>@@label}"/>
</Column>
</template:then>
</template:if>
</template:repeat>
</columns>
<ColumnListItem>
<template:repeat list="{lineItem>}" var="field">
<Text text="{field>Value/@@value}"/>
</template:repeat>
</ColumnListItem>
</Table>
</template:with>
Full listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/poListV4/view/Main.view.xml
Consume OData V4 Service with Create Option
Example Application:
/web/resource/odataCRUDV4
In order to use OData V4 appropriate settings for data source and model are necessary in manifest.json, i.e.:
... "sap.app": { ... "dataSources": { "userService": { "uri": "/odata/v4/opensap.hana.CatalogService/", "type": "OData", "settings": { "odataVersion": "4.0" } } } ... ... "sap.ui5": { ... "models": { "": { "type": "sap.ui.model.json.JSONModel", "settings": { "defaultBindingMode": "TwoWay" } }, "userModel": { "dataSource": "userService", "type": "sap.ui.model.odata.v4.ODataModel", "preload": false, "settings": { "synchronizationMode": "None", "operationMode": "Server", "autoExpandSelect": true } }, ... } }
Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/manifest.json
As the SmartTable control does not yet allow OData V4 model (in early 2019) the m.table control will be used in App.view.xml:
<Table tableId="userTable" id="userTable" growingThreshold="10" growing="true"
items="{
path: 'userModel>/User',
sorter: [{ path: 'USERID', descending: false}],
events : {dataReceived : '.onDataEvents' } }">
<columns>
<Column>
<header><Label text="User ID"/></header>
</Column>
<Column>
<header><Label text="First Name"/></header>
</Column>
<Column>
<header><Label text="Last Name"/></header>
</Column>
<Column>
<header><Label text="Email"/></header>
</Column>
</columns>
<items>
<ColumnListItem>
<cells>
<Input value="{path: 'userModel>USERID'}" name="PERS_NO" editable="false"/>
<Input value="{path: 'userModel>FIRSTNAME'}" name="FIRSTNAME"/>
<Input value="{path: 'userModel>LASTNAME'}" name="LASTNAME"/>
<Input value="{path: 'userModel>EMAIL'}" name="E_MAIL"/>
</cells>
</ColumnListItem>
</items>
</Table>
Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/view/App.view.xml
In order to use OData V4 the App.controller.js has to account for that. This happens by creating a binding the new data to the OData V4 model. Besides that no special model functions have to be called:
... callUserService: function () { try { var oModel = this.getOwnerComponent().getModel("userModel"); var result = this.getView().getModel().getData(); var oList = this.byId("userTable"), oBinding = oList.getBinding("items"), // Create a new entry through the table's list binding oContext = oBinding.create({ "USERID": 0, "FIRSTNAME": result.FirstName, "LASTNAME": result.LastName, "EMAIL": result.Email }); // Note: this promise fails only if the transient entity is deleted oContext.created().then(function () { sap.m.MessageBox.alert("User created: " + oContext.getProperty("USERID")); }, function (oError) { sap.m.MessageBox.alert(oError.toString()); }); } catch (err) { sap.m.MessageBox.alert(err.toString()); } }, callUserUpdate: function () { var oModel = this.getOwnerComponent().getModel("userModel"); var mParams = {}; mParams.error = function () { sap.m.MessageToast.show("Update failed"); }; mParams.success = function () { sap.m.MessageToast.show("Update successful"); }; oModel.submitChanges(mParams); } , ...
Entire listing: https://github.com/mattxdev/opensap-hana7/blob/master/web/resources/odataCRUDV4/controller/App.controller.js
Asynchronous Loading of Controls in Code
Loading of controls asynchronously is done by the sap.ui.require
function.
This improves initial loading time of the application and reduces its size.
Example:
onPress: function (oEvent) {
sap.ui.require(["sap/m/MessageToast"], function (oMessage) {
oMessage.show("Searching...");
} );
}
Logging Events to Console
Add data-sap-ui-logLevel to bootstrap:
<script id="sap-ui-bootstrap"
src="resources/sap-ui-core.js"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-resourceroots='{"opensap.movies": "./"}'
data-sap-ui-compatVersion="edge"
data-sap-ui-logLevel="debug"
data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
data-sap-ui-async="true"
data-sap-ui-frameOptions="trusted">
</script>
Add logging at controller level by adding sap/base/Log module:
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/base/Log"
], function (Controller, Log) {
"use strict";
return Controller.extend("opensap.movies.controller.App", {
onInit: function () {
Log.info("Controller has been initialized.");
},
onExit: function () {
Log.info("Controller will shortly be destroyed.");
},
onBeforeRendering: function () {
Log.info("The view will shortly be rendered.");
},
onAfterRendering: function () {
Log.info("The view has been rendered.");
},
onPress: function (sValue) {
sap.ui.require(["sap/m/MessageToast"], function (oMessage) {
oMessage.show("Searching..." + sValue);
} );
}
});
});
Route to View on Event
Specify routes in manifest.json, here for the Detail view:
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"viewType": "XML",
"async": true,
"viewPath": "opensap.movies.view",
"controlAggregation": "pages",
"controlId": "app",
"clearControlAggregation": false
},
"routes": [{
"name": "Home",
"pattern": "",
"target": ["Home"]
}, {
"name": "Detail",
"pattern": "movies/{movieId}/appointments/{appointmentId}",
"titleTarget": "",
"greedy": false,
"target": ["Detail"]
}],
Specify event and eventhandler in view definition, here event appointmentSelect:
<PlanningCalendar id="calendar" startDate="{path: 'movies>/initDate', formatter: '.formatter.formatDate'}" rows="{movies>/movies}"
appointmentsVisualization="Filled" appointmentSelect=".onAppointmentSelect(${$parameters>/appointment})">
<toolbarContent>
...
</toolbarContent>
<rows>
<PlanningCalendarRow title="{movies>name}" text="{movies>genre}" appointments="{path: 'movies>appointments', templateShareable: 'true'}">
<appointments>
...
</appointments>
</PlanningCalendarRow>
</rows>
</PlanningCalendar>
Full listing: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/view/App.view.xml
Add UIComponent to controller to access the app router:
sap.ui.define([
...,
"sap/ui/core/UIComponent"
], function (Controller, Log, formatter, Filter, FilterOperator, UIComponent) {
"use strict";
return Controller.extend("opensap.movies.controller.App", {
...
onAppointmentSelect: function (oAppointment) {
var oContext = oAppointment.getBindingContext("movies"),
sPath = oContext.getPath();
var aParameters = sPath.split("/");
UIComponent.getRouterFor(this).navTo("Detail", {
movieId: aParameters[2],
appointmentId: aParameters[4]
});
}
});
});
Full listing: https://github.com/mattxdev/opensap-ui52-movies/blob/master/webapp/controller/App.controller.js
Now the Detail.controller.js of the Detail view has to be enabled to respond to the new route pattern:
onInit: function () {
UIComponent.getRouterFor(this).getRoute("Detail").attachPatternMatched(this._onDetailMatched, this);
},
_onDetailMatched: function (oEvent) {
var oView = this.getView(),
sMovieIndex = oEvent.getParameter("arguments")["movieId"],
sAppointmentIndex = oEvent.getParameter("arguments")["appointmentId"];
oView.bindElement({
path: "/movies/" + sMovieIndex + "/appointments/" + sAppointmentIndex,
model: "movies",
events: {
change: this._onBindingChange.bind(this)
}
});
},
_onBindingChange: function () {
var oView = this.getView(),
oElementBinding = oView.getElementBinding("movies"),
sPath = oElementBinding.getPath();
// if the path to the data does not exist we navigate to the not found page
if (!oView.getModel("movies").getObject(sPath)) {
//See Challenge at the end: UIComponent.getRouterFor(this).getTargets().display("NotFound");
}
}
onDetailsChanged binds the view to the model using the path of the appointment the user clicked on. This will display automatically the chosen appointment details on the Detail view.
onBindingChange transfers the binding contect to the Detail view.
Implement NotFound Page
Documentation: https://ui5.sap.com/#/topic/e047e0596e8a4a1db50f4a53c11f4276
Implementation: https://github.com/mattxdev/opensap-ui52-movies/commit/6225043dbe114bf217f570d43db0a1ee395a0013
Implement Feature-Rich Controls
Feature-Rich controls implemented in exercise:
Control | Library | Documentation |
---|---|---|
Sticky table header | ||
Floating footer | ||
Object Page Layout | sap.uxap.ObjectPageLayout | |
Info Label | sap.tnt.InfoLabel | |
Object Number | sap.m.ObjectNumber | |
Status Indicator | sap.suite.ui.commons.statusindicator.StatusIndicator |
Exercise from openSAP course UI52: Week 3 Unit 4: Spicing up Your Scenario with Feature-Rich Controls
Implementation: https://github.com/mattxdev/opensap-ui52-orders/commit/6f8c9aab0a24c735c4309ae0217e4f2e87f055ac
Reusing Patterns
Reuse Patterns:
- Control
- Fragment
- View
- Component
- Library
Control Pattern:
- Encapsulates Rendering and Behavior
- Standardized Control Interface
- Smallest Reusable UI Asset
- Custom or Composite Control
- Bundled in Control Libraries
XML Composite Control
- Declarative UI definition in XML
- Special $this model for binding properties
- Control metadata description
Create XML composite control:
- Create new folder "control" inside "webapp"
- Create .js file for controller and metadata
- Create .xml file for layout
Exercise from openSAP course UI52: Week 3 Unit 5: Scaling Up with UI Reuse patterns
Implementation: https://github.com/mattxdev/opensap-ui52-orders/commit/3bc3a2ed14f0d4686abec6511063cdb633a145d0
Add CRUD Form to Flexible Column Layout
Exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input
Form will be called by add-Button ("+") in Detail View. Implementation:
<Toolbar>
<Title id="lineItemsTitle" text="{detailView>/lineItemListTitle}" titleStyle="H3" level="H3"/>
<ToolbarSpacer />
<Button icon="sap-icon://add" tooltip="{i18n>createButtonTooltip}" press=".onCreate"/>
</Toolbar>
Create a new XML View and controller. Implementations:
- https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Create.view.xml
- https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js
Add create target and create route to the manifest.json:
"routes": [
{
"pattern": "SalesOrderSet/{objectId}/create",
"name": "create",
"target": [
"master",
"object",
"create"
]
}
],
"targets": {
"create": {
"viewType": "XML",
"viewName": "Create",
"controlAggregation": "endColumnPages",
"viewId": "create"
}
}
Full implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/manifest.json
Information about Routing and Navigation: https://ui5.sap.com/#/topic/3d18f20bd2294228acb6910d8e8a5fb5
Extend existing Detail.controller.js:
https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Detail.controller.js
Add Input Validation Client and Server Side
Add value property to the input fields:
- ProductID
- Note
- Quantity
Set path and type of each value property to the corresponding property from the oData model. This way the app uses backend validation and ensures that the oData properties will receive correct entries regarding their type.
Use contraints to ensure input of correct values at the client side. In the following implementation this happens with regular expressions. Also set minimum value for quantity.
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Create.view.xml
Define defaultBindingMode to TwoWay in order to be able to handle the transport of data both from the model to the controls and back from the controls to the model.
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input
Add MessageManager for Displaying Messages
In onInit a global message manager will be registered and a special message model will be established to redtieve and display error messages.
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input
Documentation:
- https://ui5.sap.com/#/entity/sap.ui.core.message.MessageManager
- https://ui5.sap.com/#/topic/b8c4e534cdb440e9a5bbff86f9572bd6
Message Popover for Logging Errors
In general it is a good idea in terms of user experience to store all errors in one place. This helps the user to find and track errors. sap.m.MessagePopover is a good choice for this.
Add a button to the overflow toolbar for showing the message popover.
Initialize the MessageManager in onInit. Register the root element ObjectPageLayout of the view as an object for which errors will be tracked and displayed in the popover.
Ensure to remove all server-side erroors by removing all messages from the MessageManager _onCreateMatched.
Als remove old messages when the product name is changed and is about to be validated. This is implemented at the change event of the correspodings input field in the view - here implemented in the onNameChange function.
In the handler of the popover button onOpenMessages open the MessagePopover.
Implementations:
- https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Create.view.xml
- https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input
Creating Entries with OData
First, when the route is matched and the view is created an entry is created locally in _onCreateMatched by utilizing the createEntry Method, specifying a collection, in our case SalesOrderLineItemSet. Also initial values to the mandatory fields from the model are added. As a result of the createEntry function you receife a contect that points to the new object. It must be set to the view.
_onCreateMatched: function (oEvent) {
var sObjectId = oEvent.getParameter("arguments").objectId;
// create a binding context for a new order item
this.oContext = this.getModel().createEntry("/SalesOrderLineItemSet", {
properties: {
SalesOrderID: sObjectId,
ProductID: "",
Note: "",
Quantity: "1",
DeliveryDate: new Date()
},
success: this._onCreateSuccess.bind(this)
});
this.getView().setBindingContext(this.oContext);
...
},
_onCreateSuccess: function (oContext) {
// show success message
var sMessage = this.getResourceBundle().getText("newItemCreated", [oContext.ProductID]);
MessageToast.show(sMessage, {
closeOnBrowserNavigation : false
});
// navigate to the new item in display mode
this.getRouter().navTo("Info", {
objectId : oContext.SalesOrderID,
itemPosition : oContext.ItemPosition
}, true);
},
onCreate: function () {
// send new item to server for processing
this.getModel().submitChanges();
},
In the onCreate handler the submit function of the OData model will be called. It creates a HTTP request against the server. The server completes the creatte action and sends a HTTP response back.
Register to the success event afterwards via the _onCreateSuccess callback function.
Afther the _onCreateSuccess function is triggered, the view will be unbound from the created object. A MessageToast shows a success message to the user. Lastly the As a last step, navigate to the created item.
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Create.controller.js
Documentation: https://ui5.sap.com/#/topic/6c47b2b39db9404582994070ec3d57a2.html
See also exercise from openSAP course UI52: Week 4 Unit 1: Creating Items and Validating User Input
Enable Drag and Drop in Lists
The drag and drop feature will be enabled with the dragDropConfig aggregation and the DragInfo or DropInfo element inside of it, depending of if you want it to be a source or a target for drag and drop.
Add dragDropConfig aggregation and define the drag source by adding DragInfo inside of it with the property sourceAggregation:
<dragDropConfig>
<dnd:DragInfo sourceAggregation="items"/>
</dragDropConfig>
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/view/Detail.view.xml
Create a new custom button and extend its metadata:
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/control/DeleteButton.js
Add XML code to enable drop feature to the custom button:
<control:DeleteButton
icon="sap-icon://delete">
<control:dragDropConfig>
<dnd:DropInfo drop=".onDelete" />
</control:dragDropConfig>
</control:DeleteButton>
Implementation of the drop/press handler in Detail.controller.js:
onDelete : function (oEvent) {
// delete the dragged item
var oItemToDelete = oEvent.getParameter("draggedControl");
// delete the selected item from the list - if nothing selected, remove the first item
if (!oItemToDelete) {
var oList = this.byId("lineItemsList");
oItemToDelete = oList.getSelectedItem() || oList.getItems()[0];
}
// delete the item after user confirmation
var sPath = oItemToDelete.getBindingContextPath(),
sTitle = oItemToDelete.getBindingContext().getProperty("ProductID");
this._confirmDelete(sPath, sTitle);
}
In order to provide a better user experience the user should be asked for confirmation before an item is deleted. this will be done with the _confirmDelete function.
Implementation: https://github.com/mattxdev/opensap-ui52-orders/blob/master/webapp/controller/Detail.controller.js
See also exercise from openSAP course UI52: Week 4 Unit 2 - Exercise: Improving User Experience with Drag and Drop
Documentation:
- Drag and Drop Documentation: https://ui5.sap.com/#/topic/3ddb6cde6a8d416598ac8ced3f5d82d5
- API reference: https://ui5.sap.com/#/api/sap.ui.core.dnd
- Drag-and-Drop enabled for all UI5 controls: https://blogs.sap.com/2018/07/19/drag-and-drop-is-now-enabled-for-all-ui5-controls/
Debugging Tools
Open diagnostics tool: Ctrl
+ Shift
+ Alt
+ S
Use debug sources: Ctrl
+ Shift
+ Alt
+ P
After that activate the "Use Debug Sources" checkbox. When reloading in the Chrome DevTools files will be loaded with the "-dbg" suffix. These are source code files that include comments and uncompressed code of the OpenUI5 artifacts.
This can be also achieved by adding the parameter to the url: sap-ui-debug=true