Building App
Building App
Building App
The documentation may have changed since you downloaded the PDF. You can always find the latest information on SAP Help
Portal.
Note
This PDF document contains the selected topic and its subtopics (max. 150) in the selected structure. Subtopics from other structures are not included.
2016 SAP SE or an SAP affiliate company. All rights reserved. No part of this publication may be reproduced or transmitted in any form or for any purpose
without the express permission of SAP SE. The information contained herein may be changed without prior notice. Some software products marketed by SAP
SE and its distributors contain proprietary software components of other software vendors. National product specifications may vary. These materials are
provided by SAP SE and its affiliated companies ("SAP Group") for informational purposes only, without representation or warranty of any kind, and SAP
Group shall not be liable for errors or omissions with respect to the materials. The only warranties for SAP Group products and services are those that are set
forth in the express warranty statements accompanying such products and services, if any. Nothing herein should be construed as constituting an additional
warranty. SAP and other SAP products and services mentioned herein as well as their respective logos are trademarks or registered trademarks of SAP SE in
Germany and other countries. Please see www.sap.com/corporate-en/legal/copyright/index.epx#trademark for additional trademark information and notices.
Table of content
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 1 of 36
Table of content
1 Building
1.1 Step 1: Index
1.2 Step 2: Component
1.3 Step 3: Navigation and Routing
1.4 Step 4: Internationalization
1.5 Step 5: Device Model
1.6 Step 6: Custom Utilities
1.7 Step 7: Model View Controller
1.8 Step 8: Master View
1.9 Step 9: Detail View
1.10 Step 10: Detail XML Fragments
1.11 Step 11: AddProduct View
1.12 Step 12: NotFound View
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 2 of 36
1 Building
We'll take a step-by-step approach and build up the app over a series of small progressions. We'll start out at nothing, and end up with a fully functioning app.
In the previous part "Preparing", we examined the general characteristics and specific aspects of an enterprise scale app. We also looked into design patterns,
and the specific business scenario that we're going to cover. In this part, we're going to build the app.
The app is made up of a number of moving parts, which are shown in this diagram:
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 3 of 36
Let's have a quick tour of how the app is put together, visiting each file in turn.
Other Files
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 4 of 36
The Formatter.js file contains formatting utilities that are used in various controls within the Master and Detail views.
The css folder contains style.css which has a very small amount of custom CSS, specifically to position the "not found" message further down that it
would normally sit.
css/
|
+-- style.css
i18n/
|
+-- messageBundle.properties
util/
|
+-- Formatter.js
view/
|
+-+-+-+-+-+-+-+-+-+-+--
AddProduct.controller.js
AddProduct.view.xml
App.view.xml
CategoryInfoForm.fragment.xml
Detail.controller.js
Detail.view.xml
Master.controller.js
Master.view.xml
NameRequiredDialog.fragment.xml
NotFound.view.xml
SupplierAddressForm.fragment.xml
Component.js
index.html
MyRouter.js
So now we have an idea of what our app should consist of, let's start building it.
Step 1: Index
Because an application would normally live in a folder, and the default file that web servers return when the folder itself is requested is often configured to be
index.html, the name of our app's index HTML file is indeed index.html.
Step 2: Component
Step 3: Navigation and Routing
A new Routing mechanism was introduced to SAPUI5 in release 1.16. For in-app navigation, this supersedes previous techniques such as using the
sap.ui.core.EventBus or sharing navigation-container specific controller code amongst aggregated pages.
Step 4: Internationalization
For localization, or internationalization (i18n), use data binding and the sap.ui.model.resource.ResourceModel as a named model. This allows
easily using translation keys instead of the actual texts. The alternative, using ResourceBundles , requires code execution for every translated text set on
a control.
Step 5: Device Model
Before we get to the App view, there are a couple of other areas to cover. The first is the Device Model that we saw briefly in the Component section.
Step 6: Custom Utilities
When developing an application there is always the need to reuse JavaScript coding in multiple views and controllers. This is achieved by storing this code
in separate files in the util folder. The SAPUI5 modularization concept is used to dynamically load this code at runtime.
Step 7: Model View Controller
We're using the Model View Controller (MVC) concept, and this starts with a root View that the Component instantiates - the App view.
Step 8: Master View
Flushed with the success of seeing our SplitApp container on the screen, we'll add the first main view, Master, replacing the empty skeleton we used in the
Model View Controller section.
Step 9: Detail View
We'll now add the Detail view, replacing the skeleton placeholder we had up until now.
Step 10: Detail XML Fragments
In the Detail View section, we referenced XML fragments in the sap.m.IconTabFilters. These fragments are separately maintainable and reusable
collections of control declarations.
Step 11: AddProduct View
Let's round out the core functionality of the app by bringing in the facility to add a new product.
Step 12: NotFound View
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 5 of 36
Minimal Content
The heart of an application is defined in its Component part, which means that we should aim for very minimal content in the index.html file itself. Like this:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="UTF-8">
<title>TDG Demo App</title>
<script id="sap-ui-bootstrap"
src="/path/to/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m"
data-sap-ui-theme="sap_bluecrystal"
data-sap-ui-xx-bindingSyntax="complex"
data-sap-ui-resourceroots='{
"sap.ui.demo.tdg": "./"
}'>
</script>
<script>
sap.ui.getCore().attachInit(function() {
new sap.m.Shell({
app: new sap.ui.core.ComponentContainer({
height : "100%",
name : "sap.ui.demo.tdg"
})
}).placeAt("content");
});
</script>
</head>
<body class="sapUiBody" id="content" />
</html>
We have a header and the standard variant SAPUI5 bootstrap, the application startup, and a particular focus on resources and namespaces.
Note
While we're here, notice the way that the namespace and folder location are specified in a JSON structure inside the value of an HTML tag's attribute.
You'll come across this syntax again when we see the use of complex binding syntax in declarative (XML) views, where paths, types and formatters are
specified for property bindings.
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 6 of 36
When the component is specified with the name property of the sap.ui.core.ComponentContainer, it is by means of the "sap.ui.demo.tdg" namespace.
By default, the container will look for a component called "Component" in this namespace, which thus translates to sap.ui.demo.tdg.Component. This
will be found in the file Component.js in the same folder, because we've specified this same folder as being where resources in the sap.ui.demo.tdg
namespace are to be found.
Further Notes
Specific applications may require additional script tags to load external files or some HTML structure within the body or a script tag before loading SAPUI5 to
configure the SAPUI5 runtime. This suggestion here does not say such extensions are not allowed.
Progress Check
This is the first step, so we're not expecting much. All we have is the index.html file.
tdg/
|
+-- index.html
But it's the root container for our whole application, so at least we can try to call it up in our browser. This is what we get.
Ok. Not a lot. But not an entirely blank screen - we can see already that the styling is from the Blue Crystal theme. Most importantly, we see the significant
error in the developer console:
Uncaught Error: failed to load 'sap/ui/demo/tdg/Component.js' from ./Component.js: 404 Not Found
This is not unexpected. The Component Container has been instantiated and is trying to retrieve the Component ... but failing. In the next step, we'll look at
making that Component available.
Metadata
After the initial declaration of the component itself, with the fully qualified name sap.ui.demo.tdg.Component, and the loading of our custom router
module, we start with a definition of the component as an sap.ui.core.UIComponent.
jQuery.sap.declare("sap.ui.demo.tdg.Component");
jQuery.sap.require("sap.ui.demo.tdg.MyRouter");
sap.ui.core.UIComponent.extend("sap.ui.demo.tdg.Component", {
As is typical with components, the first thing we see is the component's settings configuration, in the form of the metadata. We'll tackle the configuration in
three arbitrary sections: general, config and routing.
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 7 of 36
General
metadata : {
name : "TDG Demo App",
version : "1.0",
includes : [],
dependencies : {
libs : ["sap.m", "sap.ui.layout"],
components : []
},
rootView : "sap.ui.demo.tdg.view.App",
In this general section we specify the name of the app and its version. We also specify the library dependencies, which, when the component is instantiated,
will be loaded directly. In this case it is the sap.m and sap.ui.layout libraries that we need (the latter being for the
sap.ui.layout.form.SimpleForm controls that we use to display supplier and category information in the Detail view. This application component
doesn't rely on any other components or other files, so the rest of this section is empty.
Ultimately the component, being visual (sap.ui.core.component.UIComponent) needs to return something for the user to see. In this case it's the App
view (in App.view.xml in the view folder). You can do this by specifying a createContent function in the component and returning the view there. This is
a similar pattern to JavaScript-based view and fragment definitions. However, if you don't need to enhance your view (with additional data, for example) and
you would just be returning a fresh instantiation, then you should avoid using the createContent function, and instead, specify the view as shown here,
using the rootView metadata parameter. The component itself will then take care of the instantiation.
Config
config : {
resourceBundle : "i18n/messageBundle.properties",
serviceConfig : {
name : "Northwind",
serviceUrl : "http://services.odata.org/V2/(S(sapuidemotdg))/OData/OData.svc/"
}
},
Note
For the above config settings to work, you need to disable the web security of your browser. To do this, start your Chrome browser with the arguments -disable-web-security. This is vital, as the service you are trying to consume here lies on a different server. Another way of overcoming this problem
is to configure a proxy URL on your server that redirects requests to the Northwind service. You can use a grunt task to configure a proxy such as this.
The sample application is fairly simple and does not require much configuration. Here we have the name of the resource bundle we'll be using for our
internationalization, and the URL of the OData service we'll be using for our main, or "domain" model.
Routing
routing: {
//...
}
},
The routing configuration is not shown here; please refer to the separate section on Navigation and Routing for detailed information.
Initialization
Now that we have the configuration out of the way, it's time to initialize the component.
init : function() {
sap.ui.core.UIComponent.prototype.init.apply(this, arguments);
var mConfig = this.getMetadata().getConfig();
// always use absolute paths relative to our own component
// (relative paths will fail if running in the Fiori Launchpad)
var rootPath = jQuery.sap.getModulePath("sap.ui.demo.tdg");
// set i18n model
var i18nModel = new sap.ui.model.resource.ResourceModel({
bundleUrl : [rootPath, mConfig.resourceBundle].join("/")
});
this.setModel(i18nModel, "i18n");
// Create and set domain model to the component
var sServiceUrl = mConfig.serviceConfig.serviceUrl;
var oModel = new sap.ui.model.odata.ODataModel(sServiceUrl, true);
this.setModel(oModel);
// set device model
var deviceModel = new sap.ui.model.json.JSONModel({
isTouch : sap.ui.Device.support.touch,
isNoTouch : !sap.ui.Device.support.touch,
isPhone : sap.ui.Device.system.phone,
isNoPhone : !sap.ui.Device.system.phone,
listMode : sap.ui.Device.system.phone ? "None" : "SingleSelectMaster",
listItemType : sap.ui.Device.system.phone ? "Active" : "Inactive"
});
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 8 of 36
deviceModel.setDefaultBindingMode("OneWay");
this.setModel(deviceModel, "device");
this.getRouter().initialize();
},
});
In the component initialization, we perform various setup activities. First, we retrieve the component configuration from the metadata section shown earlier.
We're particularly interested in the service URL as that is going to be used when we create our domain model.
There are actually three models that are created here, and they're all set on the component, meaning they're available to the views within. After setting the
models on the component, we initialize the router.
Internationalization Model
This is for application texts that can be translated. See the section on Internationalization for more details.
Domain Model
The domain model is also created based upon the service URL specified in the configuration. This is usually an OData service.
Device Model
Building a responsive app means ensuring that the controls behave appropriately on different devices and screen sizes. Some responsiveness within controls
is implicit, where as other aspects must be determined explicitly, based on the device on which it is running. For these latter cases we need to detect the
device and use the information to determine how to configure the control's behavior.
Detecting the device is done using the sap.ui.Device support, and the information obtained is stored in a model. See the Device Model section for more
details.
Progress Check
We've added Component.js, so our app folder content looks like this:
tdg/
|
+-- Component.js
+-- index.html
But we're still getting a Blue Crystal styled empty screen:
Related Information
Components
Navigation
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 9 of 36
While these previous techniques work well for intra-application navigation, they don't cater for the requirements for bookmarking and resuming application User
Interface (UI) state.
Navigation
Applications exist independently, and navigation within those applications usually starts at the root control, often a container such as an sap.m.App (or
sap.m.NavContainer) or an sap.m.SplitApp (or sap.m.SplitContainer). If you want to only be able to jump into your application at the starting
point, then sharing navigation-container code is a technique that will work. However, it will not give you the ability to bookmark a certain position within the
application, and it will not support resuming application flow from that bookmarked position.
Consider our app. Here we see the UI state showing the details for a particular product (Pink Lemonade), and specifically the supplier's address details.
Without routing, navigation to this UI state would require the user to find the product in the master list, select it, and then ensure that the supplier's address
was selected in the detail view. Routing gives the application programmer the ability to support navigation directly to this UI state.
With routing, and appropriate application logic, the UI state in the screenshot could be directly navigated to from this URL:
http://<host>:<port>/path/to/app/index.html#/Products(6)/supplier
Routing
The navigation described above is achieved through use of the Routing mechanisms in SAPUI5. These are split between core () and sap.m (class).
Setup
In our app, we define and set up routing in the component. Let's examine the relevant sections of Component.js now.
Loading of Custom Router
We use a custom Router sap.ui.demo.tdg.MyRouter so in our component we make sure the module is loaded and available:
jQuery.sap.require("sap.ui.demo.tdg.MyRouter");
Routing Configuration
In our component's metadata, we define the routing configuration. This configuration is used for initializing the router.
routing : {
config : {
routerClass : sap.ui.demo.tdg.MyRouter,
viewType : "XML",
viewPath : "sap.ui.demo.tdg.view",
targetAggregation : "detailPages",
clearTarget : false
},
routes : [
{
pattern : "",
name : "main",
view : "Master",
targetAggregation : "masterPages",
targetControl : "idAppControl",
subroutes : [
{
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 10 of 36
pattern : "{product}/:tab:",
name : "product",
view : "Detail"
}
]
},
{
name : "catchallMaster",
view : "Master",
targetAggregation : "masterPages",
targetControl : "idAppControl",
subroutes : [
{
pattern : ":all*:",
name : "catchallDetail",
view : "NotFound"
}
]
}
]
}
With the config section, we define some default values:
we have a custom router class (sap.ui.demo.tdg.MyRouter)
the views are XML
the absolute path to our view definitions is sap.ui.demo.tdg.view
unless stated otherwise, when the router instantiates a view, it should place it in the detail part of our sap.m.SplitApp control (via the detailPages
aggregation)
we don't want the target aggregation (detailPages) to be cleared before views are added, so we specify false for the clearTarget parameter
The routes section is an array of routing configuration objects representing routes that we want to handle. Each configuration object has a single mandatory
parameter name. All other parameters are optional.
We have a "main" route that causes the Master view to be placed in the masterPages aggregation of the sap.m.SplitApp, which is available via its id
'idAppControl'. There is also a subroute defined, in particular:
the Detail view (route name 'product') which causes the Detail view to be instantiated and placed into the detailPages aggregation of the
sap.m.SplitApp. There are two segments that we're expecting in the URL pattern for this route:
the product context, via the section {product} - in our example above, this would be "Products(6)"
the optional information tab, via the section :tab: , which will determine which sap.m.IconTabFilter will be pre-selected - in our example
above, this would be the "supplier" tab
We also have a 'catchall' route and subroute pair which is defined so that a sensible message (in this case the details in the NotFound view) can be shown to
the user if they try to navigate, via a URL, to a resource that is not valid as far as the app is concerned.
Please refer to the navigation and routing documentation for a full explanation.
Router Initialization
The router is initialized by the component in the init function:
init : function() {
...
this.getRouter().initialize();
The initialize method will start the routing it will parse the initial hash, create the needed views, start listening to hash changes and trigger the router events.
The router is retrieved by a call of getRouter on the component.
Custom Routing
Our custom routing is performed in a module sap.ui.demo.tdg.MyRouter, which is an extended standard router. It's defined in a MyRouter.js file in
the application's root folder.
This is what MyRouter.js contains:
jQuery.sap.require("sap.m.routing.RouteMatchedHandler");
jQuery.sap.require("sap.ui.core.routing.Router");
jQuery.sap.declare("sap.ui.demo.tdg.MyRouter");
sap.ui.core.routing.Router.extend("sap.ui.demo.tdg.MyRouter", {
constructor : function() {
sap.ui.core.routing.Router.apply(this, arguments);
this._oRouteMatchedHandler = new sap.m.routing.RouteMatchedHandler(this);
},
myNavBack : function(sRoute, mData) {
var oHistory = sap.ui.core.routing.History.getInstance();
var sPreviousHash = oHistory.getPreviousHash();
// The history contains a previous entry
if (sPreviousHash !== undefined) {
window.history.go(-1);
} else {
var bReplace = true; // otherwise we go backwards with a forward history
this.navTo(sRoute, mData, bReplace);
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 11 of 36
}
},
/**
* @public Changes the view without changing the hash
*
* @param oOptions {object} must have the following properties
* <ul>
* <li> currentView : the view you start the navigation from.</li>
* <li> targetViewName : the fully qualified name of the view you want to navigate to.</li>
* <li> targetViewType : the viewtype eg: XML</li>
* <li> isMaster : default is false, true if the view should be put in the master</li>
* <li> transition : default is "show", the navigation transition</li>
* <li> data : the data passed to the navContainers livecycle events</li>
* </ul>
*/
myNavToWithoutHash : function (oOptions) {
var oSplitApp = this._findSplitApp(oOptions.currentView);
// Load view, add it to the page aggregation, and navigate to it
var oView = this.getView(oOptions.targetViewName, oOptions.targetViewType);
oSplitApp.addPage(oView, oOptions.isMaster);
oSplitApp.to(oView.getId(), oOptions.transition || "show", oOptions.data);
},
backWithoutHash : function (oCurrentView, bIsMaster) {
var sBackMethod = bIsMaster ? "backMaster" : "backDetail";
this._findSplitApp(oCurrentView)[sBackMethod]();
},
destroy : function() {
sap.ui.core.routing.Router.prototype.destroy.apply(this, arguments);
this._oRouteMatchedHandler.destroy();
},
_findSplitApp : function(oControl) {
sAncestorControlName = "idAppControl";
if (oControl instanceof sap.ui.core.mvc.View && oControl.byId(sAncestorControlName)) {
return oControl.byId(sAncestorControlName);
}
return oControl.getParent() ? this._findSplitApp(oControl.getParent(), sAncestorControlName) : null;
}
});
Use
Once the routing has been configured and initialized, it can be used, in the controllers, to marshal the appropriate data and UI components, according to the
URL pattern that is matched. This is done by attaching a function to the router's routeMatched event. Here are two examples from the Master and Detail
views.
Easy access of the router and the components eventbus. Getting the eventbus and the router is needed in most of the controllers. So adding a custom
controller for reuse purposes makes sense. Here is how it looks like:
jQuery.sap.declare("sap.ui.demo.tdg.util.Controller");
sap.ui.core.mvc.Controller.extend("sap.ui.demo.tdg.util.Controller", {
getEventBus : function () {
var sComponentId = sap.ui.core.Component.getOwnerIdFor(this.getView());
return sap.ui.component(sComponentId).getEventBus();
},
getRouter : function () {
return sap.ui.core.UIComponent.getRouterFor(this);
}
});
Earlier we saw how the router was retrieved by calling getRouter on the component. We can also access the router with a static call to
sap.ui.core.UIComponent.getRouterFor.
Route Match in Master
Attaching to events of the router should normally be set up in the controller's initialization event onInit.
onInit: function() {
//on phones, we will not have to select anything in the list so we don't need to attach to events
if (sap.ui.Device.system.phone) {
return;
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 12 of 36
}
this.getRouter().attachRoutePatternMatched(this.onRouteMatched, this);
},
onRouteMatched : function(oEvent) {
var sName = oEvent.getParameter("name");
if (sName !== "main") {
return;
}
//Load the detail view in desktop
this.getRouter().myNavToWithoutHash({
currentView : this.getView(),
targetViewName : "sap.ui.demo.tdg.view.Detail",
targetViewType : "XML"
});
//Wait for the list to be loaded once
this.waitForInitialListLoading(function () {
//On the empty hash select the first item
this.selectFirstItem();
});
},
...
},
In the handler we check to see what the name of the matched route is and act appropriately. In this case, we are only looking for the main route.
Route Match in Detail controller
Similar to the routing use in the Master controller, we also want to react in the Detail:
onInit : function() {
this.oInitialLoadFinishedDeferred = jQuery.Deferred();
if(sap.ui.Device.system.phone) {
//don't wait for the master on a phone
this.oInitialLoadFinishedDeferred.resolve();
} else {
this.getView().setBusy(true);
this.getEventBus().subscribe("Master", "InitialLoadFinished", this.onMasterLoaded, this);
}
this.getRouter().attachRoutePatternMatched(this.onRouteMatched, this);
}, onRouteMatched : function(oEvent) {
var oParameters = oEvent.getParameters();
jQuery.when(this.oInitialLoadFinishedDeferred).then(jQuery.proxy(function () {
var oView = this.getView();
// when detail navigation occurs, update the binding context
if (oParameters.name !== "product") {
return;
}
var sProductPath = "/" + oParameters.arguments.product;
this.bindView(sProductPath);
var oIconTabBar = oView.byId("idIconTabBar");
oIconTabBar.getItems().forEach(function(oItem) {
oItem.bindElement(sap.ui.demo.tdg.util.Formatter.uppercaseFirstChar(oItem.getKey()));
});
// Which tab?
var sTabKey = oParameters.arguments.tab || "supplier";
this.getEventBus().publish("Detail", "TabChanged", { sTabKey : sTabKey });
if (oIconTabBar.getSelectedKey() !== sTabKey) {
oIconTabBar.setSelectedKey(sTabKey);
}
}, this));
},
bindView : function (sProductPath) {
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 13 of 36
Progress Check
We've added MyRouter.js, so our app folder content looks like this:
tdg/
|
+-- Component.js
+-- index.html
+-- MyRouter.js
But we're still getting the Blue Crystal style empty screen:
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 14 of 36
Of course, we've specified that the rootView for this component should be sap.ui.demo.tdg.view.App, and we've said that all resources in the
sap.ui.demo.tdg namespace are in this same folder, so we can see that as we don't have a view subfolder, or anything in it, there's going to be a problem.
Note however, that you also may encounter this message when you do have the view XML that your app expects; if you do, check that the XML is well formed
and expresses the controls and their properties and aggregations correctly - otherwise, you may get this error because it was not possible to parse.
Related Information
Navigation
Note
Note that when using the sap.ui.model.resource.ResourceModel, the translation keys are referred to without the slash prefix that normally
denotes a root data property. In other words, in the data binding, use {i18n>masterTitle} rather than {i18n>/masterTitle}.
Progress Check
We've added the i18n subfolder, and the messageBundle.properties file in it, so our app folder content looks like this:
tdg/
|
+-|
|
|
+-+-+--
i18n/
|
+-- messageBundle.properties
Component.js
index.html
MyRouter.html
But as you might expect, we'll get the same outcome as before, as we still don't have the App view. Let's press on.
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 15 of 36
isNoTouch : !sap.ui.Device.support.touch,
isPhone : sap.ui.Device.system.phone,
isNoPhone : !sap.ui.Device.system.phone,
listMode : sap.ui.Device.system.phone ? "None" : "SingleSelectMaster",
listItemType : sap.ui.Device.system.phone ? "Active" : "Inactive"
});
deviceModel.setDefaultBindingMode("OneWay");
this.setModel(deviceModel, "device");
Using the sap.ui.Device library, various runtime platform characteristics are determined, and stored in a client side JSON model. The properties in this
model follow a standard pattern; it's advisable to use the same properties, even if you aren't going to utilise all of them. Here's a table showing which properties
are used where in our demo app.
Property
Description
Usage
isTouch
isNoTouch
Opposite of isTouch, for opposite declarative boolean Master.view.xml, to show a refresh button on the
isPhone
usage
isNoPhone
Opposite of isPhone, for opposite declarative boolean nowhere in this particular app
usage
listMode
listItemType
aggregation
The List's mode property and the ObjectListItem's type property are similar and control how items are selectable. On a smartphone the selection should
be via the item (listMode " None ", listItemType " Active "), otherwise it should be via the List itself (listMode " SingleSelectMaster ",
listItemType " Inactive ").The Device Model is a JSON client side model, and should be set with a One Way binding mode. This is to prevent unintentional
modification of the detected device type and support when control properties are changed through UI use. Also, the recommendation is always to use "device"
for the name of this named model.
Progress Check
There is no further progress beyond the previous section; this Device Model is part of the Component, which we already have. While the information in the
Device Model influences visual aspects of our app, these will be apparent in other sections.
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 16 of 36
state="{
path: 'Rating',
formatter: 'sap.ui.demo.tdg.util.Formatter.ratingStatusState'
}" />
and one from the Detail view:
<ObjectHeader
title="{Name}"
icon="sap-icon://database"
number="{
path: 'Price',
formatter: 'sap.ui.demo.tdg.util.Formatter.currencyValue'
}"
numberUnit="USD">
...
</ObjectHeader>
We see from the Master view example that the formatter functions are used not only to format numeric values, but also to supply the appropriate value from an
enumeration (such as the sap.ui.core.ValueState for an sap.m.ObjectStatus).
Progress Check
After adding the Formatter.js file in the new util subfolder, our app folder content looks like this:
tdg/
|
+-|
|
|
+-|
|
|
+-+-+--
i18n/
|
+-- messageBundle.properties
util/
|
+-- Formatter.js
Component.js
index.html
MyRouter.html
Related Information
Modularization and Resource Handling
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 17 of 36
+-- aggregation:detailPages
|
+-- view:Detail (view/Detail.view.xml)
|
|
|
+-- controller:Detail (view/Detail.controller.js)
|
+-- view:NotFound (view/NotFound.view.xml)
|
+-- view:AddProduct (view/AddProduct.view.xml)
|
+-- controller:AddProduct (view/AddProduct.controller.js)
Progress Check
We'll add the App view, and also a couple of temporary skeleton XML declarations for the Master and Detail views (this is mostly so we can see a little bit of
progress). Our skeleton XML declarations for the Master and Detail views look like this:
<mvc:View xmlns:mvc="sap.ui.core.mvc" />
In other words, completely empty views. Now our app folder content now looks like this:
tdg/
|
+-|
|
|
+-|
|
|
|
|
+-+-+--
i18n/
|
+-- messageBundle.properties
view/
|
+-- App.view.xml
+-- Detail.view.xml
+-- Master.view.xml
Component.js
index.html
MyRouter.html
At this stage things look a little more interesting than the previous empty screen. We can see the core structure of the SplitApp.
There are no relevant error messages in the console - the runtime is delivering exactly what we asked for (a couple of empty views in the SplitApp container
control).
In fact, because there's now actually something visible, let's have a look at an alternative screenshot, where the browser window is maximised to full screen:
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 18 of 36
Here we can see that the Master and Detail aggregation proportions remain, despite the extreme width of the browser window; there are margins on the left and
right sides. This is due to the sap.m.Shell control that we introduced in the index.html.
View
Let's examine the view definition bit by bit.
<mvc:View
controllerName="sap.ui.demo.tdg.view.Master"
displayBlock="true"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m">
We start with the view declaration itself, which also points to a corresponding controller, which we'll introduce shortly. Note that in all the views in this app,
we're specifying sap.m as the default namespace (this means that elements that don't have an XML namespace prefix belong to the sap.m library). The
displayBlock="true" declaration is to prevent scrollbars appearing.
<Page
id="page"
title="{i18n>masterTitle}">
<subHeader>
<Bar id="searchBar">
<contentMiddle>
<SearchField
id="searchField"
showRefreshButton="{device>/isNoTouch}"
search="onSearch"
tooltip="{i18n>masterSearchTooltip}"
width="100%">
</SearchField>
</contentMiddle>
</Bar>
</subHeader>
The Master view contains a single top-level control - an sap.m.Page. As a UI unit this makes sense especially being within the context of the SplitApp's
masterPages aggregation. The page's title is determined via our Internationalization mechanism, and has a subHeader aggregation, which is a single
aggregation that expects an sap.m.Bar. In the middle of the Bar we have an sap.m.SearchField control. The SearchField should appear slightly different
depending on whether the device on which the app is running has touch capabilities. There's a boolean value controlling the display of the refresh button, and
this is taken from the device model - the named model "device" in the property binding for showRefreshButton. The device model was introduced briefly in
the Component section of this part, and is covered in more detail in the Device Model section. We'll examine the handler for the Search Field shortly, when we
look at the Master controller.
<content>
<List
id="list"
items="{/Products}"
mode="{device>/listMode}"
noDataText="{i18n>masterListNoDataText}"
select="onSelect"
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 19 of 36
growing="true"
growingScrollToLoad="true">
<items>
<ObjectListItem
type="{device>/listItemType}"
press="onSelect"
title="{Name}"
number="{
path: 'Price',
formatter: 'sap.ui.demo.tdg.util.Formatter.currencyValue'
}"
numberUnit="USD">
</ObjectListItem>
</items>
</List>
</content>
The Page's default aggregation is the 'content' aggregation, and contains an sap.m.List control. The List is bound to the "/Products" entity set in our
domain model (more on this in the Data Sources section), and uses growing features to provide paging facilities. The List's selection mode is dependent on
whether the device is a smartphone or not - again, covered by the device model (see later).
With an aggregation binding, a template is required that is used to present each element of the aggregation - in this case each "Product" entity in the domain
model's "/Products" entity set. We declare this in the "items" aggregation declaration - specifying an sap.m.ObjectListItem. Each Product will be
presented using a separate ObjectListItem instance in the List.
Like the List's selection mode, the ObjectListItem's behaviour (via the "type" property) is also device-dependent. We use some of the
ObjectListItem's basic properties to display information for each Product's name and price properties. Because the Northwind service that is being used
for the domain model doesn't have currency information, we're just showing "USD" for this demo.
The Product's price itself needs to be subject to special formatting if it is to look presentable in the app - we would like the values to always appear with two
decimal places. For this, we use a formatter "currencyValue" in the Formatter.js file in our app's util folder. See the "Custom Utilities" section for more
details on how this formatter works. But here, note how the formatter is used, in a complex property binding for the ObjectListItem's number property: the
binding is still introduced with the braces, but is specified as a JavaScript object with a 'path' key pointing to the model property, and a 'formatter' key pointing
to the formatter function.
<footer>
<Bar>
<contentRight>
<Button
icon="sap-icon://add"
tooltip="{i18n>masterFooterAddButtonTooltip}"
press="onAddProduct" />
</contentRight>
</Bar>
</footer>
</Page>
</mvc:View>
Finally we have an sap.m.Bar in the Page's footer aggregation. In this Bar we have an "add" button, on the right, which, when pressed, is to bring up the
AddProduct view for creating a new product. You can see this in the app screenshot in the Introduction section of this part.
Controller
Now we've got the view declaration done, we need to look at the related controller view/Master.controller.js.
jQuery.sap.require("sap.ui.demo.tdg.util.Formatter");
In order to be able to reference and use the currencyValue formatter function (in our util/Formatter.js) for each Product's price property, we need to
declare a requirement for the module. We do it at the top of the view's controller file.
sap.ui.core.mvc.Controller.extend("sap.ui.demo.tdg.view.Master", {
onInit : function() {
this.oUpdateFinishedDeferred = jQuery.Deferred();
this.getView().byId("list").attachEventOnce("updateFinished", function() {
this.oUpdateFinishedDeferred.resolve();
}, this);
In the init event, we create a jQuery Deferred object to be able to know when the update of our List control is finished - in other words, when data has been
loaded. We set the Deferred object to resolved in a handler that we attach to the "updateFinished" event which signals that the binding has been updated.
sap.ui.core.UIComponent.getRouterFor(this).attachRouteMatched(this.onRouteMatched, this);
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 20 of 36
jQuery.when(this.oUpdateFinishedDeferred).then(jQuery.proxy(function() {
var aItems;
// On the empty hash select the first item
if (sName === "main") {
this.selectDetail();
}
// Try to select the item in the list
if (sName === "product") {
aItems = oList.getItems();
for (var i = 0; i < aItems.length; i++) {
if (aItems[i].getBindingContext().getPath() === "/" + oArguments.product) {
oList.setSelectedItem(aItems[i], true);
break;
}
}
}
}, this));
},
It's only when the list has been updated with data do we want to properly handle the route. There are two cases relating to whether a product has been selected
via the hash or not. The "main" route will match where there's no hash, and the "product" route will match with a hash containing product (and optional supplier
or category tab) information. If there's no product selected, we just select the first item. Otherwise we try to find the selected product in the list and set that to
be visibly selected.
selectDetail : function() {
if (!sap.ui.Device.system.phone) {
var oList = this.getView().byId("list");
var aItems = oList.getItems();
if (aItems.length && !oList.getSelectedItem()) {
oList.setSelectedItem(aItems[0], true);
this.showDetail(aItems[0]);
}
}
},
The selectDetail function checks to see if an item has already been selected (this could have happened based on the route match handling above) and if
so, makes sure that the item's details are shown in the Detail view, via the showDetail function.
onSearch : function() {
// add filter for search
var filters = [];
var searchString = this.getView().byId("searchField").getValue();
if (searchString && searchString.length > 0) {
filters = [ new sap.ui.model.Filter("Name", sap.ui.model.FilterOperator.Contains, searchString) ];
}
// update list binding
this.getView().byId("list").getBinding("items").filter(filters);
},
This function is the handler for the SearchField in the Master view ( <SearchField ... search="onSearch" ... /> ). This is where some of the real
power of the ODataModel is to be seen. We retrieve the search text entered, and create a "Contains" filter, in the form of an sap.ui.model.Filter, to
search for that text. And by applying this Filter (within an array of possible Filters) to the List's "items" binding, an OData QUERY operation is performed
automatically via the ODataModel mechanism. Here's an example of what that request might look like, with a search term of "Lemon":
GET
http://<host>:<port>/uilib-sample/proxy/http/services.odata.org/V2/(S(sapuidemotdg))/OData/OData.svc/Products?$skip=0&$top=1&$filt
Note that the $top=1 was included as it the number of entities containing "Lemon" in the name was determined as being 1, according to an also automatic
request for a count:
GET
http://<host>:<port>/uilib-sample/proxy/http/services.odata.org/V2/(S(sapuidemotdg))/OData/OData.svc/Products?$count?$filter=subst
This automatic request for a count is based on the fact that this domain model is specified as supporting the OData $count by default. You can turn this off
with sap.ui.model.odata.ODataModel.setCountSupported function.
onSelect : function(oEvent) {
// Get the list item, either from the listItem parameter or from the event's
// source itself (will depend on the device-dependent mode).
this.showDetail(oEvent.getParameter("listItem") || oEvent.getSource());
},
At some stage, the user will select an item, either via the List itself, or via one of the aggregated ObjectListItems. Which one the user will select, depends on
the device model settings. We handle both events (one on the List, the other on the ObjectListItem) with the onSelect function.
We're looking for the control instance that has the appropriate data context. If the event is triggered from the List, then the event parameter listItem is made
available, and that's the context. Otherwise the context is the control itself that's raising the event - i.e. the individual ObjectListItem bound to the specific
entity in the Products entity set. In both cases we pass this object to the showDetail function.
showDetail : function(oItem) {
// If we're on a phone, include nav in history; if not, don't.
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 21 of 36
Progress Check
Now our app folder content now looks like this:
tdg/
|
+-|
|
|
+-|
|
|
|
|
|
+-+-+--
i18n/
|
+-- messageBundle.properties
view/
|
+-+-+-+--
App.view.xml
Detail.view.xml
Master.controller.js
Master.view.xml
Component.js
index.html
MyRouter.js
We now have something to show that's starting to look like our app!
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 22 of 36
View
Here's what the view declaration looks like.
<mvc:View
controllerName="sap.ui.demo.tdg.view.Detail"
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns="sap.m">
Like the Master view we specify that this view has a controller, which we'll get to shortly, and that the default namespace is sap.m. While the "mvc"
namespace is for the outermost View element itself, unlike the Master view we also have a "core" namespace (sap.ui.core), which is needed in this view
because we're going to pull in a couple of XML fragments using sap.ui.core.Fragment.
<Page
showNavButton="{device>/isPhone}"
navButtonPress="onNavBack"
class="sapUiFioriObjectPage"
title="{i18n>detailTitle}">
<content>
Also like the Master view, this view's main control is a Page. This time, however, we may or may not need a back button. Remember that we may be running
on a smartphone, or a tablet, or a desktop.
If we're running on a tablet or a desktop, the app will look like what you see in the screenshot, with the master and detail parts of the SplitApp visible at the
same time. So the user has no difficulty navigating to another Product in the master list. But on a smartphone, only either the master or the detail part is
displayed at any given time, meaning that if the user is viewing a detail page and wants to select another product, they first need to get to the master. This is
what the navigation button is for - to go back. And we only want it to be shown if the device is a smartphone, so we use the Device Model and a boolean flag
to set on the showNavButton property of the Page.
A Page control's default aggregation is 'content'; while we therefore don't need to specify the <content> tag here, we should do as it's good practice.
<ObjectHeader
title="{Name}"
number="{
path: 'Price',
formatter: 'sap.ui.demo.tdg.util.Formatter.currencyValue'
}"
numberUnit="USD">
<attributes>
<ObjectAttribute text="{i18n>detailFromDate} {
path: 'ReleaseDate',
type: 'sap.ui.model.type.Date'
}" />
<ObjectAttribute text="{Description}" />
</attributes>
<firstStatus>
<ObjectStatus
text="{
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 23 of 36
path: 'DiscontinuedDate',
formatter: 'sap.ui.demo.tdg.util.Formatter.discontinuedStatusValue'
}"
state="{
path: 'DiscontinuedDate',
formatter: 'sap.ui.demo.tdg.util.Formatter.discontinuedStatusState'
}" />
</firstStatus>
</ObjectHeader>
The detail consists of an sap.m.ObjectHeader control at the top. This has a couple of aggregations that are being used - attributes and
firstStatus. For all of the properties in the controls that are aggregated (such as sap.m.ObjectAttribute and sap.m.ObjectStatus), as well as
some of the basic properties (such as 'number') we use the extended binding syntax again to enable us to use formatter and type specifications.
While the formatter functions were introduced in the Custom Utilities section, it's worth highlighting here that formatter functions are not just for returning
formatted versions of the values that are passed to them. They can be used, as here in the state property of the ObjectStatus control, to return any value, in
this case one of the enumerated states in sap.ui.core.ValueState, to highlight a discontinued product status with a semantic color - red, via the Error
state.
<IconTabBar
select="onDetailSelect"
id="idIconTabBar">
<items>
<IconTabFilter
key="supplier"
text="{i18n>iconTabFilterSupplier}"
icon="sap-icon://supplier">
<content>
<!--core:Fragment fragmentName="sap.ui.demo.tdg.view.SupplierAddressForm" type="XML" /-->
</content>
</IconTabFilter>
<IconTabFilter
key="category"
text="{i18n>iconTabFilterCategory}"
icon="sap-icon://hint">
<content>
<!--core:Fragment fragmentName="sap.ui.demo.tdg.view.CategoryInfoForm" type="XML" /-->
</content>
</IconTabFilter>
</items>
</IconTabBar>
The other main part of the detail view's content is an sap.m.IconTabBar, to display information about the selected Product entity's Supplier and Category
information.
The two sap.m.IconTabFilter controls that are contained in the IconTabBar reference XML fragments where the bulk of the information layout is
declared. We'll come to the content of these XML fragments later. For now, the references ("core:Fragment...") are temporarily commented out, because
if we include them now without adding the actual XML fragment files, we'll get an error and our progress will take a few steps backwards. Note the key
properties of each IconTabFilter - we'll be using these to control the navigation to the appropriate information display and data binding in the
RouteMatched handler in the controller.
</content>
<footer>
<Bar>
</Bar>
</footer>
</Page>
</mvc:View>
To finish off the Page, we have an sap.m.Bar in the footer aggregation, so that the detail view matches the master visually.
Controller
The bulk of the detail view's controller is for handling the navigation events that are initiated through the Router.
sap.ui.core.mvc.Controller.extend("sap.ui.demo.tdg.view.Detail", {
onInit : function() {
var oView = this.getView();
sap.ui.core.UIComponent.getRouterFor(this).attachRouteMatched(function(oEvent) {
// when detail navigation occurs, update the binding context
if (oEvent.getParameter("name") === "product") {
var sProductPath = "/" + oEvent.getParameter("arguments").product;
oView.bindElement(sProductPath);
// Check that the product specified actually was found
oView.getElementBinding().attachEventOnce("dataReceived", jQuery.proxy(function() {
var oData = oView.getModel().getData(sProductPath);
if (!oData) {
sap.ui.core.UIComponent.getRouterFor(this).myNavToWithoutHash({
currentView : oView,
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 24 of 36
targetViewName : "sap.ui.demo.tdg.view.NotFound",
targetViewType : "XML"
});
}
}, this));
Progress Check
With our skeleton Detail.view.xml file replaced, and a corresponding controller added, our app folder content now looks like this:
tdg/
|
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 25 of 36
+-|
|
|
+-|
|
|
+-|
|
|
|
|
|
|
+-+-+--
i18n/
|
+-- messageBundle.properties
util/
|
+-- Formatter.js
view/
|
+-+-+-+-+--
App.view.xml
Detail.controller.js
Detail.view.xml
Master.controller.js
Master.view.xml
Component.js
index.html
MyRouter.html
The IconTabFilters for the Supplier and Category information are empty, but that's just because we haven't got round to including the XML fragments that
describe what goes in there. Let's get to that next.
SupplierAddressForm
First up is the SupplierAddressForm fragment, in file SupplierAddressForm.fragment.xml.
<core:FragmentDefinition
xmlns:l="sap.ui.layout"
xmlns:f="sap.ui.layout.form"
xmlns:core="sap.ui.core"
xmlns="sap.m">
This outermost element core:FragmentDefinition is not strictly necessary here, as our control definition has a single root node l:Grid. However, it's
good practice to always use this core:FragmentDefinition element as a container, not least because then any additional root node in the definition
doesn't cause a bigger change during maintenance. The container element doesn't have any representation in the HTML.
<l:Grid
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 26 of 36
CategoryInfoForm
Here's the CategoryInfoForm fragment.
<core:FragmentDefinition
xmlns:l="sap.ui.layout"
xmlns:f="sap.ui.layout.form"
xmlns:core="sap.ui.core"
xmlns="sap.m">
<l:Grid
defaultSpan="L12 M12 S12"
width="auto">
<l:content>
<f:SimpleForm
minWidth="1024"
maxContainerCols="2"
editable="false"
layout="ResponsiveGridLayout"
title="{i18n>categoryInfo}"
labelSpanL="3"
labelSpanM="3"
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 27 of 36
emptySpanL="4"
emptySpanM="4"
columnsL="1"
columnsM="1">
<f:content>
<Label text="{i18n>categoryInfoID}" />
<Text text="{ID}" />
<Label text="{i18n>categoryInfoName}" />
<Text text="{Name}" />
</f:content>
</f:SimpleForm>
</l:content>
</l:Grid>
</core:FragmentDefinition>
It's very similar to the SupplierAddressForm fragment. Again, the bindings for the Text controls are relative to where the fragment is inserted; in this case it will
be relative to the associated Category.
Progress Check
With the fragment files added to the view subfolder, our app folder content now looks like this:
tdg/
|
+-|
|
|
+-|
|
|
+-|
|
|
|
|
|
|
|
|
+-+-+--
i18n/
|
+-- messageBundle.properties
util/
|
+-- Formatter.js
view/
|
+-+-+-+-+-+-+--
App.view.xml
CategoryInfoForm.fragment.xml
Detail.controller.js
Detail.view.xml
Master.controller.js
Master.view.xml
SupplierAddressForm.fragment.xml
Component.js
index.html
MyRouter.html
And when we uncomment the fragment insertion points in the detail view (see the Detail View section)
<items>
<IconTabFilter
key="supplier"
text="{i18n>iconTabFilterSupplier}"
icon="sap-icon://supplier">
<content>
<core:Fragment fragmentName="sap.ui.demo.tdg.view.SupplierAddressForm" type="XML" />
</content>
</IconTabFilter>
<IconTabFilter
key="category"
text="{i18n>iconTabFilterCategory}"
icon="sap-icon://hint">
<content>
<core:Fragment fragmentName="sap.ui.demo.tdg.view.CategoryInfoForm" type="XML" />
</content>
</IconTabFilter>
</items>
our app now looks like this, when a product is selected (remember, the default IconTabFilter is the for the supplier information):
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 28 of 36
View
Let's first take a peek at what we're trying to achieve in this view definition.
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 29 of 36
It's an sap.m.Page, with an sap.ui.layout.form.SimpleForm within the flexible sap.ui.layout.Grid control. Like the Page controls in the master
and detail views, this Page also has a Bar in the footer, and in this case, there are two buttons on the right hand side.
<mvc:View
controllerName="sap.ui.demo.tdg.view.AddProduct"
xmlns:mvc="sap.ui.core.mvc"
xmlns:l="sap.ui.layout"
xmlns:f="sap.ui.layout.form"
xmlns:c="sap.ui.core"
xmlns="sap.m">
This should be familiar by now; we're declaring our view with the namespaces for the controls we're going to use within.
<Page
class="sapUiFioriObjectPage"
title="{i18n>addProductTitle}">
<l:Grid
defaultSpan="L12 M12 S12"
width="auto">
<l:content>
<f:SimpleForm
id="idAddProductForm"
minWidth="800"
maxContainerCols="2"
editable="true"
layout="ResponsiveGridLayout"
title="New Product"
labelSpanL="3"
labelSpanM="3"
emptySpanL="4"
emptySpanM="4"
columnsL="1"
columnsM="1"
class="editableForm">
<f:content>
The SimpleForm sits within the Grid. The SimpleForm's content aggregation is where we define our separate form sections. Each section begins with an
sap.ui.core.Title control, which causes a subheader style title to appear (these are the Basic Info , Discontinued and Supplier & Category texts
visible in the screenshot above).
<!-- Basic info -->
<c:Title text="{i18n>addProductTitleBasic}" />
<Label text="{i18n>addProductLabelName}" />
<Input value="{newProduct>/Detail/Name}" />
<Label text="{i18n>addProductLabelDescription}" />
<TextArea value="{newProduct>/Detail/Description}" />
<Label text="{i18n>addProductLabelReleaseDate}" />
<DateTimeInput
type="Date"
value="{newProduct>/Detail/ReleaseDate}" />
<Label text="{i18n>addProductLabelPrice}" />
<Input value="{newProduct>/Detail/Price}" />
<Label text="{i18n>addProductLabelRating}" />
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 30 of 36
Controller
sap.ui.core.mvc.Controller.extend("sap.ui.demo.tdg.view.AddProduct", {
oAlertDialog : null,
oBusyDialog : null,
We're going to show an alert dialog if there's a problem with the input, and we also have an sap.m.BusyDialog to show while the process of saving the new
product is carried out. We'll hold references to these here.
Note
In this demo app, there is little to no client side validation. That is left as an exercise for you, dear reader!
initializeNewProductData : function() {
this.getView().getModel("newProduct").setData({
Detail: {
DiscontinuedFlag: false
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 31 of 36
}
});
},
With this initializeNewProductData function, we can reset the data in the JSON model, effectively clearing the values in the form fields. We're holding
the properties within a single member "Detail" to make it simpler to reset.
onInit : function() {
this.getView().setModel(new sap.ui.model.json.JSONModel(), "newProduct");
this.initializeNewProductData();
},
This is the named model that is used to communicate data between the form in the view and controller.
showErrorAlert : function(sMessage) {
jQuery.sap.require("sap.m.MessageBox");
sap.m.MessageBox.alert(sMessage);
},
Later, we instantiate an sap.m.Dialog control to alert the user to issues with form input. But not all alerts need to use explicitly instantiated controls; in this
showErrorAlert function, invoked in a couple of places in this controller, we use the static method sap.m.MessageBox.alert as a convenience.
dateFromString : function(sDate) {
// Try to create date directly, otherwise assume dd/mm/yyyy
var oDate = new Date(sDate);
return oDate === "Invalid Date" ? new Date(sDate.split("/").reverse()) : oDate;
},
saveProduct : function(nID) {
var mNewProduct = this.getView().getModel("newProduct").getData().Detail;
// Basic payload data
var mPayload = {
ID: nID,
Name: mNewProduct.Name,
Description: mNewProduct.Description,
ReleaseDate: this.dateFromString(mNewProduct.ReleaseDate),
Price: mNewProduct.Price.toString(),
Rating: mNewProduct.Rating
};
if (mNewProduct.DiscontinuedDate) {
mPayload.DiscontinuedDate = this.dateFromString(mNewProduct.DiscontinuedDate);
}
The saveProduct function does the heavy lifting of invoking the OData CREATE operation on the OData service via the domain model. First, the data in the
JSON model is retrieved and unpacked, and a payload for the CREATE operation is created.
// Add supplier & category associations
["Supplier", "Category"].forEach(function(sRelation) {
var oSelect = this.getView().byId("idSelect" + sRelation);
var sPath = oSelect.getSelectedItem().getBindingContext().getPath();
mPayload[sRelation] = {
__metadata: {
uri: sPath
}
};
}, this);
Remember that the Product entity has associations to the Supplier and Category entities. So when we create the new Product, we must ensure that the
associations are made to the Supplier and Category chosen in the form. This is done by specifying a __metadata object for each association, with a uri
property pointing to the chosen entity's path.
Here's an example of what that payload looks like:
{
"ID":9,
"Name":"Galaxy S4",
"Description":"Samsung smartphone",
"ReleaseDate":null,
"Price":"499.00",
"Rating":4,
"Supplier":{
"__metadata":{
"uri":"/Suppliers(1)"
}
},
"Category":{
"__metadata":{
"uri":"/Categories(2)"
}
}
}
This example is for a new product that is associated with the supplier Tokyo Traders (/Suppliers(1)) and category "Electronics" (/Categories(2)).
// Send OData Create request
var oModel = this.getView().getModel();
oModel.create("/Products", mPayload, {
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 32 of 36
success : jQuery.proxy(function(mResponse) {
this.initializeNewProductData();
sap.ui.core.UIComponent.getRouterFor(this).navTo("product", {
from: "master",
product: "Products(" + mResponse.ID + ")",
tab: "supplier"
}, false);
jQuery.sap.require("sap.m.MessageToast");
// ID of newly inserted product is available in mResponse.ID
this.oBusyDialog.close();
sap.m.MessageToast.show("Product '" + mPayload.Name + "' added");
}, this),
error : jQuery.proxy(function() {
this.oBusyDialog.close();
this.showErrorAlert("Problem creating new product");
}, this)
});
},
Once the payload is ready the OData CREATE request is invoked. On success, a MessageToast is shown with the ID of the newly created Product entity,
the form data is reset, and we get the Router to navigate the user to the display of the new Product entry. Otherwise a simple alert message is shown using
the showAlertError function that we've already seen.
onSave : function() {
// Show message if no product name has been entered
// Otherwise, get highest existing ID, and invoke create for new product
if (!this.getView().getModel("newProduct").getProperty("/Detail/Name")) {
if (!this.oAlertDialog) {
this.oAlertDialog = sap.ui.xmlfragment("sap.ui.demo.tdg.view.NameRequiredDialog", this);
this.getView().addDependent(this.oAlertDialog);
}
this.oAlertDialog.open();
We impose a small restriction on the input of the new product details - there must be a name specified. If not, we instantiate an alert Dialog (if it doesn't exist
already) and show it. Where is the definition? In an XML fragment, of course!
Fragments are not only useful for for separating out UI definitions into discrete and maintainable chunks, but also allow you to use your choice of view
definition language (in our case XML) for all of your UI elements. So instead of declaring the sap.m.Dialog-based alert in-line, in this condition, with
JavaScript, you can and should declare it using the same approach (XML) as the rest of your UI. See below for the XML fragment where this Dialog is defined.
} else {
if (!this.oBusyDialog) {
this.oBusyDialog = new sap.m.BusyDialog();
}
this.oBusyDialog.open();
this.getView().getModel().read("/Products", {
urlParameters : {
"$top" : 1,
"$orderby" : "ID desc",
"$select" : "ID"
},
async : false,
success : jQuery.proxy(function(oData) {
this.saveProduct(oData.results[0].ID + 1);
}, this),
error : jQuery.proxy(function() {
this.oBusyDialog.close();
this.showErrorAlert("Cannot determine next ID for new product");
}, this)
});
}
},
If all is ok with the form input, we want to go ahead with saving the new product. Our OData service (Northwind) requires an ID key to be specified (it's not
auto-generated on creation). So we need to find the highest ID and then increment it.For the first time in this app, we're going to invoke an explicit OData
operation to retrieve data: a QUERY, via the sap.ui.model.odata.ODataModel.read function. For READ and QUERY operations on an OData service,
this should be the exception. But in this case it's required - we need to use a combination of the $top, $orderby and $select query options: "Give me the
ID property of the entity that has the highest ID in the Products entity set"
onCancel : function() {
sap.ui.core.UIComponent.getRouterFor(this).backWithoutHash(this.getView());
},
This function is called to handle the press event of the "Cancel" Button. It uses the sap.ui.demo.tdg.MyRouter.backWithoutHash function to return
to the product detail that was visible before this AddProduct view was displayed. This backWithoutHash function recursively ascends the UI tree via a
helper function _findSplitApp, to discover the app's "root' control, the sap.m.SplitApp. On finding this, it can call the appropriate back function.
onDialogClose : function(oEvent) {
oEvent.getSource().getParent().close();
}
});
The last function in this controller is a handler for the press event on the sap.m.Dialog defined in the XML fragment
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 33 of 36
NameRequiredDialog.fragment.xml.
Fragment
The XML fragment NameRequiredDialog.fragment.xml is where the alert Dialog is defined, and looks like this:
<core:FragmentDefinition
xmlns:core="sap.ui.core"
xmlns="sap.m">
<Dialog
title="{i18n>nameRequiredDialogTitle}"
type="Message">
<content>
<Text text="{i18n>nameRequiredDialogText}" />
</content>
<beginButton>
<Button text="{i18n>nameRequiredDialogButton}" press="onDialogClose" />
</beginButton>
</Dialog>
</core:FragmentDefinition>
Like the other fragments in this app (see the Detail XML Fragments section) this fragment has only a single root node ( <Dialog> ), nevertheless we're using
the <core:FragmentDefinition> wrapper here too. Note also that we're also explicitly specifying the sap.m.Dialog's default aggregation rather than
implying it ( <content> ). Finally, it's worth pointing out that the handler for the press event is to be found in the controller related to the view where this
fragment is inserted, i.e. AddProduct.controller.js.
Progress Check
Ok, we're almost there. Our app folder content now looks like this:
tdg/
|
+-|
|
|
+-|
|
|
+-|
|
|
|
|
|
|
|
|
|
|
|
+-+-+--
i18n/
|
+-- messageBundle.properties
util/
|
+-- Formatter.js
view/
|
+-+-+-+-+-+-+-+-+-+--
AddProduct.controller.js
AddProduct.view.xml
App.view.xml
CategoryInfoForm.fragment.xml
Detail.controller.js
Detail.view.xml
Master.controller.js
Master.view.xml
NameRequiredDialog.fragment.xml
SupplierAddressForm.fragment.xml
Component.js
index.html
MyRouter.html
Here's a shot of the AddProduct view with the alert Dialog shown (as we have deliberately not specified a value for the Product Name:
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 34 of 36
Progress Check
Finally, this is our complete app folder:
tdg/
|
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 35 of 36
+-|
|
|
+-|
|
|
+-|
|
|
+-|
|
|
|
|
|
|
|
|
|
|
|
|
+-+-+--
css/
|
+-- style.css
i18n/
|
+-- messageBundle.properties
util/
|
+-- Formatter.js
view/
|
+-+-+-+-+-+-+-+-+-+-+--
AddProduct.controller.js
AddProduct.view.xml
App.view.xml
CategoryInfoForm.fragment.xml
Detail.controller.js
Detail.view.xml
Master.controller.js
Master.view.xml
NameRequiredDialog.fragment.xml
NotFound.view.xml
SupplierAddressForm.fragment.xml
Component.js
index.html
MyRouter.html
When navigating to an unknown URL, this is what the user will see:
PUBLIC
2014 SAP SE or an SAP affiliate company. All rights reserved.
Page 36 of 36