Developing SAP Mobile App using Sencha Touch with OData service
In this blog, I would like to share my experience on how to develop a simple SAP mobile app using Sencha Touch with view, update, create and delete operations.
The objective of this app is to be able to view, update, create and delete a record in TravelagencyCollection-RMTSAMPLEFLIGHT that is published by OData service SAP Netweaver Gateway Service Consumption System.
Required Components
We would need the following components in order to build the app:
- Sencha Touch framework from http://www.sencha.com
Download this framework. We will write the code using this framework. - Sencha Touch OData Connector for SAP
Download the connector from https://market.sencha.com/extensions/sencha-touch-odata-connector-for-sap to connect to SAP OData available services. File we need is OData.js. - Apache
We will host the code on Apache web server and we will configure the reverse proxy to resolve the “same-origin policy” issue.
Download Apache with SSL enabled from http://httpd.apache.org/download.cgi - Get an account from SAP Netweaver Gateway Demo system
Register an account from https://supsignformssapicl.hana.ondemand.com/SUPSignForms
We need this account to be able to use the sample service. In this case is RMTSAMPLEFLIGHT.
Configure Reverse Proxy
The first step that you need to do after installing Apache web-server is to configure reverse proxy. I am not going into detail on how to install the Apache web server. In my configuration, I am using the Apache with SSL enabled.
Open your httpd.conf under Apache conf folder and uncomment the following lines:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule headers_module modules/mod_headers.so
LoadModule ssl_module modules/mod_ssl.so
Also add the following lines:
Header set Access-Control-Allow-Origin "*"
Header add Access-Control-Allow-Headers "Content-Type"
Header add Access-Control-Allow-Headers "X-Requested-With"
SSLProxyEngine "On"
RequestHeader set Front-End-Https "On"
ProxyPass /sapgw/ https://sapes1.sapdevcenter.com/
ProxyPassReverse /sapgw/ https://sapes1.sapdevcenter.com
In the URL connection string, we need to add the /sapgw/ to enable the reverse proxy, for example:
http://ASPSGPLR81BC0G/sapgw/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/TravelagencyCollection
Replace ASPSGPLR81BC0G with your computer name.
Any request to http://ASPSGPLR81BC0G/sapgw, will be directed by Apache to https://sapes1.sapdevcenter.com/
Let’s open this link from Chrome browser and you will get the result:
Page Structure
We will build three simple pages as follow:
- Travel Agency List.
On this page, user can view the list of travel agency with a button link to the Detail page.
- Detail.
On the Detail page, user can update or delete the record. They cannot update the Agency No field.
- Add Travel Agency.
On this page, user can add the new record. The Agency No is a required field.
Code Structure
As you can see in the below structure, we will build the MVC structure. You need to copy the OData.js (from Sencha Touch OData Connector from SAP) , sencha-touch-all.js and resources (from Sencha Touch) to the root folder, TravelAgency.
Let’s walk through the important files,
- View: app.js
We register the controller, model, store and views in app.js.controllers: ["TravelAgency"], models: ["TravelAgency"], stores: ["TravelAgency"], views: ["Viewport", "MainPanel", "TravelAgencyListContainer", "TravelAgencyEditor" "AddTravelAgentContainer", "AddTravelAgentMenu" ],
Page Corresponding View Travel Agency List TravelAgencyList Detail Page TravelAgencyEditor Add Travel Agency AddTravelAgentMenu
- Model and Store: TravelAgency.js
In the model and store TravelAgency.js, we define all the required fields and URL connection in the proxy.fields: [ { name: "agencynum" }, { name: "NAME" }, { name: "STREET" }, { name: "POSTBOX" }, { name: "POSTCODE" }, { name: "CITY" }, { name: "COUNTRY" }, { name: "REGION" }, { name: "TELEPHONE" }, { name: "URL" }, { name: "LANGU" }, { name: "CURRENCY" }, { name: "mimeType" } ], proxy: { type: 'odata', enablePagingParams: false, withCredentials: true, username: 'P1940517116', password: 'Password', url: "//ASPSGPLR81BC0G/sapgw/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/TravelagencyCollection", }
- View: TravelAgencyList.js
To display out the Agency No (agencynum) and Agency Name (NAME) based on the data obtained from store TravelAgency and put a button link to the Detail page, onItemDisclosure
config: { store: "TravelAgency", itemTpl: [ '<div>', '<div>Agency No: {agencynum}</div>', '<div>{NAME}</div>', '</div>', ], onItemDisclosure: function(record,btn,index) { this.fireEvent("editTravelAgencyCommand", record); } },
- View: TravelAgencyEditor.js
It will display Name, Street, Post Box, Post Code, City, Country, Region, Telephone, URL, Language and Currency based on the given agencynum. We also put the Save (onSaveTap) and Delete (onDeleteTap) functions on this view.items: [ { name: "NAME", label: "Name" }, { name: "STREET", label: "Street" }, { name: "POSTBOX", label: "Post Box" }, { name: "POSTCODE", label: "Post Code" }, { name: "CITY", label: "City" }, { name: "COUNTRY", label: "Country" }, { name: "REGION", label: "Region" }, { name: "TELEPHONE", label: "Telephone" }, { name: "URL", label: "URL" }, { name: "LANGU", label: "Language" }, { name: "CURRENCY", label: "Currency" } ], onSaveTap: function(button,e,options) { this.fireEvent("saveCommand", this.getRecord(), button); }, onDeleteTap: function(button,e,options) { this.fireEvent("deleteCommand", this.getRecord(), button); ),
-
View: AddTravelAgentMenu.js
We populate the text fields for user to enter the Agency No, Name, Street, Post Box, Post Code, City, Country, Region, Telephone, Language, Currency, URL and add the Add (onFormAdd) function.
items: [ { xtype: 'textfield', label: 'Agency No', labelWrap: true, labelWidth: '30%', name: 'agencynum', placeHolder: 'Enter Agency No', }, { xtype: 'textfield', label: 'Name', labelWrap: true, labelWidth: '30%', name: 'NAME', placeHolder: 'Enter Name' }, { xtype: 'textfield', label: 'Street', labelWidth: '30%', name: 'STREET', placeHolder: 'Enter Street' }, { xtype: 'textfield', label: 'Post Box', labelWidth: '30%', name: 'POSTBOX', placeHolder: 'Enter Post Box' }, { xtype: 'textfield', label: 'Post Code', labelWidth: '30%', name: 'POSTCODE', placeHolder: 'Enter Post Code' }, { xtype: 'textfield', label: 'City', labelWidth: '30%', name: 'CITY', placeHolder: 'Enter City' }, { xtype: 'textfield', label: 'Country', labelWidth: '30%', name: 'COUNTRY', placeHolder: 'Enter Country' }, { xtype: 'textfield', label: 'Region', labelWidth: '30%', name: 'REGION', placeHolder: 'Enter Region' }, { xtype: 'textfield', label: 'Telephone', labelWidth: '30%', name: 'TELEPHONE', placeHolder: 'Enter Telephone' }, { xtype: 'textfield', label: 'Language', labelWidth: '30%', name: 'LANGU', placeHolder: 'Enter Language' }, { xtype: 'textfield', label: 'Currency', labelWidth: '30%', name: 'CURRENCY', placeHolder: 'Enter Currency' }, { xtype: 'textfield', label: 'URL', labelWidth: '30%', name: 'URL', placeHolder: 'Enter URL' }, ], onFormAdd: function(button,e,options){ var formObj = button.up('addtravelagentmenu'); var formData = formObj.getValues(); this.fireEvent("addCommand", this.getRecord(), button); }
- Controller: TravelAgency.js
On controller, we define the method for save, delete and update record.Update Method
We need to specify an id field in order to update the record based on agencynum.onSaveButton: function(record, button) { travel = Ext.create('TravelAgency.model.TravelAgency', { id : "//ASPSGPLR81BC0G/sapgw/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/TravelagencyCollection('" + agencynum: record.get('agencynum'), }); var formObj = button.up('travelagencyeditor'); var formData = formObj.getValues(); travel.set('agencynum', formData.agencynum); travel.set('NAME', formData.NAME); travel.set('STREET', formData.STREET); travel.set('POSTBOX', formData.POSTBOX); travel.set('POSTCODE', formData.POSTCODE); travel.set('CITY', formData.CITY); travel.set('COUNTRY', formData.COUNTRY); travel.set('REGION', formData.REGION); travel.set('TELEPHONE', formData.TELEPHONE); travel.set('URL', formData.URL); travel.set('LANGU', formData.LANGU); travel.set('CURRENCY', formData.CURRENCY); travel.save(function (record, operation) { console.log(record); console.log(operation); if (operation.getError()==null) { console.log('record udated. Id:' + record.get('agencynum')); Ext.Msg.alert('Status', "Record updated"); } else { console.log('Create record failed'); Ext.Msg.alert('Status', operation.getError()); } var store = Ext.getStore("TravelAgency"); store.loadPage(1); }); },
Delete Method
Similar to Update method, we need to specify an ID (agencynum) to delete the record.onDeleteButton: function(record, button) { mp = this.getMainPanel(); travel = Ext.create('TravelAgency.model.TravelAgency', { id : "//ASPSGPLR81BC0G/sapgw/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT/TravelagencyCollection('" + record.get('agencynum') + "')", agencynum: record.get('agencynum'), }); var formObj = button.up('travelagencyeditor'); var formData = formObj.getValues(); travel.erase(function (record, operation) { console.log(record); console.log(operation); if (operation.getError()==null) { console.log('record deleted'); Ext.Msg.alert('Status', 'Record deleted successfully.'); } else { console.log('Delete record failed'); Ext.Msg.alert('Status', 'Delete record failed.'); } var store = Ext.getStore("TravelAgency"); store.loadPage(1); Ext.Viewport.animateActiveItem(mp, { type: "slide", direction: "left" }); }); }
Create Method
For Create method, we need to ensure the agencynum is not empty.onAddButton: function(record, button) { var formObj = button.up('addtravelagentmenu'); var formData = formObj.getValues(); travel = Ext.create('TravelAgency.model.TravelAgency', { agencynum: formData.agencynum, NAME: formData.NAME, STREET: formData.STREET, POSTBOX: formData.POSTBOX, POSTCODE: formData.POSTCODE, CITY: formData.CITY, COUNTRY: formData.COUNTRY, REGION: formData.REGION, TELEPHONE: formData.TELEPHONE, URL: formData.URL, LANGU: formData.LANGU, CURRENCY: formData.CURRENCY }); if (formData.agencynum == "") { Ext.Msg.alert('ERROR', 'Agency No could not be empty'); } else { travel.set('agencynum', formData.agencynum); travel.set('NAME', formData.NAME); travel.set('STREET', formData.STREET); travel.set('POSTBOX', formData.POSTBOX); travel.set('POSTCODE', formData.POSTCODE); travel.set('CITY', formData.CITY); travel.set('COUNTRY', formData.COUNTRY); travel.set('REGION', formData.REGION); travel.set('TELEPHONE', formData.TELEPHONE); travel.set('URL', formData.URL); travel.set('LANGU', formData.LANGU); travel.set('CURRENCY', formData.CURRENCY); travel.save(function (record, operation) { console.log(record); console.log(operation); if (operation.wasSuccessful()) { Ext.Msg.alert('Status', 'Record added successfully. Record ID: ' + record.get('agencynum')); console.log('record created. Id:' + record.get('agencynum')); formObj.setValues({ agencynum: '', NAME: '', STREET: '', POSTBOX: '', POSTCODE: '', CITY: '', COUNTRY: '', REGION: '', TELEPHONE: '', URL: '', LANGU: '', CURRENCY: '' }); } else { console.log('Create record failed'); } var store = Ext.getStore("TravelAgency"); store.loadPage(1); }); } },
After you complete writing all the codes, you can launch the app from Chrome browse and it will give you the Travel Agency List page:
http://<your-apache-web-server>/TravelAgency/index.html
Some screenshots of edit, update, delete operations in action:
The complete source code can be found in GitHub: https://github.com/ferrygun/TravelAgency
Thanks for reading my blog, feel free to drop any comment/question.
Hi Ferry,
Apache
We will host the code on Apache web server and we will configure the reverse proxy to resolve the "same-origin policy" issue.
Download Apache with SSL enabled from http://httpd.apache.org/download.cgi
By above point for Apache configuration what does mean "Apache with SSL Enabled" and also provided does not contain such type of source. Can you please elaborate some deeeep.
Regards,
Ranjith
My SAP Netweaver Gateway is in https mode, so that's why I needed Apache with SSL-enabled. In the httpd.conf, I need to uncomment "LoadModule ssl_module modules/mod_ssl.so" and add "SSLProxyEngine On". Hope it helps.
Hi Ferry,
I am not able to download the Sencha Touch OData Connector for SAP (https://market.sencha.com/extensions/sencha-touch-odata-connector-for-sap). Once I login, I get an error message...
We're sorry, but something went wrong.
Is there another link where I can download this OData Connector ?
Confirmed, I get the same problem. Also created new account at Sencha.
Has anyone tried consuming deep entity Gateway service in Sencha Touch for POST/PUT methods?
If so kindly share the code..
regards
Nitesh
Hi Nitesh,
Please create a new Discussion marked as a Question. The Comments section of a Blog (or Document) is not the right vehicle for asking questions as the results are not easily searchable. Once your issue is solved, a Discussion with the solution (and marked with Correct Answer) makes the results visible to others experiencing a similar problem. If a blog or document is related, put in a link. Read the Getting Started documents (link at the top right) including the Rules of Engagement.
NOTE: Getting the link is easy enough for both the author and Blog. Simply MouseOver the item, Right Click, and select Copy Shortcut. Paste it into your Discussion. You can also click on the url after pasting. Click on the A to expand the options and select T (on the right) to Auto-Title the url.
Thanks, Mike (Moderator)
SAP Technology RIG