Creating modularized SAPUI5 applications with XML Views, Routing and i18n

In this tutorial you will learn how to create modularized SAPUI5 applications using XML Views and Routing. We will create a simple Shell application that loads its content from XML Views, uses Routing and allows deep linking in order to allow access to your single page application by bookmarks. You will also learn how to make your SAPUI5 application i18n aware. Furthermore, we will create a simple custom control that uses D3.js, an external and open source library which we will package into our application. The custom control is used in one of our XML Views to demonstrate how to reference custom controls in XML Views.

Table of contents:


1. Creating the index.html file

Our index.html is the entry point for our SAPUI5 based single page application. It loads SAPUI5, registers some important module paths an initializes the application. There are different approaches of doing all this. I have decided to apply a pattern that keeps the index.html pretty empty and therefore easy to read. We have to register our module paths before we can load the modules. A module is a vague term, a module can actually be almost everything. For us, every module that starts with namespace nabisoft.modules can be found in the subfolder ./modules/ and represents our views and controllers (only *.controller.js and *.view.xml files are in here - plus one Main.view.js file). Sometimes people use view, views or module instead of modules for the namespace and folder name. Just decide what every you prefer. The namespace nabisoft.lib references the subfolder ./js/lib/nabisoft/ and contains our global libraries that we need in our application. nabisoft.control references the subfolder ./js/control/ and will contain our custom controls. nabisoft.lib.ext contains 3rd party libs, i.e. D3.js. With this little setup we have now already configured the first steps needed for the modularization of SAPUI5 applications. In line 29 of the code box below you can see that we load nabisoft.lib.app. This will make the app object available globally, which encapsulates the initialization of the rest of our application and offers some helpful functions. One of the helpful functions is app.includeStyleSheet(sPath), which helps to load a stylesheet if it has not been loaded yet. Before we initialize our app via app.init() we load some other of our custom libs to have them available globally. nabisoft.lib.appConstants and nabisoft.lib.appUtils could offer some utility functions and constants (it is arguable to move these two libs directly into nabisoft.lib.app).

./index.html
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta http-equiv='Content-Type' content='text/html;charset=UTF-8'/>
        
        <!-- we are not doing this here, because it's not cachebuster safe! -->
        <!-- <link rel="stylesheet" type="text/css" href="./css/app.css" />  -->

        <script id="sap-ui-bootstrap"
            type="text/javascript"
            src="https://openui5.hana.ondemand.com/1.36.12/resources/sap-ui-core.js"
            data-sap-ui-theme="sap_goldreflection"
            data-sap-ui-libs="sap.ui.commons,sap.ui.ux3">
        </script>
        <!-- instead of sap_goldreflection you could also use for example sap_bluecrystal -->

        <script>
            (function(){
                "use strict";
                
                //register modules paths
                jQuery.sap.registerModulePath("nabisoft.modules", "./modules/");        //controllers and views
                jQuery.sap.registerModulePath("nabisoft.lib", "./js/lib/nabisoft/");    //our own libs
                jQuery.sap.registerModulePath("nabisoft.lib.ext", "./js/lib/ext/");     //for 3rd party libs, i.e. D3.js
                jQuery.sap.registerModulePath("nabisoft.control", "./js/control/");     //our custom controls
                
                //load our core app lib and the global css (if cachebuster was configured, then this would be cachebuster safe!) 
                jQuery.sap.require("nabisoft.lib.app");
                app.includeStyleSheet('css/app.css');
                
                //now load other stuff that should be globally available
                jQuery.sap.require("nabisoft.lib.appConstants");
                jQuery.sap.require("nabisoft.lib.appUtils");
                
                //init our app
                app.init();                
                
            })();
        </script>

    </head>
    <body class="sapUiBody" role="application">
        <div id="content"></div>
    </body>
</html>

Both jQuery.sap.require(...) and app.includeStyleSheet(sPath) are cachebuster safe, meaning that if you have configured your SAPUI5 application to be cachbuster aware then you don't have to worry too much about browsers caching too much. The SAPUI5 core team has implemented a very smart cachebuster feature, however, you need server side components to support this (Java/ABAP components are offered by SAP). We are not going to focus on the cachebuster, just keep in mind that this tutorial is 100% cachebuster safe. For more information about cachebusters please check the official SAPUI5 documentation (simply search for cachebuster).

As you can see in the index.html we also call app.includeStyleSheet('css/app.css'). The file app.css could contain some global css rules (in our little demo the file is empty):

./css/app.css
/* global relevant css styles */

2. Implementing our custom libs (nabisoft.lib.*)

Our custom libs are located in nabisoft.lib.* and they are referenced in the index.html (see above). Below you can see the code for nabisoft.lib.app, nabisoft.lib.appConstants and nabisoft.lib.appUtils. I have added comments to help you understand the code better.

In app.js we have an init() function which creates a global "appModel", an i18n model and loads the initial view: nabisoft.modules.Main - our one and only JS View. As you can see the JS View is in namespace nabisoft.modules, which means that SAPUI5 will try to load the file ./modules/Main.js (we will check the code later).

./js/lib/nabisoft/app.js
(function(window){
    "use strict";

    jQuery.sap.declare('nabisoft.lib.app');

    var oAppModel, oResourceBundle, sBasePath, sBaseUrl, sResourceBundlePath, sAppRouterName;

    oAppModel           = null;
    oResourceBundle     = null;
    sBasePath           = window.location.pathname.replace(/\/[^\/]*\.?[^\/]*$/, '/');            // in this demo ==> /demo/sapui5/shell/ShellRouterDemo/
    sBaseUrl            = window.location.protocol + '//' + window.location.host + sBasePath;
    sResourceBundlePath = sBaseUrl + "i18n/messages.properties";
    sAppRouterName      = "nabisoftAppRouter";
    
    window.app = {};
    
    app.init = function () {
        var oI18nModel, ui5core;
        
        ui5core = sap.ui.getCore();
        
        oAppModel = new sap.ui.model.json.JSONModel();
        ui5core.setModel(oAppModel, "appModel");
        
        //we could force a certain locale/language
        //oI18nModel = new sap.ui.model.resource.ResourceModel({bundleUrl:sResourceBundlePath, bundleLocale:"en"});
        //or we just let ui5 decide:
        //(HINT: this will cause a messages_de_DE.properties 404 (Not Found) in our setup for German browsers, but then messages_de.properties is found)
        oI18nModel = new sap.ui.model.resource.ResourceModel({bundleUrl:sResourceBundlePath});
        ui5core.setModel(oI18nModel, "i18n");
        oResourceBundle = oI18nModel.getResourceBundle();
        
        //this is the one and only JS View which will use in our application (all other views are XML Views) 
        var view = sap.ui.view({id:"shellMainView", viewName:"nabisoft.modules.Main", type:sap.ui.core.mvc.ViewType.JS});
        view.placeAt("content");

    };

    /**
     * Return the localized string for the given key
     * @param {string} key Key of the localization property
     * @param {Array=} args Array of strings containing the values to be used for replacing placeholders *optional*
     * @return {string} the corresponding translated text
     */
    app.i18n = function (key, args) {
        return oResourceBundle.getText(key, args);
    };

    app.getAppRouterName = function () {
        return sAppRouterName;
    };

    app.getAppRouter = function () {
        return sap.ui.core.routing.Router.getRouter(sAppRouterName);
    };

    app.getAppModel = function () {
        return oAppModel;
    };

    app.setShell = function (oShell) {
        app.oShell = oShell;
    };

    app.getShell = function () {
        return app.oShell;
    };

    app.getBaseUrl = function () {
        return sBaseUrl;
    };
    
    /**
     * Loads a stylesheet in case it has not been loaded yet. If the stylesheet has been loaded already, then nothing happens (no replacement).
     * This is a handy function which prevents some issues that can occur, especially in older browsers (IE7).
     * For replacement enabled inclusion please use jQuery.sap.includeStyleSheet(sUrl, sId);
     * @param {string} sUrl the application relative path to the css file
     * @param {string} id dom id of the css that shall be used. If not defined, then the dom id will be calculated from sUrl ("/" will be replaced with "-")
     */
    app.includeStyleSheet = function (sUrl, sId) {
        if (!sUrl) {
            return;
        }
        if (!sId){
            sId = sUrl.replace(/\//g, "-");
        }
        if (!document.getElementById(sId)){
            return jQuery.sap.includeStyleSheet(sUrl, sId);
        }
    };
    
    /**
     * Reload the current page and ignore browser cache
     */
    app.reload = function () {
        window.location.reload(true);
    };

    /**
     * Allows to change the language on the fly
     * @param {string} lang new language to be used application wide
     */
    app.setLang = function (lang) {
        var oModel, oCore, oConfig;

        oCore = sap.ui.getCore();
        oConfig = oCore.getConfiguration();

        if (oConfig.getLanguage() !== lang) {
            oConfig.setLanguage(lang);

            oModel = new sap.ui.model.resource.ResourceModel({
                bundleUrl : sResourceBundlePath,
                bundleLocale : lang});
            oCore.setModel(oModel, 'i18n');
            oResourceBundle = oModel.getResourceBundle();
        }
    };

})(window);

Our appConstants contain global constants that could be used from anywhere in the application. Since the appConstanst object is added to the window object you can easily access our constants, i.e. appConstants.CONSTANT_ONE. Feel free to add your own constants here.

./js/lib/nabisoft/appConstants.js
(function (window) {
    "use strict";

    jQuery.sap.declare('nabisoft.lib.appConstants');
    
    window.appConstants = {
        CONSTANT_ONE : "My Constant 1 Value",
        CONSTANT_TWO : "My Constant 2 Value"
    };
}(window));

Our appUtils contain utility functions that could be used from anywhere in the application. Since the appUtils object is added to the window object you can simply access utility functions, i.e. appUtils.addNumbers(2,7). Feel free to add your own utility functions here.

./js/lib/nabisoft/appUtils.js
(function(window){
    "use strict";

    jQuery.sap.declare('nabisoft.lib.appUtils');

    window.appUtils = {}; 
    /**
     * this is just an example
     */
    appUtils.addNumbers = function (param1, param2) {
        return param1 + param2;
    };

})(window);

3. Adding the i18n resources

Our nabisoft.lib.app creates and registers an i18n model (ResourceModel). The so called bundle url is /i18n/messages.properties. For illustration purposes we will create messages for English (en) and German (de) language: /i18n/messages_en.properties and /i18n/messages_de.properties (we won't use messages_de_DE.properties etc). Please notice that the files are UTF-8 files and that we encode special chars as Unicode chars, i.e. the German Ü is encoded as \u00DC. The properties also allow placeholders.

We can use the translated messages either via our handy function, i.e. app.i18n("shell.headerItems.username"), or via data binding (i.e. text = "{i18n>shell.headerItems.username}"). You will see both options in our Main.view.js file later. You can even pass placeholders, i.e. app.i18n('shell.search.triggeredMessage', ["my search string"]). In some projects developers prefer to have a shorter shortcut than app.i18n(...), typically app.txt(...), app.msg(...), app.m(...) or app.t(...) - please decide what ever you prefer. To change the language dynamically (i.e. by a user action), you can simply call our function app.setLang("en") or app.setLang("de"). Try this out in your Chrome console and you will see how fascinating and easy that is. However, keep in mind that the texts and messages on the UI will only change immediately in case the corresponding controls are bound to our i18n texts via SAPUI5 data binding!

Here is the content of each of the files:

./i18n/messages.properties
#################################################################
# DO NOT ADD ANY PROPERTIES TO THIS FILE - USE THE LOCALIZED ONES
#################################################################
./i18n/messages_en.properties
date=date
browser=browser

shell.headerItems.username=User Name
shell.headerItems.personalize=Personalize
shell.headerItems.help=Help
shell.headerItems.help.tooltip=Help Menu
shell.headerItems.help.reportIncident=Report Incident
shell.headerItems.help.about=About
shell.headerItems.logout.pressedMessage=Logout Button has been clicked
shell.search.triggeredMessage=Search triggered: {0} 
shell.feedSubmit=Feed entry submitted: {0}
shell.paneCLosed=Pane has been closed: {0}
./i18n/messages_de.properties
date=datum
browser=browser

shell.headerItems.username=Benutzername
shell.headerItems.personalize=Personalisieren
shell.headerItems.help=Hilfe
shell.headerItems.help.tooltip=Hilfemen\u00FC
shell.headerItems.help.reportIncident=Problem melden
shell.headerItems.help.about=\u00DCber
shell.headerItems.logout.pressedMessage=Logout Button wurde geklickt
shell.search.triggeredMessage=Suche getriggert: {0}
shell.feedSubmit=Feed eintrag hinzugf\u00FCgt: {0}
shell.paneCLosed=Pane wurde geschlossen: {0}

4. Implementing the Main View and Controller incl. Router configuration

Let's start with our Main.controller.js, which is pretty much empty. Please make sure to change the code as you wish:

./modules/Main.controller.js
jQuery.sap.require("sap.ui.core.routing.Router");

sap.ui.controller("nabisoft.modules.Main", {

    oShell : null,

    /**
     * Called when a controller is instantiated and its View controls (if
     * available) are already created. Can be used to modify the View before it
     * is displayed, to bind event handlers and do other one-time
     * initialization.
     *
     * @memberOf nabisoft.modules.Main
     */
    onInit : function() {

    },

    /**
     * Similar to onAfterRendering, but this hook is invoked before the
     * controller's View is re-rendered (NOT before the first rendering!
     * onInit() is used for that one!).
     *
     * @memberOf nabisoft.modules.Main
     */
    onBeforeRendering : function() {
        
    },

    /**
     * Called when the View has been rendered (so its HTML is part of the
     * document). Post-rendering manipulations of the HTML could be done here.
     * This hook is the same one that SAPUI5 controls get after being rendered.
     *
     * @memberOf nabisoft.modules.Main
     */
    onAfterRendering : function() {

    },

    /**
     * Called when the Controller is destroyed. Use this one to free resources
     * and finalize activities.
     *
     * @memberOf nabisoft.modules.Main
     */
    onExit : function() {

    }

});

The Main.view.js contains a little more code. As you can see from the file name this file is a JS View. What we want to do here is to creata a default Shell and configure and initialize the Router. Furthermore, in some parts of the code you will see how to bind or access our i18n texts/messages. Some parts of the code comes from the official SAPUI5 documentation, i.e. the worksetItemSelected callback function (you should actually set a breakpoint in there to better understand how the right URL/route is selected). As you can see in the Router configuration we reference different other views, all of them are XML Views. Thanks to this configuration SAPUI5 will handle everything for us, including deep linking (i.e. for bookmarks). Isn't this great? Please read the official SAPUI5 documentation to learn more about Routers and configuration of Routers.

./modules/Main.view.js
jQuery.sap.require("sap.ui.ux3.Shell");

sap.ui.jsview("nabisoft.modules.Main", {

    /**
     * Specifies the Controller belonging to this View.
     * In the case that it is not implemented, or that "null" is returned, this View does not have a Controller.
     */
    getControllerName : function() {
        return "nabisoft.modules.Main";
    },

    /**
     * Is initially called once after the Controller has been instantiated. It is the place where the UI is constructed.
     * Since the Controller is given to this method, its event handlers can be attached right away.
     */
    createContent : function(oController) {
        var oRouter, oShell;
        
        //alert("nabisoft.modules.Main.view.js called");

        oShell = new sap.ui.ux3.Shell("demoShell", {
            appTitle : "Shell Demo Application with Routing",
            appIcon : "images/SAPLogo.gif",
            appIconTooltip : "SAP logo",
            showLogoutButton : true,
            showSearchTool : true,
            showInspectorTool : true,
            showFeederTool : true,
            worksetItems: [
                new sap.ui.ux3.NavigationItem("WI_home",{key:"wi_home",text:"Home"}),
                new sap.ui.ux3.NavigationItem("WI_1",{key:"wi_1",text:"Sub Items", subItems:[
                    new sap.ui.ux3.NavigationItem("WI_1_1",{key:"wi_1_1",text:"One"}),
                    new sap.ui.ux3.NavigationItem("WI_1_2",{key:"wi_1_2",text:"Two"}),
                    new sap.ui.ux3.NavigationItem("WI_1_3",{key:"wi_1_3",text:"Three"})
                ]}),
                new sap.ui.ux3.NavigationItem("WI_EXMPLS",{key:"wi_exmpls",text:"Examples"})
            ],
            paneBarItems: [
                new sap.ui.core.Item("PI_Date",{key:"pi_date",text:"{i18n>date}"}),
                new sap.ui.core.Item("PI_Browser",{key:"pi_browser",text:"{i18n>browser}"})
            ],
            //content: oTextHome,
            toolPopups : [ new sap.ui.ux3.ToolPopup("contactTool", {
                title : "New Contact",
                tooltip : "Create New Contact",
                icon : "images/Contact_regular.png",
                iconHover : "images/Contact_hover.png",
                content : [ new sap.ui.commons.TextView({
                    text : "Here could be a contact sheet."
                }) ],
                buttons : [ new sap.ui.commons.Button("cancelContactButton", {
                    text : "Cancel",
                    press : function(oEvent) {
                        sap.ui.getCore().byId("contactTool").close();
                    }
                }) ]
            }) ],
            headerItems : [ new sap.ui.commons.TextView({
                text : "{i18n>shell.headerItems.username}",
                tooltip : "{i18n>shell.headerItems.username}"
            }), new sap.ui.commons.Button({
                text : "{i18n>shell.headerItems.personalize}",
                tooltip : "{i18n>shell.headerItems.personalize}",
                press : function(oEvent) {
                    alert("Here you could open an personalize dialog");
                }
            }), new sap.ui.commons.MenuButton({
                text : "{i18n>shell.headerItems.help}",
                tooltip : "{i18n>shell.headerItems.help.tooltip}",
                menu : new sap.ui.commons.Menu("menu1", {
                    items : [ new sap.ui.commons.MenuItem("menuitem1", {
                        text : "{i18n>shell.headerItems.help}"
                    }), new sap.ui.commons.MenuItem("menuitem2", {
                        text : "{i18n>shell.headerItems.help.reportIncident}"
                    }), new sap.ui.commons.MenuItem("menuitem3", {
                        text : "{i18n>shell.headerItems.help.about}"
                    }) ]
                })
            }) ],
            worksetItemSelected : function(oEvent) {
                var sSelectedId, oHashChanger;
                sSelectedId  = oEvent.getParameter("id");
                oHashChanger = sap.ui.core.routing.HashChanger.getInstance();
                oHashChanger.setHash(oRouter.getURL("_" + sSelectedId));
            },
            paneBarItemSelected : function(oEvent) {
                var sKey = oEvent.getParameter("key");
                var oShell = oEvent.oSource;
                switch (sKey) {
                   case "pi_date":
                        var oDate = new Date();
                        oShell.setPaneContent(new sap.ui.commons.TextView({
                            text : oDate.toLocaleString()
                        }), true);
                        break;
                    case "pi_browser":
                        oShell.setPaneContent(new sap.ui.commons.TextView({
                            text : "You browser provides the following information:\n" + navigator.userAgent
                        }), true);
                        break;
                    default:
                        break;
                    }
                },
                logout : function() {
                    alert(app.i18n('shell.headerItems.logout.pressedMessage'));
                },
                search : function(oEvent) {
                    alert(app.i18n('shell.search.triggeredMessage', [oEvent.getParameter("text")]));
                },
                feedSubmit : function(oEvent) {
                    alert(app.i18n('shell.feedSubmit', [oEvent.getParameter("text")]));
                },
                paneClosed : function(oEvent) {
                    alert(app.i18n('shell.paneCLosed', [oEvent.getParameter("id")]));
                }
            });

            //ROUTING
            oRouter = new sap.ui.core.routing.Router([
                {
                    pattern: [":?query:", "/:?query:"],
                    name: "_WI_home",
                    view: "nabisoft.modules.Home",
                    viewType: sap.ui.core.mvc.ViewType.XML,
                    targetControl: "demoShell",
                    targetAggregation: "content",
                    clearTarget: true,
                    callback: function(oRoute, oArguments) {
                        oShell.setSelectedWorksetItem("WI_home");
                    }
                }, {
                    pattern: ["examples:?query:"],
                    name: "_WI_EXMPLS",
                    view: "nabisoft.modules.Examples",
                    viewType: sap.ui.core.mvc.ViewType.XML,
                    targetControl: "demoShell",
                    targetAggregation: "content",
                    clearTarget: true,
                    callback: function(oRoute, oArguments) {
                        oShell.setSelectedWorksetItem("WI_EXMPLS");
                    }
                }, /*{      //we will never get this here
                    pattern: "subitems/one",
                    name: "_WI_1",
                    view: "nabisoft.modules.subitems.One",
                    viewType: sap.ui.core.mvc.ViewType.XML,
                    targetControl: "demoShell",
                    targetAggregation: "content",
                    clearTarget: true,
                    callback: function(oRoute, oArguments) {
                        oShell.setSelectedWorksetItem("WI_1_1");
                    }
                },*/ {
                    pattern: ["subitems/one:?query:", "subitems:?query:"],  //order is important
                    name: "_WI_1_1",
                    view: "nabisoft.modules.subitems.One",
                    viewType: sap.ui.core.mvc.ViewType.XML,
                    targetControl: "demoShell",
                    targetAggregation: "content",
                    clearTarget: true,
                    callback: function(oRoute, oArguments) {
                        oShell.setSelectedWorksetItem("WI_1_1");
                    }
                }, {
                    pattern: "subitems/two:?query:",
                    name: "_WI_1_2",
                    view: "nabisoft.modules.subitems.Two",
                    viewType: sap.ui.core.mvc.ViewType.XML,
                    targetControl: "demoShell",
                    targetAggregation: "content",
                    clearTarget: true,
                    callback: function(oRoute, oArguments) {
                        oShell.setSelectedWorksetItem("WI_1_2");
                    }
                }, {
                    pattern: "subitems/three:?query:",
                    name: "_WI_1_3",
                    view: "nabisoft.modules.subitems.Three",
                    viewType: sap.ui.core.mvc.ViewType.XML,
                    targetControl: "demoShell",
                    targetAggregation: "content",
                    clearTarget: true,
                    callback: function(oRoute, oArguments) {
                        oShell.setSelectedWorksetItem("WI_1_3");
                    }
                }
            ]);
            oRouter.initialize();

            return oShell;
    }

});

5. Implementing XML Views referenced by Router configuration

In the Router configuration (see previous step) we've configured five routes. For each of them we need to provide the corresponding View and Controller. The code is pretty much straightforward, so here is the code for each View/Controller pair (see below).

As you can see below, each XML View displays its name by using a TextView control. Additionally, I added a simple HTML Div element to show you how easy it is to use HTML code in your XML Views (you could also change the namespaces to make HTML the default namespace). Furthermore, the Home.view.xml uses a custom control which we will implement in the next step. To use our custom control I have added xmlns:nabi="nabisoft.control", which allows us to simply use <nabi:D3Circle radius="40"/> in the XML View. Our custom control uses D3.js to draw a very simple green SVG circle and it has only one property (radius).

./modules/Home.controller.js
sap.ui.controller("nabisoft.modules.Home", {
    onInit: function() {},
    onBeforeRendering: function() {},
    onAfterRendering: function() {},
    onExit: function() { }
});
./modules/Home.view.xml
<core:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.ui.commons"
    xmlns:nabi="nabisoft.control"
    controllerName="nabisoft.modules.Home" xmlns:html="http://www.w3.org/1999/xhtml">

    <TextView text="Home"/>
    <html:div>My Div</html:div>
    <nabi:D3Circle radius="40"/>    <!-- custom control -->

</core:View>
./modules/Examples.controller.js
sap.ui.controller("nabisoft.modules.Examples", {
    onInit: function() {},
    onBeforeRendering: function() {},
    onAfterRendering: function() {},
    onExit: function() { }
});
./modules/Examples.view.xml
<core:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.ui.commons"
    controllerName="nabisoft.modules.Examples" xmlns:html="http://www.w3.org/1999/xhtml">

    <TextView text="Examples"/>
    <html:div>My Div</html:div>

</core:View>
./modules/subitems/One.controller.js
sap.ui.controller("nabisoft.modules.subitems.One", {
    onInit: function() {},
    onBeforeRendering: function() {},
    onAfterRendering: function() {},
    onExit: function() { }
});
./modules/subitems/One.view.xml
<core:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.ui.commons"
    controllerName="nabisoft.modules.subitems.One" xmlns:html="http://www.w3.org/1999/xhtml">

    <TextView text="Subitem One"/>
    <html:div>My Div</html:div>

</core:View>
./modules/subitems/Two.controller.js
sap.ui.controller("nabisoft.modules.subitems.Two", {
    onInit: function() {},
    onBeforeRendering: function() {},
    onAfterRendering: function() {},
    onExit: function() { }
});
./modules/subitems/Two.view.xml
<core:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.ui.commons"
    controllerName="nabisoft.modules.subitems.Two" xmlns:html="http://www.w3.org/1999/xhtml">

    <TextView text="Subitem Two"/>
    <html:div>My Div</html:div>

</core:View>
./modules/subitems/Three.controller.js
sap.ui.controller("nabisoft.modules.subitems.Three", {
    onInit: function() {},
    onBeforeRendering: function() {},
    onAfterRendering: function() {},
    onExit: function() { }
});
./modules/subitems/Three.view.xml
<core:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.ui.commons"
    controllerName="nabisoft.modules.subitems.Three" xmlns:html="http://www.w3.org/1999/xhtml">

    <TextView text="Subitem Three"/>
    <html:div>My Div</html:div>

</core:View>

6. Implementing our custom control (D3Circle)

In our Home.view.xml we use nabisoft.control.D3Circle to display a simple green SVG circle. As you can see below our D3Circle control has exactly one property (radius) of type int with a default value (50).

Please note jQuery.sap.require("nabisoft.lib.ext.d3") and jQuery.sap.includeStyleSheet("js/control/D3Circle.css") is used to require/load a dependency to our D3.js and to include the CSS file that belongs to our control. Also note that the renderer keeps things very simple and that the D3.js magic happens in the onAfterRendering event. This is a common pattern you need to use in case you want to execute code that changes the DOM elements that belong to your control. Also keep in mind that our D3.js code is very simple and that it is not really using the powerful advantages and features that D3.js offers. Below you can see the code of our nabisoft.control.D3Circle control and its CSS file:

./js/control/D3Circle.js
;(function () {
    "use strict";

    jQuery.sap.declare('nabisoft.control.D3Circle');

    jQuery.sap.require("nabisoft.lib.ext.d3");
    jQuery.sap.includeStyleSheet("js/control/D3Circle.css");

    sap.ui.core.Control.extend("nabisoft.control.D3Circle", {

        metadata: {
            properties : {
                radius   : {type : "int", defaultValue: 50}
            },
            //defaultAggregation : "...",
            aggregations : { },
            associations : { },
            events       : { }
        },
        
        init : function(){ },

        onAfterRendering: function (oEvent){
            var jqContent, svg, radius;
            
            radius = this.getRadius();
            if (radius <10){
                radius = 10;
            }
            
            //HINT: jQuery(this.getDomRef()) and this.$() is equal - it gives you the jQuery object for this control's DOM element
            svg = d3.select(this.getDomRef()).append("svg")
                        .attr({
                            "class" : "nabiD3CircleSvg",    
                            "width" : 500,
                            "height": 500
                         });
            svg.append("circle").attr({
                cx : 250,
                cy : 100,
                r  : radius
            });

        },

        renderer : {

            render : function(rm, oControl) {
                rm.write("<div");
                rm.writeControlData(oControl);
                rm.addClass("nabiD3Circle");
                rm.writeClasses();
                rm.write(">");
                rm.write("</div>");
            }
        }
    });

}());
./js/control/D3Circle.css
.nabiD3Circle{
    margin: 15px 0;    
}

svg.nabiD3CircleSvg {

}

svg.nabiD3CircleSvg circle{
    fill         : green;
    stroke       : rgba(0,255,0,0.25);
    stroke-width : 10;
}

7. Adding external/3rd party libraries (D3.js)

From time to time you want to use external libraries in your SAPUI5 application. D3.js is a good example, a Javascript library that allows you to do amazing things. For illustration purposes we have created a custom control in the previous step which uses D3.js and now we need to make D3.js available to our application. Of course, we want to do this the SAPUI5 way. For this purpose we create a file ./js/lib/ext/d3.js, then we add a line jQuery.sap.declare('nabisoft.lib.ext.d3'); and finally we paste the official D3.js code - that's it (see code below). After that we can easily require D3.js in any of our controls (or controllers) by simply calling jQuery.sap.require("nabisoft.lib.ext.d3");. That's exactly what we want - it's the SAPUI5 way of providing and referencing 3rd party libraries.

./js/lib/ext/d3.js
/**
 * Latest D3 from http://d3js.org/d3.v3.js or http://d3js.org/d3.v3.min.js  
 */
jQuery.sap.declare('nabisoft.lib.ext.d3');
//complete code from http://d3js.org/d3.v3.js follows right here
//... 

8. Live Demo

This tutorial has illustrated how to modularize a simple SAPUI5 (Shell) application. We have used multiple XML Views and one JS View. We have also seen how to make use of the i18n and routing (deep linkinng) features of SAPUI5. Furthermore, we have created a simple custom control that uses D3.js, an external and open source library which we have also packaged into our application the SAPUI5 way. The custom control is used in one of our XML Views to demonstrate how to reference custom controls in XML Views.

Comments
  • If you like this tutorial help us to maintain it:
  • And don't forget to let others know about it:
Thanks,
posted by Duncan
Thu Oct 22 11:46:49 UTC 2015
This is an insightful showcase on what you can do with openui5
Please Provide Source Code for this
posted by Mallikarjuna B
Wed Sep 23 14:14:55 UTC 2015
Hello Team , 

I have done all this things in my eclipse work space but i am facing many issues while running this demo , can you please let me know how can i go head on this m, if it possible can you please share the code. Heartful thanks for this great help.


Regards
Mallikarjuna B.
Thanks and download ZIP
posted by Remo
Mon Jul 20 11:44:35 UTC 2015
Great tutorial, much appreciated!
There's a small typo "libs" vs. "lib", but beside that everything worked fine.

@Marc: Here's my code http://www.ortic.com/openui.zip
Sourcecode
posted by Marc
Wed Jun 24 15:32:04 UTC 2015
Hi, 

great tutorial!
Is it possible to get access to the complete source code?

Cheers,
Marc