Skip to content

Commit b11f00f

Browse files
committed
docs: enhance Fiori extension instructions with critical pitfalls for custom timeline navigation
1 parent 4e6f63b commit b11f00f

1 file changed

Lines changed: 188 additions & 127 deletions

File tree

packages/fiori-docs-embeddings/data_local/fiori_extension_instructions.md

Lines changed: 188 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -951,203 +951,264 @@ sap.ui.define([
951951

952952
--------------------------------
953953

954-
**TITLE**: Custom Page with Timeline Control Navigation from List Report Toolbar
954+
**TITLE**: Custom Page with Timeline Control Navigation from List Report Toolbar ✅ VERIFIED WORKING PATTERN
955955

956-
**INTRODUCTION**: This guide explains how to add a "Show Timeline" button to the List Report table toolbar that navigates to a custom page. The custom page displays all travel records in a Timeline control (sap.suite.ui.commons.Timeline) sorted ascending by BeginDate. The custom page uses the sap.fe.macros.Page building block, a sap.m.Panel, and the Timeline control. This pattern demonstrates how to implement outbound navigation to a fully custom view in a Fiori Elements application.
956+
**INTRODUCTION**: This guide explains how to add a "Show Timeline" button to the List Report table toolbar that navigates to a custom FPM page. The custom page displays all travel records in a `sap.suite.ui.commons.Timeline` control sorted ascending by `BeginDate`. The custom page uses `sap.fe.macros.Page` as the container, `sap.m.Panel` as a wrapper, and `sap.suite.ui.commons.Timeline` with inline `TimelineItem` binding. This pattern was verified end-to-end including mock server and live OData V4. Follow every critical pitfall — each one causes silent failures or runtime errors.
957957

958-
**TAGS**: custom page, timeline, navigation, toolbar button, List Report, sap.fe.macros.Page, Page building block, sap.m.Panel, sap.suite.ui.commons.Timeline, TimelineItem, controller extension, custom route, manifest routing, extension, building blocks, custom navigation
958+
Replace all placeholder names with your actual values:
959+
- `my.app.namespace` → your app namespace (e.g. `fin.test.rap.lr3`)
960+
- `EntitySet` → your OData entity set name (e.g. `Travel`)
961+
- `ListReportTarget` → your List Report routing target name
962+
- `TimelinePageTarget` → your timeline page routing target name
963+
- `TimelinePageRoute` → your timeline route name (e.g. `TravelTimeline`)
964+
965+
**TAGS**: custom page, timeline, navigation, toolbar button, List Report, sap.fe.macros.Page, Page building block, sap.m.Panel, sap.suite.ui.commons.Timeline, TimelineItem, controller extension, custom route, manifest routing, extension, building blocks, custom navigation, FPM, HashChanger, press handler, module ID
966+
967+
---
968+
969+
**CRITICAL PITFALL A — The manifest `press` handler for toolbar actions resolves a PLAIN `.js` module, NOT a `.controller.js` module**: When a custom toolbar action is declared in `manifest.json` under `controlConfiguration["@com.sap.vocabularies.UI.v1.LineItem"].actions` with `"press": "my.app.namespace.ext.controller.ListReportExt.onShowTimeline"`, the Fiori Elements framework resolves this by loading the AMD module `my/app/namespace/ext/controller/ListReportExt.js` (note: plain `.js`, NOT `ListReportExt.controller.js`). The `ListReportExt.controller.js` file is registered under a different module ID: `my/app/namespace/ext/controller/ListReportExt.controller`. Attempting to use only `ListReportExt.controller.js` causes `ModuleError: failed to load 'my/app/namespace/ext/controller/ListReportExt.js'` and the button click does nothing. **Fix**: Create a SEPARATE file `webapp/ext/controller/ListReportExt.js` (no `.controller.` in the filename) that exports a plain object with the handler function. This file coexists with `ListReportExt.controller.js`.
970+
971+
**CRITICAL PITFALL B — In a plain FPM module handler (`.js`, not `.controller.js`), `this` is the exported object — use `HashChanger` for navigation, NOT the router**: In a plain AMD module (the `.js` file described in Pitfall A), the exported handler function is called with `this` set to the exported object itself — there is no SAPUI5 component or controller context. `UIComponent.getRouterFor(this)` returns `undefined` because `this` is not a ManagedObject. Calling `this.base.getView().getController().getOwnerComponent().getRouter()` crashes with `TypeError: Cannot read properties of undefined`. **Fix**: Use `sap.ui.core.routing.HashChanger.getInstance().setHash("RoutePattern")` to navigate. This directly sets the browser hash (triggering the router's pattern matching) without needing a component or controller reference. Use the route PATTERN (not the route name) as the hash value. For example, if the route has `"pattern": "TravelTimeline:?query:"`, set the hash to `"TravelTimeline"` (omit the `:?query:` optional query suffix).
959972

960-
**STEP**: Add custom route and target for the Timeline page in manifest.json
973+
**CRITICAL PITFALL C — Route pattern must be unique**: The timeline route pattern must not conflict with any existing route pattern. In Fiori Elements apps, the List Report route typically uses `":?query:"` and the Object Page route uses `"EntitySet({key}):?query:"`. Use a distinct string prefix (e.g. `"TravelTimeline:?query:"`) that cannot match those patterns. Do NOT use a pattern like `"Travel({key}):?query:"` — it would conflict with the Object Page route.
974+
975+
**CRITICAL PITFALL D — MCP tools may overwrite `navigation.EntitySet.detail.route` when modifying manifest.json**: Some automated tooling (including MCP-based code generators) may regenerate the manifest routing section and reset `navigation.EntitySet.detail.route` from your custom timeline target back to the Object Page. After any automated manifest modification, verify that the `navigation` section still maps row clicks to `TravelObjectPage` (or whatever your Object Page target is), NOT to the timeline target. The timeline target should only be reachable via the toolbar button.
976+
977+
**CRITICAL PITFALL E — The FPM custom page target must use `sap.fe.core.fpm` component, NOT `sap.fe.templates.ListReport` or a plain View target**: When the timeline page target is registered in manifest.json, it must use `"name": "sap.fe.core.fpm"` as the component name (not a view type/name directly at the target level). The `viewName` goes inside `options.settings`. This is required for `sap.fe.macros.Page` to work as the root element of the view. A plain `"type": "View"` target does NOT initialise the FPM context needed by `macros:Page`.
978+
979+
**CRITICAL PITFALL F — `TimelineItem` controls inside a bound `Timeline` need an explicit `id` attribute when `flexEnabled: true`**: When `sap.ui5.flexEnabled: true` is set in manifest.json, all controls need stable IDs for SAPUI5 Flexibility. Add `id="timelineItemTemplate"` (or similar) to the `suite:TimelineItem` element inside the Timeline's content aggregation binding template.
980+
981+
---
961982

962-
**DESCRIPTION**: Add a new route and target in the manifest.json routing configuration to define the custom timeline page. The target references a custom XML view.
983+
**STEP 1**: Add the timeline route and FPM target to manifest.json routing
984+
985+
**DESCRIPTION**: Add a new route with a unique pattern and a new target using `sap.fe.core.fpm`. The target's `options.settings` must include `contextPath` (the OData entity set path) and `viewName` (the fully qualified view module name). The `navigation` section must keep the List Report row-click navigation pointing to the Object Page, NOT to the timeline target.
986+
987+
**FILE**: webapp/manifest.json
963988

964989
**LANGUAGE**: JSON
965990

966991
**CODE**:
967992
```JSON
968-
"routes": [
993+
"routing": {
994+
"routes": [
969995
{
970-
"pattern": ":?query:",
971-
"name": "TravelsList",
972-
"target": "TravelsList"
996+
"pattern": ":?query:",
997+
"name": "EntitySetList",
998+
"target": "EntitySetList"
973999
},
9741000
{
975-
"pattern": "TravelsTimeline:?query:",
976-
"name": "TravelsTimeline",
977-
"target": "TravelsTimeline"
1001+
"pattern": "EntitySet({key}):?query:",
1002+
"name": "EntitySetObjectPage",
1003+
"target": "EntitySetObjectPage"
1004+
},
1005+
{
1006+
"pattern": "TravelTimeline:?query:",
1007+
"name": "TravelTimelinePage",
1008+
"target": "TravelTimelinePage"
9781009
}
979-
],
980-
"targets": {
981-
"TravelsTimeline": {
982-
"type": "View",
983-
"id": "TravelsTimeline",
984-
"viewLevel": 1,
985-
"name": "com.sap.travel.travelmanagementapp.ext.view.TravelsTimeline",
986-
"viewType": "XML"
1010+
],
1011+
"targets": {
1012+
"EntitySetList": {
1013+
"type": "Component",
1014+
"id": "EntitySetList",
1015+
"name": "sap.fe.templates.ListReport",
1016+
"options": {
1017+
"settings": {
1018+
"entitySet": "EntitySet",
1019+
"navigation": {
1020+
"EntitySet": {
1021+
"detail": {
1022+
"route": "EntitySetObjectPage"
1023+
}
1024+
}
1025+
}
1026+
}
1027+
}
1028+
},
1029+
"EntitySetObjectPage": {
1030+
"type": "Component",
1031+
"id": "EntitySetObjectPage",
1032+
"name": "sap.fe.templates.ObjectPage",
1033+
"options": {
1034+
"settings": {
1035+
"entitySet": "EntitySet",
1036+
"navigation": {}
1037+
}
1038+
}
1039+
},
1040+
"TravelTimelinePage": {
1041+
"type": "Component",
1042+
"id": "TravelTimelinePage",
1043+
"name": "sap.fe.core.fpm",
1044+
"options": {
1045+
"settings": {
1046+
"contextPath": "/EntitySet",
1047+
"viewName": "my.app.namespace.ext.view.TravelTimeline"
1048+
}
1049+
}
9871050
}
1051+
}
9881052
}
9891053
```
9901054

1055+
**STEP 2**: Register the toolbar action in manifest.json
9911056

992-
**ADDITIONAL RELATED CODE BLOCKS**:
1057+
**DESCRIPTION**: Add the `showTimeline` action under `controlConfiguration["@com.sap.vocabularies.UI.v1.LineItem"].actions` in the List Report target settings. The `press` value must reference `my.app.namespace.ext.controller.ListReportExt.onShowTimeline` — where `ListReportExt` (without `.controller.`) is the plain module file created in Step 4 (see Pitfall A).
9931058

994-
**FILE**: manifest.json (toolbar action registration)
1059+
**FILE**: webapp/manifest.json (inside the List Report target `options.settings`)
9951060

9961061
**LANGUAGE**: JSON
9971062

9981063
**CODE**:
9991064
```JSON
10001065
"controlConfiguration": {
1001-
"@com.sap.vocabularies.UI.v1.LineItem": {
1002-
"actions": {
1003-
"ShowTimelineAction": {
1004-
"press": ".extension.com.sap.travel.travelmanagementapp.ext.controller.ListReportExtension.onShowTimeline",
1005-
"text": "Show Timeline",
1006-
"enabled": true,
1007-
"visible": true,
1008-
"position": {
1009-
"placement": "After",
1010-
"anchor": "StandardAction::Create"
1011-
}
1012-
}
1013-
}
1066+
"@com.sap.vocabularies.UI.v1.LineItem": {
1067+
"actions": {
1068+
"showTimeline": {
1069+
"press": "my.app.namespace.ext.controller.ListReportExt.onShowTimeline",
1070+
"text": "{i18n>showTimeline}",
1071+
"enabled": true,
1072+
"visible": true
1073+
}
10141074
}
1075+
}
10151076
}
10161077
```
10171078

1018-
**FILE**: ext/controller/ListReportExtension.controller.js (Timeline navigation handler)
1079+
**STEP 3**: Add `sap.suite.ui.commons` library dependency to manifest.json
1080+
1081+
**DESCRIPTION**: The Timeline control comes from `sap.suite.ui.commons`. Add it to `sap.ui5.dependencies.libs` with `"lazy": false` so it is loaded upfront.
1082+
1083+
**FILE**: webapp/manifest.json
1084+
1085+
**LANGUAGE**: JSON
1086+
1087+
**CODE**:
1088+
```JSON
1089+
"sap.ui5": {
1090+
"dependencies": {
1091+
"libs": {
1092+
"sap.m": {},
1093+
"sap.ui.core": {},
1094+
"sap.fe.templates": {},
1095+
"sap.suite.ui.commons": {
1096+
"lazy": false
1097+
}
1098+
}
1099+
}
1100+
}
1101+
```
1102+
1103+
**STEP 4**: Create the plain press-handler module `ListReportExt.js`
1104+
1105+
**DESCRIPTION**: This is a SEPARATE file from `ListReportExt.controller.js`. It exports a plain object (not a ControllerExtension) with the `onShowTimeline` method. FE resolves `my.app.namespace.ext.controller.ListReportExt.onShowTimeline` by loading `ListReportExt.js` (no `.controller.` in filename — see Pitfall A). Use `HashChanger.getInstance().setHash(...)` for navigation — do NOT use the router (see Pitfall B). The hash value is the route pattern prefix WITHOUT the `:?query:` optional part.
1106+
1107+
**FILE**: webapp/ext/controller/ListReportExt.js
10191108

10201109
**LANGUAGE**: JavaScript
10211110

10221111
**CODE**:
10231112
```JavaScript
1024-
sap.ui.define([
1025-
"sap/ui/core/mvc/ControllerExtension"
1026-
], function (ControllerExtension) {
1113+
sap.ui.define(["sap/ui/core/routing/HashChanger"], function (HashChanger) {
10271114
"use strict";
10281115

1029-
return ControllerExtension.extend(
1030-
"com.sap.travel.travelmanagementapp.ext.controller.ListReportExtension",
1031-
{
1032-
onShowTimeline: function () {
1033-
// IMPORTANT: use this.base.getView() in a ControllerExtension, NOT this.getView()
1034-
// Use getOwnerComponent().getRouter(), NOT getAppComponent().getRouter()
1035-
var oRouter = this.base.getView().getController().getOwnerComponent().getRouter();
1036-
oRouter.navTo("TravelsTimeline");
1037-
}
1116+
return {
1117+
// Called via press="my.app.namespace.ext.controller.ListReportExt.onShowTimeline"
1118+
// in manifest.json controlConfiguration actions.
1119+
// 'this' here is the exported object — NOT a controller or component instance (Pitfall B).
1120+
// Use HashChanger for navigation — router is not accessible without a component context.
1121+
onShowTimeline: function (oBindingContext, aSelectedContexts) {
1122+
// Set the hash to the route pattern prefix (omit the :?query: optional suffix)
1123+
// This triggers the router's pattern matching for the "TravelTimeline:?query:" route
1124+
HashChanger.getInstance().setHash("TravelTimeline");
10381125
}
1039-
);
1126+
};
10401127
});
10411128
```
10421129

1043-
**FILE**: ext/view/TravelsTimeline.view.xml
1130+
**STEP 5**: Create the timeline view
1131+
1132+
**DESCRIPTION**: The view uses `sap.fe.macros.Page` as the root element (required for FPM context — Pitfall E). The `macros:Page` wraps a `sap.m.Panel` which contains the `sap.suite.ui.commons.Timeline`. The Timeline uses an aggregation binding on `/EntitySet` with `$orderby: 'BeginDate asc'` to fetch records sorted ascending. The `suite:TimelineItem` template has an explicit `id` attribute (required when `flexEnabled: true` — Pitfall F). The `controllerName` is required and must point to a valid controller file.
1133+
1134+
**FILE**: webapp/ext/view/TravelTimeline.view.xml
10441135

10451136
**LANGUAGE**: XML
10461137

10471138
**CODE**:
10481139
```XML
10491140
<mvc:View
1141+
xmlns:core="sap.ui.core"
10501142
xmlns:mvc="sap.ui.core.mvc"
10511143
xmlns="sap.m"
10521144
xmlns:macros="sap.fe.macros"
1053-
xmlns:timeline="sap.suite.ui.commons"
1054-
controllerName="com.sap.travel.travelmanagementapp.ext.controller.TravelsTimeline"
1055-
displayBlock="true">
1056-
<macros:Page id="timelinePage" title="Travels Timeline">
1145+
xmlns:suite="sap.suite.ui.commons"
1146+
controllerName="my.app.namespace.ext.view.TravelTimeline">
1147+
<macros:Page
1148+
id="TravelTimelineMacrosPage"
1149+
title="{i18n>TravelTimelineTitle}"
1150+
description="{i18n>TravelTimelineDescription}"
1151+
avatarSrc="sap-icon://timeline">
10571152
<Panel
1058-
id="timelinePanel"
1059-
headerText="All Travels by Start Date"
1060-
expandable="false"
1061-
expanded="true"
1062-
width="100%">
1063-
<timeline:Timeline
1064-
id="travelsTimeline"
1065-
showIcons="false"
1153+
id="travelTimelinePanel"
1154+
headerText="{i18n>TravelTimelinePanelTitle}">
1155+
<suite:Timeline
1156+
id="travelTimeline"
1157+
sortOldestFirst="true"
10661158
showSearch="false"
1067-
enableScroll="true"
1068-
sort="Ascending"
1069-
sortOldestFirst="true">
1070-
</timeline:Timeline>
1159+
content="{
1160+
path: '/EntitySet',
1161+
parameters: {
1162+
$orderby: 'BeginDate asc'
1163+
}
1164+
}">
1165+
<suite:TimelineItem
1166+
id="travelTimelineItem"
1167+
dateTime="{BeginDate}"
1168+
title="{= 'Record #' + ${EntitySetID}}"
1169+
text="{Memo}"
1170+
userName="{CustomerName}" />
1171+
</suite:Timeline>
10711172
</Panel>
1072-
<footer>
1073-
<OverflowToolbar>
1074-
<ToolbarSpacer />
1075-
<Button text="Back" press=".onNavBack" />
1076-
</OverflowToolbar>
1077-
</footer>
10781173
</macros:Page>
10791174
</mvc:View>
10801175
```
10811176

1082-
**FILE**: ext/controller/TravelsTimeline.controller.js
1177+
**STEP 6**: Add i18n keys
10831178

1084-
**LANGUAGE**: JavaScript
1179+
**FILE**: webapp/i18n/i18n.properties
10851180

1086-
**CODE**:
1087-
```JavaScript
1088-
sap.ui.define([
1089-
"sap/ui/core/mvc/Controller",
1090-
"sap/suite/ui/commons/TimelineItem"
1091-
], function (Controller, TimelineItem) {
1092-
"use strict";
1181+
**LANGUAGE**: properties
10931182

1094-
return Controller.extend(
1095-
"com.sap.travel.travelmanagementapp.ext.controller.TravelsTimeline",
1096-
{
1097-
onInit: function () {
1098-
var oRouter = this.getOwnerComponent().getRouter();
1099-
oRouter.getRoute("TravelsTimeline").attachPatternMatched(this._onRouteMatched, this);
1100-
},
1183+
**CODE**:
1184+
```properties
1185+
#XBUT: Toolbar button label for Show Timeline action
1186+
showTimeline=Show Timeline
11011187

1102-
_onRouteMatched: function () {
1103-
this._loadTimelineData();
1104-
},
1188+
#XTIT: Title of the Travel Timeline custom page
1189+
TravelTimelineTitle=Travel Timeline
11051190

1106-
_loadTimelineData: function () {
1107-
var oModel = this.getOwnerComponent().getModel();
1108-
var oTimeline = this.byId("travelsTimeline");
1109-
1110-
oModel.bindList("/Travels", undefined, [
1111-
new sap.ui.model.Sorter("BeginDate", false)
1112-
]).requestContexts().then(function (aContexts) {
1113-
oTimeline.destroyContent();
1114-
aContexts.forEach(function (oContext) {
1115-
var oData = oContext.getObject();
1116-
var oItem = new TimelineItem({
1117-
dateTime: oData.BeginDate,
1118-
title: "Travel " + oData.TravelID,
1119-
text: oData.Description || "",
1120-
icon: "sap-icon://travel-expense"
1121-
});
1122-
oTimeline.addContent(oItem);
1123-
});
1124-
});
1125-
},
1191+
#XFLD: Description shown under the page title
1192+
TravelTimelineDescription=All Travels sorted by Start Date
11261193

1127-
onNavBack: function () {
1128-
var oRouter = this.getOwnerComponent().getRouter();
1129-
oRouter.navTo("TravelsList");
1130-
}
1131-
}
1132-
);
1133-
});
1194+
#XTIT: Header text of the panel wrapping the timeline
1195+
TravelTimelinePanelTitle=Travels Timeline
11341196
```
11351197

1136-
**FILE**: manifest.json (sap.suite.ui.commons library dependency)
1198+
**STEP 7**: Verify navigation still works for row clicks (Object Page)
1199+
1200+
**DESCRIPTION**: After completing all steps above, verify that clicking a table row in the List Report still navigates to the Object Page (not the timeline). In manifest.json, check that `navigation.EntitySet.detail.route` is set to the Object Page route name (e.g. `EntitySetObjectPage`), NOT to `TravelTimelinePage`. If any automated tooling was used on manifest.json, re-verify this (Pitfall D).
11371201

11381202
**LANGUAGE**: JSON
11391203

11401204
**CODE**:
11411205
```JSON
1142-
"sap.ui5": {
1143-
"dependencies": {
1144-
"libs": {
1145-
"sap.m": {},
1146-
"sap.ui.core": {},
1147-
"sap.fe.templates": {},
1148-
"sap.suite.ui.commons": {}
1149-
}
1206+
"navigation": {
1207+
"EntitySet": {
1208+
"detail": {
1209+
"route": "EntitySetObjectPage"
11501210
}
1211+
}
11511212
}
11521213
```
11531214

0 commit comments

Comments
 (0)