Files
gh-secondsky-sap-skills-ski…/references/routing-navigation.md
2025-11-30 08:55:36 +08:00

726 lines
17 KiB
Markdown

# SAPUI5 Routing & Navigation
**Source**: Official SAP SAPUI5 Documentation
**Documentation**: [https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials](https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials)
**Last Updated**: 2025-11-21
---
## Overview
SAPUI5 implements hash-based routing for single-page applications, enabling deep linking, browser history support, and seamless navigation without page reloads.
**Key Benefits**:
- Browser back/forward button support
- Bookmarkable URLs (deep linking)
- State preservation through URL parameters
- SEO-friendly with hash-based navigation
- Mobile back button handling
---
## Core Concepts
### Hash-Based Navigation
SAPUI5 uses URL hash to manage application state:
```
[https://myapp.com/index.html#/products/123?tab=details](https://myapp.com/index.html#/products/123?tab=details)
^^^^^^^^^^^^^^^^^^^^^^^^^
Hash
```
**Components**:
- **Pattern**: `/products/{productId}`
- **Parameters**: `{productId}` = `123`
- **Query**: `?tab=details`
### Routes
Routes match hash patterns and trigger handlers:
```javascript
{
"pattern": "products/{productId}",
"name": "productDetail",
"target": "productDetail"
}
```
**When matched**: Loads target view and passes parameters.
### Targets
Targets define what to display and where:
```javascript
{
"productDetail": {
"viewName": "ProductDetail",
"viewLevel": 2,
"viewId": "productDetail",
"controlId": "app",
"controlAggregation": "pages"
}
}
```
---
## Configuration
### manifest.json Routing Configuration
**Complete Example**:
```json
{
"sap.ui5": {
"routing": {
"config": {
"routerClass": "sap.m.routing.Router",
"type": "View",
"viewType": "XML",
"path": "my.app.view",
"controlId": "app",
"controlAggregation": "pages",
"transition": "slide",
"bypassed": {
"target": "notFound"
},
"async": true
},
"routes": [
{
"pattern": "",
"name": "home",
"target": "home"
},
{
"pattern": "products",
"name": "productList",
"target": "productList"
},
{
"pattern": "products/{productId}",
"name": "productDetail",
"target": ["productList", "productDetail"]
},
{
"pattern": "products/{productId}/items/{itemId}",
"name": "itemDetail",
"target": ["productList", "productDetail", "itemDetail"]
}
],
"targets": {
"home": {
"viewName": "Home",
"viewId": "home",
"viewLevel": 1
},
"productList": {
"viewName": "ProductList",
"viewId": "productList",
"viewLevel": 1
},
"productDetail": {
"viewName": "ProductDetail",
"viewId": "productDetail",
"viewLevel": 2
},
"itemDetail": {
"viewName": "ItemDetail",
"viewId": "itemDetail",
"viewLevel": 3
},
"notFound": {
"viewName": "NotFound",
"viewId": "notFound"
}
}
}
}
}
```
### Configuration Options
**Router Config**:
- `routerClass`: Router implementation (sap.m.routing.Router, sap.f.routing.Router)
- `type`: View type (View, Component)
- `viewType`: View format (XML, JSON, JS, HTML)
- `path`: View namespace prefix
- `controlId`: Container control ID
- `controlAggregation`: Aggregation to add views to
- `transition`: Animation (slide, show, flip, fade)
- `async`: Asynchronous view loading (always use true)
- `bypassed`: Target for unmatched routes
**Route Properties**:
- `pattern`: URL pattern to match
- `name`: Route name (for navigation)
- `target`: Single target or array of targets
- `subroutes`: Nested routes (deprecated, use multiple targets)
- `greedy`: Match even if longer patterns exist
**Target Properties**:
- `viewName`: View name (without path)
- `viewId`: View instance ID
- `viewLevel`: Hierarchy level (for transitions)
- `controlId`: Parent control ID
- `controlAggregation`: Parent aggregation
- `clearControlAggregation`: Clear before adding
---
## Initializing Router
### In Component.js
```javascript
sap.ui.define([
"sap/ui/core/UIComponent"
], function(UIComponent) {
"use strict";
return UIComponent.extend("my.app.Component", {
metadata: {
manifest: "json"
},
init: function() {
// Call parent init
UIComponent.prototype.init.apply(this, arguments);
// Initialize router
this.getRouter().initialize();
}
});
});
```
**Important**: Always call `getRouter().initialize()` in component's init method.
---
## Navigation
### Programmatic Navigation
**Navigate to Route**:
```javascript
// Simple navigation
this.getOwnerComponent().getRouter().navTo("productList");
// With parameters
this.getOwnerComponent().getRouter().navTo("productDetail", {
productId: "123"
});
// With query parameters
this.getOwnerComponent().getRouter().navTo("productDetail", {
productId: "123"
}, {
query: {
tab: "details",
mode: "edit"
}
});
// Replace history (no back button)
this.getOwnerComponent().getRouter().navTo("home", {}, {}, true);
```
**Navigate Back**:
```javascript
// Browser back
window.history.go(-1);
// Or use NavContainer
var oNavContainer = this.byId("app");
oNavContainer.back();
```
### Link in XML View
**sap.m.Link**:
```xml
<Link
text="View Product"
href="{
parts: ['productId'],
formatter: '.formatProductLink'
}"/>
```
**Controller**:
```javascript
formatProductLink: function(sProductId) {
var oRouter = this.getOwnerComponent().getRouter();
return oRouter.getURL("productDetail", {
productId: sProductId
});
}
```
---
## Route Matching
### Pattern Matched Event
**Attach in Controller**:
```javascript
sap.ui.define([
"sap/ui/core/mvc/Controller"
], function(Controller) {
"use strict";
return Controller.extend("my.app.controller.ProductDetail", {
onInit: function() {
var oRouter = this.getOwnerComponent().getRouter();
oRouter.getRoute("productDetail").attachPatternMatched(this._onPatternMatched, this);
},
_onPatternMatched: function(oEvent) {
var mArguments = oEvent.getParameter("arguments");
var sProductId = mArguments.productId;
// Load product data
this._loadProduct(sProductId);
// Handle query parameters
var mQuery = oEvent.getParameter("arguments")["?query"];
if (mQuery && mQuery.tab) {
this._selectTab(mQuery.tab);
}
},
_loadProduct: function(sProductId) {
var sPath = "/Products('" + sProductId + "')";
this.getView().bindElement({
path: sPath,
events: {
dataRequested: function() {
this.getView().setBusy(true);
}.bind(this),
dataReceived: function() {
this.getView().setBusy(false);
}.bind(this)
}
});
},
_selectTab: function(sTabKey) {
this.byId("iconTabBar").setSelectedKey(sTabKey);
}
});
});
```
### Route Matched Event
Match any route:
```javascript
oRouter.attachRouteMatched(function(oEvent) {
var sRouteName = oEvent.getParameter("name");
console.log("Route matched: " + sRouteName);
});
```
### Bypassed Event
No route matched:
```javascript
oRouter.attachBypassed(function(oEvent) {
var sHash = oEvent.getParameter("hash");
console.log("No route matched: " + sHash);
});
```
---
## Route Parameters
### Mandatory Parameters
```javascript
// Route pattern
"products/{productId}"
// Navigation
this.getRouter().navTo("productDetail", {
productId: "123" // Required
});
// Access in controller
var sProductId = mArguments.productId;
```
### Optional Parameters
```javascript
// Route pattern with :optional: prefix
"products/:productId:"
// Navigation (parameter optional)
this.getRouter().navTo("productList", {
// productId can be omitted
});
// Or with parameter
this.getRouter().navTo("productList", {
productId: "123"
});
```
### Query Parameters
```javascript
// Not in pattern, passed via options
this.getRouter().navTo("productDetail", {
productId: "123"
}, {
query: {
tab: "specifications",
mode: "edit",
highlight: "true"
}
});
// Access in controller
var mQuery = oEvent.getParameter("arguments")["?query"];
var sTab = mQuery.tab; // "specifications"
var sMode = mQuery.mode; // "edit"
var bHighlight = mQuery.highlight === "true";
```
### Rest Parameters
Capture remaining path:
```javascript
// Pattern with rest parameter
"files/{path*}"
// Matches
"/files/documents/reports/2025.pdf"
// Access
var sPath = mArguments.path; // "documents/reports/2025.pdf"
```
---
## Master-Detail Pattern
### Flexible Column Layout
**manifest.json**:
```json
{
"sap.ui5": {
"routing": {
"config": {
"routerClass": "sap.f.routing.Router",
"flexibleColumnLayout": {
"defaultTwoColumnLayoutType": "TwoColumnsMidExpanded",
"defaultThreeColumnLayoutType": "ThreeColumnsMidExpanded"
}
},
"routes": [
{
"pattern": "",
"name": "master",
"target": ["master"]
},
{
"pattern": "products/{productId}",
"name": "detail",
"target": ["master", "detail"]
},
{
"pattern": "products/{productId}/items/{itemId}",
"name": "detailDetail",
"target": ["master", "detail", "detailDetail"]
}
],
"targets": {
"master": {
"viewName": "Master",
"viewLevel": 1,
"controlAggregation": "beginColumnPages"
},
"detail": {
"viewName": "Detail",
"viewLevel": 2,
"controlAggregation": "midColumnPages"
},
"detailDetail": {
"viewName": "DetailDetail",
"viewLevel": 3,
"controlAggregation": "endColumnPages"
}
}
}
}
}
```
**Layout Management**:
```javascript
// Get FCL
var oFCL = this.byId("flexibleColumnLayout");
// Change layout
oFCL.setLayout("TwoColumnsMidExpanded");
// Available layouts
// - OneColumn
// - TwoColumnsBeginExpanded
// - TwoColumnsMidExpanded
// - ThreeColumnsMidExpanded
// - ThreeColumnsEndExpanded
// - ThreeColumnsMidExpandedEndHidden
// - ThreeColumnsBeginExpandedEndHidden
```
---
## Advanced Patterns
### Nested Routing
Use multiple targets instead of subroutes:
```javascript
// Bad (deprecated subroutes)
{
"pattern": "products",
"name": "products",
"subroutes": [...]
}
// Good (multiple targets)
{
"pattern": "products/{productId}",
"name": "productDetail",
"target": ["productList", "productDetail"]
}
```
### Greedy Routes
Match even when longer patterns exist:
```javascript
{
"pattern": "products/{productId}",
"name": "productDetail",
"greedy": true
}
```
### Title Propagation
Set page title based on route:
```javascript
// In controller
_onPatternMatched: function(oEvent) {
var sProductId = oEvent.getParameter("arguments").productId;
this.getView().bindElement({
path: "/Products('" + sProductId + "')",
events: {
dataReceived: function(oData) {
var sTitle = oData.getParameter("data").Name;
// Set page title
this.getOwnerComponent().getRouter().getTitleTarget("productDetail").setTitle(sTitle);
}.bind(this)
}
});
}
```
---
## Error Handling
### Not Found Page
**Configure in manifest.json**:
```json
{
"routing": {
"config": {
"bypassed": {
"target": "notFound"
}
},
"targets": {
"notFound": {
"viewName": "NotFound",
"viewId": "notFound"
}
}
}
}
```
**NotFound.view.xml**:
```xml
<mvc:View
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<MessagePage
title="{i18n>notFoundTitle}"
text="{i18n>notFoundText}"
description="{i18n>notFoundDescription}"
icon="sap-icon://documents"
showNavButton="true"
navButtonPress=".onNavBack"/>
</mvc:View>
```
### Object Not Found
Handle when object doesn't exist:
```javascript
_loadProduct: function(sProductId) {
this.getView().bindElement({
path: "/Products('" + sProductId + "')",
events: {
dataRequested: function() {
this.getView().setBusy(true);
}.bind(this),
dataReceived: function(oData) {
this.getView().setBusy(false);
// Check if data exists
if (!oData.getParameter("data")) {
this._showObjectNotFound();
}
}.bind(this)
}
});
},
_showObjectNotFound: function() {
this.getOwnerComponent().getRouter().getTargets().display("objectNotFound");
}
```
---
## Best Practices
### 1. Always Use Async
```json
{
"routing": {
"config": {
"async": true
}
}
}
```
### 2. Initialize in Component
```javascript
init: function() {
UIComponent.prototype.init.apply(this, arguments);
this.getRouter().initialize();
}
```
### 3. Use Pattern Matched
Don't use onBeforeRendering/onAfterRendering for navigation logic:
```javascript
// Good
onInit: function() {
this.getRouter().getRoute("detail").attachPatternMatched(this._onPatternMatched, this);
}
// Bad
onBeforeRendering: function() {
// Don't load data here
}
```
### 4. Clean Up Event Handlers
```javascript
onExit: function() {
this.getRouter().getRoute("detail").detachPatternMatched(this._onPatternMatched, this);
}
```
### 5. Use getURL for Links
```javascript
// Generate URL
var sURL = this.getRouter().getURL("productDetail", {
productId: "123"
});
// Use in href
oLink.setHref(sURL);
```
---
## Troubleshooting
### Issue: Router not initializing
**Check**:
1. Called `getRouter().initialize()` in Component?
2. manifest.json routing config correct?
3. Console errors?
### Issue: Route not matching
**Check**:
1. Pattern syntax correct?
2. Parameters match?
3. Route name used in navTo?
**Debug**:
```javascript
// Log all routes
var aRoutes = this.getRouter().getRoutes();
aRoutes.forEach(function(oRoute) {
console.log(oRoute.getPattern());
});
```
### Issue: View not displaying
**Check**:
1. Target viewName correct?
2. controlId exists?
3. controlAggregation correct?
**Debug**:
```javascript
// Attach route matched
this.getRouter().attachRouteMatched(function(oEvent) {
console.log("Route matched:", oEvent.getParameter("name"));
console.log("Arguments:", oEvent.getParameter("arguments"));
});
```
---
## Official Documentation
- **Routing and Navigation**: [https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials](https://github.com/SAP-docs/sapui5/tree/main/docs/04_Essentials) (search: routing)
- **API Reference - Router**: [https://sapui5.hana.ondemand.com/#/api/sap.ui.core.routing.Router](https://sapui5.hana.ondemand.com/#/api/sap.ui.core.routing.Router)
- **API Reference - Route**: [https://sapui5.hana.ondemand.com/#/api/sap.ui.core.routing.Route](https://sapui5.hana.ondemand.com/#/api/sap.ui.core.routing.Route)
- **Flexible Column Layout**: [https://sapui5.hana.ondemand.com/#/api/sap.f.FlexibleColumnLayout](https://sapui5.hana.ondemand.com/#/api/sap.f.FlexibleColumnLayout)
---
**Note**: This document covers routing and navigation in SAPUI5. For Fiori Elements routing, see the fiori-elements.md reference file.