Show TOC

Step 3: Navigation and RoutingLocate this document in the navigation structure

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.

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 : [
						{
							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 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 component’s 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;
	}
    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) {
		var oView = this.getView();
		oView.bindElement(sProductPath);

		//Check if the data is already on the client
		if(!oView.getModel().getData(sProductPath)) {

			// Check that the product specified actually was found.
			oView.getElementBinding().attachEventOnce("dataReceived", jQuery.proxy(function() {
				var oData = oView.getModel().getData(sProductPath);
				if (!oData) {
					this.showEmptyView();
					this.fireDetailNotFound();
				} else {
					this.fireDetailChanged(sProductPath);
				}
			}, this));

		} else {
			this.fireDetailChanged(sProductPath);
		}

	},
	},

Here, on a product route match, we set the Detail view's binding context to the specific product context that was selected in the URL ("Products(6)").

We should also deal with the situation where the Product with the ID specified does not exist, by telling the user the Product wasn't found. How do we do this? Rather than just present empty bindings, we check whether the model has already loaded the data (maybe the user already has viewed this detail). If we don’t find data locally, a request will be send by the binding. We can set a handler to be fired on a dataReceived event relating to the element binding. On that event, we can then check the actual data in the model to make sure it's been possible to retrieve it. If not, we can navigate the user to a 'not found' display. We also tell the master that the notFound view was shown and the tab was selected. So we can clear the selection if there was no entry found. Also we want to write the correct tab in the url. Since the master is doing this, we have to inform it.

We also ensure that the binding of the sap.m.IconTabFilters are set correctly. Finally, we make sure that the pre-selected tab in the sap.m.IconTabBar is the one that was specified in the URL (the optional last part of the pattern, denoted by :tab:), defaulting to the supplier tab if none was specified.

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:

This time, however, we see a slightly different error in the console:

Uncaught Error: resource sap/ui/demo/tdg/view/App.view.xml could not be loaded from
        ./view/App.view.xml. Check for 'file not found' or parse errors.

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.