Skip to content

Commit 4e6f63b

Browse files
committed
docs: add critical pitfalls for custom popup data entry using macros:Field and JSON model
1 parent 91ac3fa commit 4e6f63b

1 file changed

Lines changed: 297 additions & 5 deletions

File tree

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

Lines changed: 297 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,34 @@ Replace all placeholder names with your actual values:
4545

4646
**CRITICAL PITFALL 14 — Must `return oDialog` from the `.then()` callback inside `Fragment.load().then()`**: When storing a Fragment.load Promise as `this._pRelatedEntityDialog`, the `.then()` callback where `oView.addDependent(oDialog)` is called MUST end with `return oDialog`. If omitted, the Promise resolves to `undefined` (the implicit return value). All subsequent calls like `this._pRelatedEntityDialog.then(function(oDialog) { oDialog.open(); })` will then receive `undefined` as `oDialog` — `oDialog.open()` silently fails or throws `TypeError: Cannot read properties of undefined`. This is the single most common silent failure in the Fragment.load Promise pattern. Always write: `.then(function(oDialog) { oView.addDependent(oDialog); return oDialog; })`. As a related convention: prefix all Promise-stored dialog references with `_p` (e.g. `_pRelatedEntityDialog`, `_pAgencyPopup`) to visually distinguish them from direct object references — this makes it immediately clear that `.then()` must be used to access the dialog.
4747

48+
**CRITICAL PITFALL 15 — `sap.fe.macros.Field` renders in display-only mode in a `Fragment.load()` dialog that is NOT bound to an OData context**: When a dialog loaded via `Fragment.load()` binds its fields to a JSON model (not an OData model), `sap.fe.macros.Field` building blocks render in display-only mode regardless of `editMode="Editable"`. The building block relies on the Fiori Elements OData model context to resolve and apply edit mode at runtime — without an OData binding context on the dialog or a parent view context, the field stays read-only and user input is not possible. **Fix**: For data-entry dialogs where field values are stored in a JSON model (not read from OData), replace `macros:Field` with standard `sap.m` controls: use `sap.m.Input` for string/text fields (TravelID, AgencyID, etc.) and `sap.m.DatePicker` for date fields. `sap.m.DatePicker` with `valueFormat="yyyy-MM-dd"` and `displayFormat="mediumDate"` provides locale-aware date formatting and a calendar dropdown picker out of the box — no OData context required. Note: `macros:Field` CAN work in dialogs that are properly `bindElement()`-bound to an OData entity path (as in the read-only popup pattern above) when the field is being read from OData, but NOT when the field value is bound only to a JSON model.
49+
50+
**CRITICAL PITFALL 16 — The correct manifest.json location for Object Page header toolbar custom actions is `content.header.actions` inside `options.settings`, NOT `controlConfiguration.headerActions`**: To add a custom button to the Object Page header toolbar, add it under `options.settings.content.header.actions` in the Object Page routing target configuration. Each action is a key under `actions` with `press`, `text`, `enabled`, and `visible` properties. The `press` value uses the `.extension.<namespace>.<ControllerName>.<methodName>` format (same as other FE-managed view handlers). Using `controlConfiguration.headerActions`, `controlConfiguration.content.header.actions`, or any other path will silently fail — no button appears in the header, no error is thrown. Correct structure:
51+
```JSON
52+
"TravelObjectPage": {
53+
"type": "Component",
54+
"id": "TravelObjectPage",
55+
"name": "sap.fe.templates.ObjectPage",
56+
"options": {
57+
"settings": {
58+
"entitySet": "Travel",
59+
"content": {
60+
"header": {
61+
"actions": {
62+
"customMyAction": {
63+
"press": ".extension.my.app.ext.controller.ObjectPageExt.onMyAction",
64+
"text": "{i18n>myActionLabel}",
65+
"enabled": true,
66+
"visible": true
67+
}
68+
}
69+
}
70+
}
71+
}
72+
}
73+
}
74+
```
75+
4876
---
4977

5078
**STEP 1**: Register the custom column in manifest.json
@@ -711,15 +739,15 @@ sap.ui.define([
711739

712740
--------------------------------
713741

714-
**TITLE**: Custom Popup for Data Entry using Field Building Block and JSON Model
742+
**TITLE**: ⚠️ BROKEN — Custom Popup for Data Entry using macros:Field and JSON Model (Wrong manifest path + fields render display-only)
715743

716-
**INTRODUCTION**: This guide explains how to add a button to the Object Page header toolbar that opens a custom popup dialog for data entry. The popup uses sap.fe.macros.Field building blocks bound to a local JSON model, allowing the user to input travel merge parameters. This pattern is useful for custom actions requiring user input before calling an OData action or performing programmatic operations.
744+
**INTRODUCTION**: ⚠️ THIS PATTERN HAS TWO CRITICAL BUGS and is documented here for error recognition only. Do not use it. See the verified working pattern below instead. Bug 1: The manifest.json action is registered under `controlConfiguration.headerActions` — this path does not exist and the button never appears. The correct path is `content.header.actions` inside `options.settings` (see Pitfall 16). Bug 2: The fragment uses `sap.fe.macros.Field` bound to a JSON model — these fields render in display-only mode because `macros:Field` requires an OData binding context to activate edit mode (see Pitfall 15). As a result all input fields are read-only and users cannot enter any data.
717745

718-
**TAGS**: custom popup, data entry, Field building block, sap.fe.macros.Field, object page, header toolbar, toolbar button, JSON model, controller extension, fragment, dialog, custom action, extension, building blocks
746+
**TAGS**: custom popup, data entry, Field building block, sap.fe.macros.Field, object page, header toolbar, toolbar button, JSON model, controller extension, fragment, dialog, custom action, extension, building blocks, BROKEN, DO NOT USE
719747

720748
**STEP**: Register the Object Page header action in manifest.json
721749

722-
**DESCRIPTION**: Add a custom action to the `controlConfiguration` for the Object Page header. The action triggers a controller extension method that opens the popup dialog.
750+
**DESCRIPTION**: ⚠️ WRONG PATH — `controlConfiguration.headerActions` does not work. See Pitfall 16 and the verified working pattern below for the correct `content.header.actions` path.
723751

724752
**LANGUAGE**: JSON
725753

@@ -1123,4 +1151,268 @@ sap.ui.define([
11231151
}
11241152
```
11251153

1126-
--------------------------------
1154+
--------------------------------
1155+
1156+
**TITLE**: Object Page Header Toolbar Button → Data Entry Popup with Standard Controls (Input, DatePicker) ✅ VERIFIED WORKING PATTERN
1157+
1158+
**INTRODUCTION**: This guide explains how to add a custom button to the Object Page header toolbar that opens a data-entry popup dialog. The popup uses standard `sap.m` controls — `sap.m.Input` for text/string fields and `sap.m.DatePicker` for date fields — bound to a local JSON model. This is the correct pattern when field values are stored in a JSON model (not OData). Do NOT use `sap.fe.macros.Field` for this use case — those building blocks render display-only when not backed by an OData binding context (see Pitfall 15). Do NOT register the action under `controlConfiguration.headerActions` — the correct path is `content.header.actions` (see Pitfall 16).
1159+
1160+
Replace all placeholder names with your actual values:
1161+
- `my.app` → your app namespace (e.g. `fin.test.rap.lr3`)
1162+
- `ObjectPageExt` → your Object Page controller extension name
1163+
- `MyEntityObjectPage` → your Object Page routing target name
1164+
- `MyEntity` → your OData entity set name
1165+
1166+
**TAGS**: custom popup, data entry, object page, header toolbar, toolbar button, JSON model, sap.m.Input, sap.m.DatePicker, DatePicker, date picker, controller extension, fragment, dialog, custom action, extension, standard controls, verified working
1167+
1168+
---
1169+
1170+
**STEP 1**: Register the header toolbar action in manifest.json
1171+
1172+
**DESCRIPTION**: Add the action under `options.settings.content.header.actions` in the Object Page routing target. This is the ONLY correct location — `controlConfiguration.headerActions` silently fails (Pitfall 16). The `press` handler uses the full `.extension.<namespace>.<ControllerName>.<methodName>` format because the Object Page view is FE-managed (Pitfall 2). Replace `my.app`, `ObjectPageExt`, `MyEntityObjectPage`, and `MyEntity` with your actual values.
1173+
1174+
**FILE**: webapp/manifest.json (inside the Object Page routing target)
1175+
1176+
**LANGUAGE**: JSON
1177+
1178+
**CODE**:
1179+
```JSON
1180+
"MyEntityObjectPage": {
1181+
"type": "Component",
1182+
"id": "MyEntityObjectPage",
1183+
"name": "sap.fe.templates.ObjectPage",
1184+
"options": {
1185+
"settings": {
1186+
"entitySet": "MyEntity",
1187+
"content": {
1188+
"header": {
1189+
"actions": {
1190+
"customDataEntryAction": {
1191+
"press": ".extension.my.app.ext.controller.ObjectPageExt.onOpenDataEntryPopup",
1192+
"text": "{i18n>dataEntryActionLabel}",
1193+
"enabled": true,
1194+
"visible": true
1195+
}
1196+
}
1197+
}
1198+
}
1199+
}
1200+
}
1201+
}
1202+
```
1203+
1204+
**STEP 2**: Register the Object Page controller extension in manifest.json
1205+
1206+
**LANGUAGE**: JSON
1207+
1208+
**CODE**:
1209+
```JSON
1210+
"extends": {
1211+
"extensions": {
1212+
"sap.ui.controllerExtensions": {
1213+
"sap.fe.templates.ObjectPage.ObjectPageController": {
1214+
"controllerName": "my.app.ext.controller.ObjectPageExt"
1215+
}
1216+
}
1217+
}
1218+
}
1219+
```
1220+
1221+
**STEP 3**: Create the popup dialog fragment
1222+
1223+
**DESCRIPTION**: Use `sap.m.Input` for string fields and `sap.m.DatePicker` for date fields. Key rules: (1) `sap.m.DatePicker` requires `valueFormat` matching the OData date format (`yyyy-MM-dd`) and `displayFormat="mediumDate"` for locale-aware user-facing formatting — this also activates the calendar dropdown picker. (2) All button press handlers use direct dot-prefix `.onXxx` — NOT `.extension.XXX.onXxx` — because the fragment is loaded via `Fragment.load()` with `controller: this` (Pitfall 2). (3) All controls in the dialog need stable `id` attributes because `flexEnabled: true` (Pitfall 3). (4) Use `xmlns:form="sap.ui.layout.form"` and `<form:SimpleForm>` with `<form:content>` aggregation for the form layout.
1224+
1225+
**FILE**: webapp/ext/fragment/DataEntryPopup.fragment.xml
1226+
1227+
**LANGUAGE**: XML
1228+
1229+
**CODE**:
1230+
```XML
1231+
<core:FragmentDefinition
1232+
xmlns="sap.m"
1233+
xmlns:core="sap.ui.core"
1234+
xmlns:form="sap.ui.layout.form">
1235+
<Dialog
1236+
id="dataEntryDialog"
1237+
title="{i18n>dataEntryDialogTitle}"
1238+
contentWidth="32rem"
1239+
resizable="true"
1240+
draggable="true">
1241+
<content>
1242+
<form:SimpleForm
1243+
id="dataEntryForm"
1244+
editable="true"
1245+
layout="ResponsiveGridLayout"
1246+
labelSpanXL="5"
1247+
labelSpanL="5"
1248+
labelSpanM="5"
1249+
labelSpanS="12"
1250+
columnsXL="1"
1251+
columnsL="1"
1252+
columnsM="1">
1253+
<form:content>
1254+
<!-- String / text fields: use sap.m.Input -->
1255+
<Label id="field1Label" text="{i18n>field1Label}" labelFor="field1Input" />
1256+
<Input
1257+
id="field1Input"
1258+
value="{entryModel>/Field1}"
1259+
placeholder="{i18n>field1Label}"
1260+
editable="true" />
1261+
1262+
<!-- Date fields: use sap.m.DatePicker -->
1263+
<!-- valueFormat must match the OData Edm.Date wire format (yyyy-MM-dd) -->
1264+
<!-- displayFormat="mediumDate" gives locale-aware display + calendar dropdown -->
1265+
<Label id="startDateLabel" text="{i18n>startDateLabel}" labelFor="startDatePicker" />
1266+
<DatePicker
1267+
id="startDatePicker"
1268+
value="{entryModel>/StartDate}"
1269+
valueFormat="yyyy-MM-dd"
1270+
displayFormat="mediumDate"
1271+
editable="true" />
1272+
1273+
<Label id="endDateLabel" text="{i18n>endDateLabel}" labelFor="endDatePicker" />
1274+
<DatePicker
1275+
id="endDatePicker"
1276+
value="{entryModel>/EndDate}"
1277+
valueFormat="yyyy-MM-dd"
1278+
displayFormat="mediumDate"
1279+
editable="true" />
1280+
</form:content>
1281+
</form:SimpleForm>
1282+
</content>
1283+
<beginButton>
1284+
<!-- MUST use direct .onConfirm — NOT .extension.XXX.onConfirm (Pitfall 2) -->
1285+
<Button
1286+
id="dataEntryConfirmBtn"
1287+
text="{i18n>confirmButton}"
1288+
type="Emphasized"
1289+
press=".onConfirmDataEntry" />
1290+
</beginButton>
1291+
<endButton>
1292+
<!-- MUST use direct .onCloseDataEntryPopup — NOT .extension.XXX.onCloseDataEntryPopup (Pitfall 2) -->
1293+
<Button
1294+
id="dataEntryCancelBtn"
1295+
text="{i18n>cancelButton}"
1296+
press=".onCloseDataEntryPopup" />
1297+
</endButton>
1298+
</Dialog>
1299+
</core:FragmentDefinition>
1300+
```
1301+
1302+
**STEP 4**: Create the controller extension
1303+
1304+
**DESCRIPTION**: Key rules: (1) Use `this.base.getView()` — NOT `this.getView()` — in a ControllerExtension (Pitfall 6). (2) Initialise the JSON model (`entryModel`) in `onInit` using `this.base.getView().setModel()`. (3) Use `Fragment.load()` with a unique id suffix to avoid control ID conflicts (Pitfall 8). (4) Always `return oDialog` from the `.then()` callback (Pitfall 14). (5) Guard `onCloseDataEntryPopup` with `if (this._pDataEntryPopup)` to avoid errors if called before the fragment is ever loaded. (6) Pre-populate fields from the current Object Page context inside `onOpenDataEntryPopup` using `oView.getBindingContext().getProperty("FieldName")`.
1305+
1306+
**FILE**: webapp/ext/controller/ObjectPageExt.controller.js
1307+
1308+
**LANGUAGE**: JavaScript
1309+
1310+
**CODE**:
1311+
```JavaScript
1312+
sap.ui.define([
1313+
"sap/ui/core/mvc/ControllerExtension",
1314+
"sap/ui/core/Fragment",
1315+
"sap/ui/model/json/JSONModel"
1316+
], function (ControllerExtension, Fragment, JSONModel) {
1317+
"use strict";
1318+
1319+
return ControllerExtension.extend(
1320+
// Replace with your actual controller extension name
1321+
"my.app.ext.controller.ObjectPageExt",
1322+
{
1323+
override: {
1324+
onInit: function () {
1325+
// IMPORTANT: use this.base.getView() — NOT this.getView() — in a ControllerExtension
1326+
var oView = this.base.getView();
1327+
1328+
// Initialise the JSON model for the data-entry popup
1329+
oView.setModel(
1330+
new JSONModel({
1331+
Field1: "",
1332+
StartDate: null, // null = no date pre-selected
1333+
EndDate: null
1334+
}),
1335+
"entryModel"
1336+
);
1337+
}
1338+
},
1339+
1340+
// Called via press=".extension.my.app.ext.controller.ObjectPageExt.onOpenDataEntryPopup"
1341+
// in the Object Page (FE-managed view) — full .extension.XXX path required (Pitfall 2)
1342+
onOpenDataEntryPopup: function () {
1343+
// IMPORTANT: use this.base.getView() — NOT this.getView() (Pitfall 6)
1344+
var oView = this.base.getView();
1345+
1346+
// Optional: pre-populate a field from the current Object Page context
1347+
var oContext = oView.getBindingContext();
1348+
if (oContext) {
1349+
oView.getModel("entryModel").setProperty("/Field1", oContext.getProperty("Field1"));
1350+
}
1351+
1352+
if (!this._pDataEntryPopup) {
1353+
this._pDataEntryPopup = Fragment.load({
1354+
// Always add a unique suffix to avoid control ID conflicts (Pitfall 8)
1355+
id: oView.getId() + "--dataEntryPopup",
1356+
// Replace with your actual fragment module path
1357+
name: "my.app.ext.fragment.DataEntryPopup",
1358+
// controller: this → press handlers in the fragment resolve directly against this instance
1359+
controller: this
1360+
}).then(function (oDialog) {
1361+
oView.addDependent(oDialog);
1362+
return oDialog; // MUST return oDialog — omitting this causes oDialog.open() to fail (Pitfall 14)
1363+
});
1364+
}
1365+
1366+
this._pDataEntryPopup.then(function (oDialog) {
1367+
oDialog.open();
1368+
});
1369+
},
1370+
1371+
// Called via press=".onConfirmDataEntry" in the fragment
1372+
// Direct dot-prefix is correct here — fragment is loaded with controller: this (Pitfall 2)
1373+
onConfirmDataEntry: function () {
1374+
var oEntryModel = this.base.getView().getModel("entryModel");
1375+
var oData = oEntryModel.getData();
1376+
// TODO: use oData.Field1, oData.StartDate, oData.EndDate
1377+
// e.g. call an OData action or update a property
1378+
this.onCloseDataEntryPopup();
1379+
},
1380+
1381+
// Called via press=".onCloseDataEntryPopup" in the fragment (Pitfall 2)
1382+
onCloseDataEntryPopup: function () {
1383+
if (this._pDataEntryPopup) {
1384+
this._pDataEntryPopup.then(function (oDialog) {
1385+
oDialog.close();
1386+
});
1387+
}
1388+
}
1389+
}
1390+
);
1391+
});
1392+
```
1393+
1394+
**STEP 5**: Add i18n keys
1395+
1396+
**FILE**: webapp/i18n/i18n.properties
1397+
1398+
**LANGUAGE**: properties
1399+
1400+
**CODE**:
1401+
```properties
1402+
#XBUT: Header toolbar button label
1403+
dataEntryActionLabel=Open Data Entry
1404+
1405+
#XTIT: Title of the data entry popup dialog
1406+
dataEntryDialogTitle=Data Entry
1407+
1408+
#XTIT: Field labels
1409+
field1Label=Field 1
1410+
startDateLabel=Start Date
1411+
endDateLabel=End Date
1412+
1413+
#XBUT: Button labels
1414+
confirmButton=Confirm
1415+
cancelButton=Cancel
1416+
```
1417+
1418+
--------------------------------

0 commit comments

Comments
 (0)