Consume oData v2 service as Model
In this example, we will try to consume SAP CAPM oData V4 service as an oData model in a UI5 application and bind it to a Table.
Prerequisite: Ensure that you have installed the OData V2 adapter plugin in your CAP application to expose the OData v2 APIs.
npm i @cap-js-community/odata-v2-adapterWe will follow below steps -
- Create Destination using CAP application details
- Configure App Router for oData
- Define Data Source
- Define Data Model
Step 1 - Create Destination using CAP application details
In our case, the SAP CAP application exposes the following OData service-
https://trial-us10-development-tspace-cap-application-srv.cfapps.us10-004.hana.ondemand.com/odata/v2/custom-service/WarehousesSo, we will create Destination using below details -
| Property | Value |
|---|---|
| Name | --any-name-- (In our case, we are using cap_application_srv) |
| Type | HTTP |
| ProxyType | Internet |
| URL | --your-cap-app-url-- (in our case, its- https://trial-us10-development-tspace-cap-application-srv.cfapps.us10-004.hana.ondemand.com) |
| Authentication | OAuth2UserTokenExchange |
| Client ID | --cap-app-client-id-- |
| Client Secret | --cap-app-client-secret-- |
| Token Service URL | --cap-app-client-id-- (add /oauth/token at the end) |
| verificationKey | --from cap app environment variables-- |
| xsappname | --from cap app environment variables-- |
| sap.cloud.service | --appName.service-- |
Step 2 - Configure App Router for oData
Define the app router for odata requests in xs-app.json file after resources object like-
{
"authenticationType": "xsuaa",
"csrfProtection": false,
"source": "^/odata/v2/custom-service/(.*)",
"target": "/odata/v2/custom-service/$1",
"destination": "cap_application_srv"
}In our case-
- Destination- cap_application_srv
- Service- /odata/v2/custom-service
and we are able to see the metadata using -
https://trial-us10-development-tspace-cap-application-srv.cfapps.us10-004.hana.ondemand.com/odata/v2/custom-service/$metadataStep 3 - Define Data Source
Now, define the odata as Data Source under sap.app section after crossNavigation on manifest.json file like-
"dataSources": {
"WarehouseDataSource": {
"uri": "/odata/v2/custom-service/",
"type": "OData",
"settings": {
"odataVersion": "2.0"
}
}
}In our case, the data source is defined with the name WarehouseDataSource.
Step 4 - Define Data Model
Now use the data source to define a oData model under models section on sap.ui5 section in manifest.json file like-
"WarehouseModel": {
"dataSource": "WarehouseDataSource",
"preload": true,
"settings": {
"synchronizationMode": "None",
"operationMode": "Server",
"autoExpandSelect": true,
"earlyRequests": true
}
}
In our case, the model is defined with the name WarehouseModel.
Its done, now we can use the odata model in your UI5 Application.
CRUD Operation with oData v4 model
Objective-
In this application, we will create a SAP UI5 application that addresses the following use case-
- The first view displays all warehouse details retrieved from the CAP application OData V4 service. The service is consumed as an OData model, which is then bound to a table.
- Users can click any row in the table to navigate to View2, where they can edit and update the selected warehouse record.
- View1 includes an Add New Record button, Clicking this button navigates the user to the NewWarehouse view, where a new warehouse record can be created. Once the record is successfully created, the application automatically redirects the user back to View1.
Implementation -
Use Case - 1
We will displays all warehouse details retrieved from the CAP application OData V4 service. The service is consumed as an OData model, which is then bound to a table on View1.
Since we have already configured the OData V4 service as an OData model named WarehouseModel, we can bind it to the table in View1.view.xml, as shown below -
items="{WarehouseModel>/Warehouses}"So the View1 Table looks like-
<!-- Show Table Details -->
<Table id="idWarehouseTable" items="{WarehouseModel>/Warehouses}" headerText="Warehouse Details">
<headerToolbar>
<OverflowToolbar>
<content>
<Title text="Warehouses" level="H2"/>
<ToolbarSpacer />
</content>
</OverflowToolbar>
</headerToolbar>
<columns>
<Column>
<Text text="ID" />
</Column>
<Column>
<Text text="Registered Name" />
</Column>
<Column>
<Text text="Owner" />
</Column>
<Column>
<Text text="Location" />
</Column>
</columns>
<items>
<ColumnListItem vAlign="Middle">
<cells>
<Text text="{WarehouseModel>ID}" />
<Text text="{WarehouseModel>name}" />
<Text text="{WarehouseModel>owner}" />
<Text text="{WarehouseModel>location}" />
</cells>
</ColumnListItem>
</items>
</Table>Use Case - 2
Users can click any row in the table to navigate to View2, which displays the selected warehouse details. View2 allows users to edit and save the changes to the backend through the OData service, and also provides an option to navigate back to View1.
To make the table rows selectable and invoke the selectRow method when a row is selected, define the attributes on View1 Table as follows-
<Table id="idWarehouseTable" items="{WarehouseModel>/Warehouses}" headerText="Warehouse Details" mode="SingleSelectMaster" selectionChange="selectedRow">Next, define the selectRow method in View1.controller.js to retrieve the selected warehouse record and navigate to View2 using parameterized routing, as shown below-
// Method to get selected Table row details and route to another view.
selectedRow: function (oEvent) {
const selectedID = oEvent.getParameter("listItem").getBindingContext("WarehouseModel").getObject().ID;
debugger
console.log("row pressed");
// Route to View2 with ID on path
const router = UIComponent.getRouterFor(this);
router.navTo("DetailView", {
id: selectedID
});
}Next, create View2.view.xml in view folder and View2.controller.js in controller folder to implement the UI and its corresponding logic. Then, configure the route and target for View2 in manifest.json like-
"routes": [
{
"name": "RouteView1",
"pattern": ":?query:",
"target": [
"TargetView1"
]
},
{
"name" : "DetailView",
"pattern" : "Warehouse/{id}",
"target" : "WarehouseDetails"
}
]
"targets": {
"TargetView1": {
"id": "View1",
"name": "View1"
},
"WarehouseDetails" : {
"id" : "View2",
"name" : "View2"
}
}Next, implement the logic in View2.controller.js to retrieve the selected warehouse ID from the route parameters.
Once the ID is obtained, bind the corresponding warehouse record to the view using element binding. This ensures that the UI automatically displays the details of the selected warehouse, as shown below-
onInit() {
// Extract the details from the Router path.
const oRouter = UIComponent.getRouterFor(this);
oRouter.getRoute("DetailView").attachPatternMatched(this._onObjectMatched, this);
},
// method to Bind the selected model data with UI.
_onObjectMatched(oEvent) {
const id = oEvent.getParameter("arguments").id;
// Bind the view to the selected entity
this.getView().bindElement({
path: "/Warehouses('" + id + "')",
model: "WarehouseModel"
});
MessageToast.show('Details for Warehouse - ' + id);
}And we can then bind the UI with the element in View2 and the navigation back button at page like-
<mvc:View
controllerName="manage.warehouse.controller.View2"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m"
>
<Page id="page2" title="Warehouse Details" showNavButton="true" navButtonPress="onNavBack">
<!-- ************************ Page Header ************************ -->
<ObjectHeader
title="Warehouse ID - {WarehouseModel>ID}"
class="sapUiResponsivePadding--header" >
<statuses>
<ObjectStatus
text="Active"
state="Success" />
</statuses>
</ObjectHeader>
<Panel>
<!-- ************************ Edit / Save / Cancel Buttons ************************ -->
<HBox justifyContent="End">
<Button text="Edit" type="Emphasized" visible="{View2Model>/editVisible}" press="onPressEdit" />
<Button class="sapUiTinyMarginBegin" type="Accept" text="Save" visible="{View2Model>/saveVisible}" press="onPressSave" />
<Button class="sapUiTinyMarginBegin" type="Reject" text="Cancel" visible="{View2Model>/cancelVisible}" press="onPressCancel" />
</HBox>
<!-- ************************ Binding Data with UI ************************ -->
<VBox>
<Label text="Warehouse Name" />
<Input id="warehouseNameInput" value="{WarehouseModel>name}" editable="{View2Model>/inputEditable}" />
<Label text="Owner" />
<Input id="warehouseOwnerInput" value="{WarehouseModel>owner}" editable="{View2Model>/inputEditable}" />
<Label text="Location" />
<Input id="warehouseLocationInput" value="{WarehouseModel>location}" editable="{View2Model>/inputEditable}" />
</VBox>
</Panel>
</Page>
</mvc:View>And since we also defined the Edit, Save and Cancel button, we can then define their logics like-
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/core/UIComponent",
"sap/m/MessageToast"
], (Controller, UIComponent, MessageToast) => {
"use strict";
return Controller.extend("manage.warehouse.controller.View2", {
onInit() {
// Extract the details from the Router path.
const oRouter = UIComponent.getRouterFor(this);
oRouter.getRoute("DetailView").attachPatternMatched(this._onObjectMatched, this);
},
// method to extract the value from route and Bind the selected data with UI.
_onObjectMatched(oEvent) {
const id = oEvent.getParameter("arguments").id;
// Bind the view to the selected entity
this.getView().bindElement({
path: "/Warehouses('" + id + "')",
model: "WarehouseModel"
});
MessageToast.show('Details for Warehouse - ' + id);
},
// Method to Navigate back to View1
onNavBack: async function () {
const oRouter = sap.ui.core.UIComponent.getRouterFor(this);
oRouter.navTo('RouteView1');
},
/*
Below method will do the following -
1. Make the Save and Cancel button visible.
2. Disappear the Edit Button.
3. Make Input field editable.
*/
onPressEdit: function (oEvent) {
this.getView().getModel('View2Model').setProperty('/inputEditable', true);
this.getView().getModel('View2Model').setProperty('/inputEditable', true);
this.getView().getModel('View2Model').setProperty('/inputEditable', true);
this.getView().getModel('View2Model').setProperty('/editVisible', false);
this.getView().getModel('View2Model').setProperty('/saveVisible', true);
this.getView().getModel('View2Model').setProperty('/cancelVisible', true);
MessageToast.show('Edit Enabled');
},
/*
Below method will do the following -
1. Make the Save and Cancel button disappear.
2. Visible the Edit Button.
3. Make Input field non-editable.
*/
onPressCancel: function (oEvent) {
this.getView().getModel('View2Model').setProperty('/inputEditable', false);
this.getView().getModel('View2Model').setProperty('/inputEditable', false);
this.getView().getModel('View2Model').setProperty('/inputEditable', false);
this.getView().getModel('View2Model').setProperty('/editVisible', true);
this.getView().getModel('View2Model').setProperty('/saveVisible', false);
this.getView().getModel('View2Model').setProperty('/cancelVisible', false);
MessageToast.show('No changes Saved');
},
/*
This method will do the following -
1. Get the data of input fields.
2. Update the data on the backend.
3. Make Input field editable.
4. Make the Save and Cancel button disappear.
5. Visible the Edit Button.
6. Make Input field non-editable.
*/
onPressSave: async function (oEvent) {
const name = this.getView().byId('warehouseNameInput').getValue();
const owner = this.getView().byId('warehouseOwnerInput').getValue();
const location = this.getView().byId('warehouseLocationInput').getValue();
// Update the data on the backend using the OData V2 service model.
const oModel = this.getView().getModel("WarehouseModel");
// Get the binding context, which represents the currently bound entity (specific record).
const oContext = this.getView().getBindingContext("WarehouseModel");
// Get the path of the bound entity (e.g. /Warehouses('82034810943')).
const sPath = oContext.getPath();
const oPayload = {
name: name,
owner: owner,
location: location
};
oModel.update(sPath, oPayload, {
success: function () {
MessageToast.show("Warehouse updated successfully");
},
error: function () {
MessageBox.error("Update failed");
}
});
this.getView().getModel('View2Model').setProperty('/inputEditable', false);
this.getView().getModel('View2Model').setProperty('/inputEditable', false);
this.getView().getModel('View2Model').setProperty('/inputEditable', false);
this.getView().getModel('View2Model').setProperty('/editVisible', true);
this.getView().getModel('View2Model').setProperty('/saveVisible', false);
this.getView().getModel('View2Model').setProperty('/cancelVisible', false);
MessageToast.show('Successfully updated the details');
}
});
});When the user navigates back to View1 after updating a warehouse record, the latest changes may not be reflected in the table automatically. To ensure the table displays the updated data, refresh the table's binding whenever the View1 route is matched, as shown below-
onInit() {
const oRouter = UIComponent.getRouterFor(this);
oRouter.getRoute("RouteView1").attachPatternMatched(this._onPatternMatched, this);
},
// Method to update/refresh the binding data with Table as well if User updated the Table row details.
_onPatternMatched: function () {
const oBinding = this.byId("idWarehouseTable").getBinding("items");
oBinding.refresh();
},Use Case - 3
View1 includes an Add New Record button, Clicking this button navigates the user to the NewWarehouse view, where a new warehouse record can be created. Once the record is successfully created, the application automatically redirects the user back to View1.
Define the Button on View1.view.xml to create new Warehouse Record clicking on which will navigate to NewWarehouse View Page.
<!-- ********** Create new Warehouse Entry ********** -->
<HBox justifyContent="End">
<Button text="New" type="Emphasized" icon="sap-icon://add" press="onPressCreate" />
</HBox>
and its logic like-
onPressCreate : function(){
const router = UIComponent.getRouterFor(this);
router.navTo("NewWarehouseView");
}Now, create NewWarehouse.view.xml file in view folder and NewWarehouse.controller.js file in controller folder and define the Route and Target on manifest.json file like-
"routes": [
{
"name": "RouteView1",
"pattern": ":?query:",
"target": [
"TargetView1"
]
},
{
"name" : "NewWarehouseView",
"pattern" : "NewWarehouse",
"target" : "NewWarehouseTarget"
},
{
"name" : "DetailView",
"pattern" : "Warehouse/{id}",
"target" : "WarehouseDetails"
}
]And its target like-
"targets": {
"TargetView1": {
"id": "View1",
"name": "View1"
},
"NewWarehouseTarget" : {
"id": "NewWarehouse",
"name" : "NewWarehouse"
},
"WarehouseDetails" : {
"id" : "View2",
"name" : "View2"
}
}We can define the Input fields in NewWarehouse View like -
<Label text="Name" />
<Input id="name" placeholder="Warehouse Name" />
<Label text="Owner" />
<Input id="owner" placeholder="Warehouse Owner" />
<Label text="Location" />
<Input id="location" placeholder="Warehouse Location" />
<Label text="" />
<Button text="Register" type="Emphasized" press="onRegister" />
<Label text="" />
<Button text="Cancel" type="Reject" press="onCancel" />And will define its logic in NewWarehouse.controller.js file like -
// Cancel Button logic will Navigate back to View1
onCancel: function () {
const router = UIComponent.getRouterFor(this);
router.navTo("RouteView1");
},
/*
Below method will do the following -
1. Get the data of Input Fields.
2. Validate the data.
3. Get the oData Model details and its binding.
4. Push the data to db using the odata Model.
5. Set the Input fields to empty.
6. Navigate back to View1.
*/
onRegister: async function () {
const wName = this.getView().byId("name").getValue();
const wOwner = this.getView().byId("owner").getValue();
const wLocation = this.getView().byId("location").getValue();
if (wName.length == 0 || wOwner.length == 0 || wLocation.length == 0) {
MessageToast.show("Please fill all the details.");
return;
}
const oModel = this.getView().getModel("WarehouseModel");
const oPayload = {
name: this.byId("name").getValue(),
owner: this.byId("owner").getValue(),
location: this.byId("location").getValue()
};
try {
oModel.create("/Warehouses", oPayload, {
success: function (oData) {
MessageToast.show("Warehouse created successfully");
},
error: function (oError) {
MessageBox.error("Failed to create warehouse");
}
});
this.getView().byId("name").setValue("");
this.getView().byId("owner").setValue("");
this.getView().byId("location").setValue("");
// Navigate back to View1
this.getOwnerComponent().getRouter().navTo("RouteView1");
} catch (err) {
sap.m.MessageBox.error("Failed to create warehouse");
console.error(err);
}
}!!! Its Done !!!