diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4aa8594f..44374e64 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -99,6 +99,14 @@ jobs: if: ${{ !cancelled() }} run: npm run check:examples + # every `client->` name on the site - in the prose and in the snippets + # check:examples cannot compile because they are not whole classes - + # against z2ui5_if_client at the release this site names. Four pages + # taught API that 1.143.0 had deleted, and nothing here was red. + - name: API names + if: ${{ !cancelled() }} + run: npm run check:api-names + # the Working Samples blocks: still generated from what the catalogue says # today, still pointing at classes that exist, and still pointed back at # from those classes. A link into another repository is the one kind that diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d4899afb..1b83e017 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -103,6 +103,9 @@ jobs: - name: ABAP examples if: ${{ !cancelled() }} run: npm run check:examples + - name: API names + if: ${{ !cancelled() }} + run: npm run check:api-names - name: sample links if: ${{ !cancelled() }} run: npm run check:samples diff --git a/AGENTS.md b/AGENTS.md index 163ea6aa..af7729cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,11 +24,11 @@ person reads the page. Do not put "as an AI, …" prose back into `docs/`. ## Build & verify — run before every commit ```bash -npm run check # test + check:version + docs:build + check:examples + check:samples +npm run check # test + check:version + docs:build + check:examples + check:api-names + check:samples ``` -A documentation repository has no compiler for its prose, but five things in it -are decidable, and all five are decided before a merge: +A documentation repository has no compiler for its prose, but six things in it +are decidable, and all six are decided before a merge: | | | |---|---| @@ -36,9 +36,10 @@ are decidable, and all five are decided before a merge: | `check:version` | the release number in the nav bar, the deprecations page and the changelog, against the newest release tag of the framework — this one goes stale without anybody touching this repository | | `docs:build` | a page that does not build is a page nobody can read | | `check:examples` | the ABAP in the fenced blocks, against the real framework: does it compile, and does the view it builds name controls and properties that exist on the UI5 floor this documentation targets | +| `check:api-names` | every `client->` name on the site — method, parameter, `cs_*` constant — against `z2ui5_if_client` at the release this site names, plus every `blob/main/` link into the framework's tree. `check:examples` compiles the fenced blocks that are whole CLASSES; this is the rest of the page: the sentence, the two-line snippet, the constant block a page reproduces, the source link. Four pages taught API that 1.143.0 had deleted and nothing was red | | `check:samples` | the **Working Samples** blocks, against [abap2UI5/samples](https://github.com/abap2UI5/samples) | -`.github/workflows/check.yml` runs the same five, in the same order. Keep the +`.github/workflows/check.yml` runs the same six, in the same order. Keep the two in step: a step that exists only in `package.json` is a step no pull request has to pass, which is how `npm test` — the pin added *because* the catalogue parser broke twice in silence — went a release without CI. @@ -48,7 +49,7 @@ clone it as a sibling. Without one it *skips* rather than fails, so verify the output says what you think it says. CI checks out `abap2UI5/samples@main` explicitly for this reason. -There used to be a sixth, `check:counts`, holding four figures on a +There used to be one more, `check:counts`, holding four figures on a `resources/samples.md` page against the catalogues themselves. That page is gone — the home page opens [the samples page](https://abap2ui5.github.io/samples/) directly and each catalogue introduces itself — and with it the only prose @@ -112,7 +113,7 @@ The code travels in the playground's URL fragment, read out of the rendered block at click time — so nothing is hosted here, and the example that runs is the text on the page rather than a copy of it. -**This is the sixth decidable thing in this repository and the only one CI +**This is the seventh decidable thing in this repository and the only one CI cannot decide.** Whether an example runs is a question only a playground can answer, and a playground is a three-minute build of another repository. So the rules in `docs/.vitepress/playground.mjs` are an approximation, they fail diff --git a/CLAUDE.md b/CLAUDE.md index e216a73f..cfcc4f3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md All project guidance lives in **[AGENTS.md](AGENTS.md)** — the single source of -truth for this repository (how the site is built, the five gates, the playground rule engine, and what may be written where). +truth for this repository (how the site is built, the six gates, the playground rule engine, and what may be written where). Read `AGENTS.md` before making any change. diff --git a/docs/advanced/extensibility/user_exits.md b/docs/advanced/extensibility/user_exits.md index a71523dd..2ddb97f4 100644 --- a/docs/advanced/extensibility/user_exits.md +++ b/docs/advanced/extensibility/user_exits.md @@ -3,7 +3,7 @@ outline: [2, 4] --- # User Exits -abap2UI5 offers predefined user exits for tweaking the standard behavior. The interface [`Z2UI5_IF_EXIT`](https://github.com/abap2UI5/abap2UI5/blob/main/src/02/z2ui5_if_exit.intf.abap) exposes the user exits. To use them on your system, build a class that implements the interface and its methods. The abap2UI5 class [`Z2UI5_CL_UI5_USER_EXIT`](https://github.com/abap2UI5/abap2UI5/blob/main/src/01/04/z2ui5_cl_ui5_user_exit.clas.abap) calls them dynamically. Put your class in a custom package — **not** in the abap2UI5 packages. +abap2UI5 offers predefined user exits for tweaking the standard behavior. The interface [`Z2UI5_IF_EXIT`](https://github.com/abap2UI5/abap2UI5/blob/main/src/99/z2ui5_if_exit.intf.abap) exposes the user exits. To use them on your system, build a class that implements the interface and its methods. The abap2UI5 class [`Z2UI5_CL_UI5_USER_EXIT`](https://github.com/abap2UI5/abap2UI5/blob/main/src/01/04/z2ui5_cl_ui5_user_exit.clas.abap) calls them dynamically. Put your class in a custom package — **not** in the abap2UI5 packages. The interface exposes two exit methods: - **`set_config_http_get`** — called on the initial HTTP GET request (page load). Use it to set frontend properties like the UI5 theme, the UI5 version, or the inline CSS. @@ -41,10 +41,14 @@ ENDCLASS. ``` ::: tip The interface is being renamed -`z2ui5_if_exit` becomes `z2ui5_if_ui5_exit`, following the framework's naming. -Both work — abap2UI5 looks up both interfaces, so an existing exit keeps -running — and the examples here move to the new name once it is in a release. -See [Deprecations](/resources/deprecations). +`z2ui5_if_exit` becomes +[`z2ui5_if_ui5_exit`](https://github.com/abap2UI5/abap2UI5/blob/main/src/02/z2ui5_if_ui5_exit.intf.abap), +following the framework's naming. Both work — abap2UI5 looks up both +interfaces, so an existing exit keeps running — and the examples here move to +the new name once it is in a release. On `main` the old interface has already +moved to the frozen `src/99` package, which is why the link above points +there; it still ships and is still called. See +[Deprecations](/resources/deprecations). ::: ::: warning The tab title is not set here diff --git a/docs/configuration/authorization.md b/docs/configuration/authorization.md index e1cc900a..9a2e6df4 100644 --- a/docs/configuration/authorization.md +++ b/docs/configuration/authorization.md @@ -30,7 +30,7 @@ CLASS z2ui5_cl_my_http_handler IMPLEMENTATION. ENDIF. " Call the abap2UI5 handler - z2ui5_cl_http_handler=>run( server ). + z2ui5_cl_ui5_http_handler=>run( server ). ENDMETHOD. ENDCLASS. @@ -61,7 +61,7 @@ CLASS z2ui5_cl_my_http_handler IMPLEMENTATION. ENDIF. " Call the abap2UI5 handler if authorized - z2ui5_cl_http_handler=>run( server ). + z2ui5_cl_ui5_http_handler=>run( server ). ENDMETHOD. ENDCLASS. ``` diff --git a/docs/configuration/logout.md b/docs/configuration/logout.md index e3e23774..b0617e2d 100644 --- a/docs/configuration/logout.md +++ b/docs/configuration/logout.md @@ -8,12 +8,12 @@ A logout button isn't a single action. Depending on how an abap2UI5 app was star ## The Logout Event Fire the event from any controller method to log the user off: ```abap -client->_event_client( client->cs_event-system_logout ). +client->follow_up_action( client->cs_event-system_logout ). ``` Optionally pass a same-origin URL as the first argument to control where the user lands afterwards. The default is `/sap/public/bc/icf/logoff`: ```abap -client->_event_client( +client->follow_up_action( val = client->cs_event-system_logout t_arg = VALUE #( ( `/sap/public/bsp/sap/system/logoff.htm` ) ) ). ``` @@ -25,7 +25,7 @@ abap2UI5 can be launched in three ways. Each one creates a different combination | Context | Typical URL | Fiori Launchpad shell? | |---|---|---| | Fiori Launchpad tile | `…/sap/bc/ui2/flp#Z2UI5-display` | yes | -| ICF HTTP handler (e.g. `Z2UI5_CL_HTTP_HANDLER`) | `…/sap/bc/` | no | +| ICF HTTP handler (e.g. `Z2UI5_CL_UI5_HTTP_HANDLER`) | `…/sap/bc/` | no | | BSP application (`Z2UI5`, `Z2UI5_V2`) | `…/sap/bc/bsp/sap/z2ui5/index.html` | no | ### Session Layers diff --git a/docs/configuration/security.md b/docs/configuration/security.md index 288348b3..82e1c579 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -23,7 +23,7 @@ The frontend is a Single-Page Application (SPA) built with SAPUI5 or OpenUI5. Th abap2UI5 never sends the app's business logic to the client. All business processes stay safely on the server, and sensitive data never reaches the frontend. ## Content-Security-Policy -To strengthen security, abap2UI5 uses a Content Security Policy (CSP) by default. CSP blocks attacks like cross-site scripting (XSS) and data injection by restricting which resources the browser can load. The default policy allows a fixed set of trusted sources — the SAP and OpenUI5 CDNs plus jsDelivr and cdnjs; the complete policy is shown below. It also carries hardening directives (`object-src 'none'`, `base-uri 'self'`, `frame-ancestors 'self'`) that block plugin content, pin `` to the app origin and forbid cross-origin framing. +To strengthen security, abap2UI5 uses a Content Security Policy (CSP) by default. CSP blocks attacks like cross-site scripting (XSS) and data injection by restricting which resources the browser can load. The default policy allows a fixed set of trusted sources — the SAP and OpenUI5 CDNs plus jsDelivr and cdnjs; the complete policy is shown below. It also carries two hardening directives — `object-src 'none'` and `base-uri 'self'` — that block plugin content and pin `` to the app origin. It deliberately carries **no** `frame-ancestors`: browsers ignore that directive in a `` CSP (and log a console warning about it), so cross-origin framing is forbidden by the real `X-Frame-Options` response header instead — see [Response headers](#response-headers) below. The default **does** contain `'unsafe-eval'`: the ui5loader of OpenUI5 `1.71` — the oldest supported release — still evaluates module source as a string, and without `'unsafe-eval'` a `1.71` bootstrap fails with a CSP `EvalError`. Modern UI5 releases load all modules without `eval()`, so if you pin a modern release you can tighten the policy — see [Hardening: Dropping `'unsafe-eval'`](#hardening-dropping-unsafe-eval) below. @@ -37,7 +37,7 @@ By default, abap2UI5 uses the CSP below (defined in `z2ui5_cl_ui5_user_exit`): openui5.hana.ondemand.com *.openui5.hana.ondemand.com sdk.openui5.org *.sdk.openui5.org cdn.jsdelivr.net *.cdn.jsdelivr.net cdnjs.cloudflare.com *.cdnjs.cloudflare.com; worker-src 'self' blob:; - object-src 'none'; base-uri 'self'; frame-ancestors 'self';"/> + object-src 'none'; base-uri 'self';"/> ``` ### Customizing the CSP @@ -78,7 +78,7 @@ METHOD z2ui5_if_exit~set_config_http_get. | cdn.jsdelivr.net *.cdn.jsdelivr.net | && | cdnjs.cloudflare.com *.cdnjs.cloudflare.com; | && |worker-src 'self' blob:; | && - |object-src 'none'; base-uri 'self'; frame-ancestors 'self'; "/>|. + |object-src 'none'; base-uri 'self'; "/>|. ENDMETHOD. ``` @@ -95,6 +95,34 @@ not an allowed source of script: default-src 'self' 'unsafe-inline' data: ui5.sa Only tighten the policy when every system you deploy to bootstraps a modern release. ::: +## Response headers +Not everything hardening can be done in a `` CSP is done there — a page +served over HTTP carries headers too, and some directives only work as one. +abap2UI5 sets these on every response, out of the box: + +| Header | Value | What it does | +|---|---|---| +| `X-Frame-Options` | `SAMEORIGIN` | forbids cross-origin framing. This, not the CSP, is where clickjacking protection lives: `frame-ancestors` in a `` CSP is ignored by browsers | +| `X-Content-Type-Options` | `nosniff` | the browser honours the declared content type instead of guessing one | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | a cross-origin request leaks the origin, never the path or query | +| `Permissions-Policy` | `geolocation=(self), microphone=(self), camera=(self), payment=(), usb=()` | the device APIs abap2UI5 offers stay available to the app itself; payment and USB are off | +| `cache-control` / `Pragma` / `Expires` | `no-cache, no-store, must-revalidate` / `no-cache` / `0` | the roundtrip responses carry application state — nothing about them may be cached | + +They live in `cs_config-t_security_header` and are set in the same +[user exit](/advanced/extensibility/user_exits) as the CSP, so an installation +behind a proxy that already sets one of them can drop or change it: + +```abap +METHOD z2ui5_if_exit~set_config_http_get. + + " keep everything the framework set, override one entry + DELETE cs_config-t_security_header WHERE n = `Referrer-Policy`. + APPEND VALUE #( n = `Referrer-Policy` v = `no-referrer` ) + TO cs_config-t_security_header. + +ENDMETHOD. +``` + ## Cross-Site Request Forgery (CSRF) Every state-changing request in abap2UI5 is a POST, so the framework ships its own CSRF defense instead of relying on a fronting SAP ICF/CSRF layer that may or may not be there. The check compares the host authority of the request's `Origin` (or `Referer`) header against the `Host` header — a cross-origin POST is rejected with an error response before any app logic runs. diff --git a/docs/cookbook/browser_interaction/url_handling.md b/docs/cookbook/browser_interaction/url_handling.md index 06bb9b7d..97ebc457 100644 --- a/docs/cookbook/browser_interaction/url_handling.md +++ b/docs/cookbook/browser_interaction/url_handling.md @@ -26,7 +26,7 @@ client->follow_up_action( ``` ### Browser History -Two client methods control the browser history from the backend: +One client method writes the browser history from the backend: Push a new history entry — the value is appended to the URL hash, so app state becomes bookmarkable and the browser back button steps through your pushed states: ```abap @@ -36,9 +36,15 @@ client->set_push_state( `&my-app-state=detail` ). There is no method that presses the browser's back button for you — the pushed states above are what the back button walks through, and leaving an app is [`nav_app_leave( )`](/cookbook/event_navigation/navigation), which -returns to the calling app rather than to the previous URL. +returns to the calling app rather than to the previous URL. To step back +through the pushed states from ABAP, hand the raw expression to +`follow_up_action( )`: -For a complete example, see sample `Z2UI5_CL_DEMO_APP_139`. +```abap +client->follow_up_action( |history.back()| ). +``` + +For a complete example, see sample `Z2UI5_CL_SMP_APP_322`. ::: tip Hash-based app routing For app-to-app navigation, the framework can own the URL hash itself: with [Routing](/cookbook/event_navigation/routing) enabled, each app gets a bookmarkable route and the browser Back/Forward buttons navigate the app stack — no manual push states needed. diff --git a/docs/cookbook/cheat_sheet.md b/docs/cookbook/cheat_sheet.md index 918bf30b..8ff32f22 100644 --- a/docs/cookbook/cheat_sheet.md +++ b/docs/cookbook/cheat_sheet.md @@ -10,8 +10,8 @@ A one-page recap of the rules that decide whether an abap2UI5 app works or misbe | Implement `z2ui5_if_app` and put everything in the single `main` method | It is the only entry point the framework calls — on the initial load *and* on every user interaction → [Life Cycle](/cookbook/event_navigation/life_cycle) | | Dispatch with one `IF` / `ELSEIF` chain over `check_on_init( )`, `check_on_navigated( )` and `check_on_event( )` | Each check answers for its own phase only; separate `IF` blocks let two branches run in the same roundtrip → [Life Cycle](/cookbook/event_navigation/life_cycle) | | Always call `view_display( )` in the `check_on_navigated( )` branch | After a called app returns via `nav_app_leave( )`, the browser still shows *its* view — without a re-display the user is left on a stale or blank screen → [Navigation](/cookbook/event_navigation/navigation) | -| Declare every attribute you bind in the `PUBLIC SECTION` | Binding works via dynamic `ASSIGN`; `PROTECTED` and `PRIVATE` attributes are silently ignored → [Binding](/cookbook/model/binding) | -| Keep state in public attributes, not in local variables | Between two events the controller is serialized to the client and back — locals, `DATA(...)` declarations, open cursors and locks do not survive → [Statefulness](/cookbook/expert_more/statefulness) | +| Declare every attribute you bind in the `PUBLIC SECTION` | Binding works via dynamic `ASSIGN` and cannot reach `PROTECTED` / `PRIVATE`; the roundtrip fails with `BINDING_ERROR` → [Binding](/cookbook/model/binding) | +| Keep state in attributes, not in local variables | Between two events the app instance is serialized into a draft on the SERVER and read back — attributes survive at any visibility; locals, `DATA(...)` declarations, open cursors and locks do not → [Statefulness](/cookbook/expert_more/statefulness) | | Respect the UI5 aggregation rules even though the builder does not enforce them | The builder lets you nest anything inside anything; UI5 does not, and the mismatch surfaces as broken rendering rather than a syntax error → [Definition](/cookbook/view/definition) | | Never use a deprecated UI5 control | It renders today and vanishes on the next UI5 upgrade → [Deprecated Controls](/cookbook/view/deprecated_controls) | diff --git a/docs/cookbook/device_capabilities/barcode_scanning.md b/docs/cookbook/device_capabilities/barcode_scanning.md index b00bc590..b7c52beb 100644 --- a/docs/cookbook/device_capabilities/barcode_scanning.md +++ b/docs/cookbook/device_capabilities/barcode_scanning.md @@ -45,7 +45,6 @@ Since UI5 version 1.102, the `sap.ndc.BarcodeScannerButton` control is part of t ENDMETHOD. ``` -To see barcode scanning in action, check the `Z2UI5_CL_DEMO_APP_124` sample app. ::: tip **UI5 Versions** This feature works only with the UI5 framework, not with OpenUI5. diff --git a/docs/cookbook/device_capabilities/pdf.md b/docs/cookbook/device_capabilities/pdf.md index 16a83e0a..6dc61749 100644 --- a/docs/cookbook/device_capabilities/pdf.md +++ b/docs/cookbook/device_capabilities/pdf.md @@ -11,6 +11,14 @@ Render a PDF directly in your app — for printouts from Adobe Forms, SmartForms The simplest path is the built-in popup `Z2UI5_CL_POP_PDF`. It expects the PDF as a `data:application/pdf;base64,...` URI and embeds it in an iframe: +::: warning The built-in popups are frozen +`Z2UI5_CL_POP_PDF` and its siblings live in the framework's frozen `src/99/02` package: +they still run, so existing apps keep working, but they are not maintained and +are on the [removal list](/resources/deprecations). +New code should take its popups from the separate +[popups addon](https://github.com/abap2UI5-addons/popups). +::: + ```abap METHOD z2ui5_if_app~main. diff --git a/docs/cookbook/event_navigation/exception.md b/docs/cookbook/event_navigation/exception.md index affab964..13fb804d 100644 --- a/docs/cookbook/event_navigation/exception.md +++ b/docs/cookbook/event_navigation/exception.md @@ -8,6 +8,14 @@ samples: Beyond plain messages, abap2UI5 ships dedicated popups and fallbacks for handling exceptions and unexpected failures. +::: warning The built-in popups are frozen +`Z2UI5_CL_POP_ERROR` and its siblings live in the framework's frozen `src/99/02` package: +they still run, so existing apps keep working, but they are not maintained and +are on the [removal list](/resources/deprecations). +New code should take its popups from the separate +[popups addon](https://github.com/abap2UI5-addons/popups). +::: + ### Error Popup To display full details of your exception: ```abap diff --git a/docs/cookbook/event_navigation/frontend.md b/docs/cookbook/event_navigation/frontend.md index 217c8cd7..8819590a 100644 --- a/docs/cookbook/event_navigation/frontend.md +++ b/docs/cookbook/event_navigation/frontend.md @@ -53,11 +53,9 @@ The following frontend events are available: keyboard_shortcut TYPE string VALUE `KEYBOARD_SHORTCUT`, open_new_tab TYPE string VALUE `OPEN_NEW_TAB`, location_reload TYPE string VALUE `LOCATION_RELOAD`, - nav_to_route TYPE string VALUE `NAV_TO_ROUTE`, system_logout TYPE string VALUE `SYSTEM_LOGOUT`, download_b64_file TYPE string VALUE `DOWNLOAD_B64_FILE`, urlhelper TYPE string VALUE `URLHELPER`, - history_back TYPE string VALUE `HISTORY_BACK`, store_data TYPE string VALUE `STORE_DATA`, play_audio TYPE string VALUE `PLAY_AUDIO`, wizard_set_next_step TYPE string VALUE `WIZARD_SET_NEXT_STEP`, @@ -74,7 +72,7 @@ The following frontend events are available: END OF cs_event. ``` -Some of these events have their own pages: [`keyboard_shortcut`](/cookbook/browser_interaction/keyboard_shortcuts) binds key combinations to backend events, [`nav_to_route`](/cookbook/event_navigation/routing) navigates by hash route, and [`smart_variant_init` / `filter_bar_variant_init`](/cookbook/expert_more/smart_controls) wire variant management for smart controls. +Some of these events have their own pages: [`keyboard_shortcut`](/cookbook/browser_interaction/keyboard_shortcuts) binds key combinations to backend events, [`set_nav_routing`](/cookbook/event_navigation/routing) switches hash routing on, and [`smart_variant_init` / `filter_bar_variant_init`](/cookbook/expert_more/smart_controls) wire variant management for smart controls. For example, to open a new tab directly from a button press (no backend involved): ```abap METHOD z2ui5_if_app~main. @@ -104,11 +102,11 @@ The control-call constants — `control_by_id`, `control_global`, `binding_call` | Event | `t_arg` (positional) | | ---------------- | ------------------------------------------------------------------------------------ | | `control_by_id` | `id`, `method`, `params…` — call a method on a control resolved by id | -| `control_global` | `object`, `method`, `params…` — `MESSAGE_TOAST`, `MESSAGE_BOX`, `BUSY_INDICATOR`, `THEMING` | +| `control_global` | `object`, `method`, `params…` — `MESSAGE_TOAST`, `MESSAGE_BOX`, `BUSY_INDICATOR`, `THEMING`, `POPUP`, `INVISIBLE_MESSAGE`, `FORMATTING` | | `binding_call` | `id`, `aggregation`, `method`, `params…` — e.g. `filter` (path, operator, value1, value2) or `sort` (path, descending, group) on the aggregation's binding | | `bind_element` | `index`, `_bind( table )` — element-bind a whole view slot to a table row, see below | -For `control_by_id`, any public control method is callable as long as it is not on the framework's **denylist**: methods that would break abap2UI5's own invariants (destroying views, re-rendering, detaching the framework's handlers, …) are blocked, ordinary setters and toggles (`setVisible`, `toggleBy`, `enablePostButton`, …) simply work. A small set of methods is additionally special-cased for typed arguments. `control_global` and `binding_call` remain strict whitelists — only the listed global objects and the binding methods `filter` / `sort` are callable. +For `control_by_id`, any public control method is callable as long as it is not on the framework's **denylist**: methods that would break abap2UI5's own invariants (destroying views, re-rendering, detaching the framework's handlers, …) are blocked, ordinary setters and toggles (`setVisible`, `toggleBy`, `enablePostButton`, …) simply work. A small set of methods is additionally special-cased for typed arguments. `control_global` and `binding_call` remain strict whitelists — only the listed global objects and the binding methods `filter` / `sort` are callable. Three of those objects are less obvious than the rest: `POPUP-setWithinArea` confines every popup to one control instead of to the window (UI5 ≥ 1.89; an empty argument releases it again), `INVISIBLE_MESSAGE-announce` reads a text out to a screen reader without rendering it (UI5 ≥ 1.78; `t_arg` = text, mode), and `FORMATTING-setCustomCurrencies` / `-addCustomCurrency` register currency codes the standard `sap.ui.model.type.Currency` does not know (UI5 ≥ 1.120) — `set…` REPLACES the whole registration, `add…` adds one code. ```abap " toggle a MessagePopover open, anchored to the pressing button, no roundtrip @@ -132,7 +130,7 @@ client->follow_up_action( t_arg = VALUE #( ( index ) ( client->_bind( t_product ) ) ) ). ``` -The `view` parameter selects the slot to bind; `t_arg` carries the row index and the table's binding path. See demo app 470 in the [samples repository](https://github.com/abap2UI5/samples) for a complete example. +The `view` parameter selects the slot to bind; `t_arg` carries the row index and the table's binding path. See `Z2UI5_CL_SMP_APP_470` in the [samples repository](https://github.com/abap2UI5/samples) for a complete example. ### The `view` parameter diff --git a/docs/cookbook/event_navigation/life_cycle.md b/docs/cookbook/event_navigation/life_cycle.md index 341256b9..5576576b 100644 --- a/docs/cookbook/event_navigation/life_cycle.md +++ b/docs/cookbook/event_navigation/life_cycle.md @@ -55,10 +55,10 @@ For a tiny app with one or two events, inline the view and the handler directly ## Lifecycle Pitfalls -A few details of the request lifecycle are easy to miss and produce bugs that look like framework issues but are actually pattern mistakes. These are not enforced by the compiler and not reported at runtime. +A few details of the request lifecycle are easy to miss and produce bugs that look like framework issues but are actually pattern mistakes. None of them is enforced by the compiler, and all but the first go unreported at runtime. ### Bound Attributes Must Be Public -Anything passed to `client->_bind( )` must live in `PUBLIC SECTION` — the framework binds via dynamic ASSIGN and silently ignores `PROTECTED`/`PRIVATE` attributes. Helper variables that never appear in a `_bind( )` call can stay private. Details and rationale on [Binding → Bound Attributes Must Be Public](/cookbook/model/binding). +Anything passed to `client->_bind( )` must live in `PUBLIC SECTION` — the framework binds via dynamic ASSIGN and cannot see `PROTECTED`/`PRIVATE` attributes. This one *is* reported: the roundtrip fails with `BINDING_ERROR - No class attribute for binding found`. Helper variables that never appear in a `_bind( )` call can stay private. Details and rationale on [Binding → Bound Attributes Must Be Public](/cookbook/model/binding). ### The View Is Only Sent When You Call `view_display` abap2UI5 does not re-render the view automatically. After an event, if you do **not** call `client->view_display( ... )` again, the frontend keeps the previous view tree and only the model data is updated from the serialized state. This is the common case — most event handlers should mutate state and return, leaving the view alone. @@ -99,7 +99,9 @@ Every HTTP request carries at most one event. `check_on_event( )` returns `abap_ Two consequences follow: - **Do not assume event ordering inside one `main`.** You cannot look at "the previous event" from within an event handler; the previous event ran in a separate request and the work process has been released since. -- **State across events lives in public class attributes.** Between two events, abap2UI5 serializes the controller to the client and deserializes it on the next request. Anything stored in public attributes (and in serializable types) survives; local variables, open cursors, and acquired locks do not. For sessions that need surviving server-side resources, see [Statefulness](/cookbook/expert_more/statefulness). +- **State across events lives in class attributes.** Between two events abap2UI5 serializes the whole app instance and stores it **on the server** — a draft row in `z2ui5_t_01`, which expires after four hours by default. The browser carries the draft id, never the state. + + Every attribute survives, whatever its visibility: `PUBLIC` is what `_bind( )` needs in order to *reach* an attribute, not what keeps it alive. Unbound working state therefore belongs in `PROTECTED` — a bloated public section only makes every roundtrip serialize more. What does not survive is what the serializer cannot write: local variables, open cursors, acquired locks, and references to classes that are not serializable. For sessions that need surviving server-side resources, see [Statefulness](/cookbook/expert_more/statefulness). diff --git a/docs/cookbook/event_navigation/navigation.md b/docs/cookbook/event_navigation/navigation.md index de7bcc7f..af234f80 100644 --- a/docs/cookbook/event_navigation/navigation.md +++ b/docs/cookbook/event_navigation/navigation.md @@ -58,7 +58,7 @@ Sounds familiar? The abap2UI5 framework echoes classic `call screen` and `leave ::: ::: tip Browser Back & Forward -By default, the browser's Back button leaves the abap2UI5 page — it does not step through the app stack. Enable [hash-based routing](/cookbook/event_navigation/routing) with `client->set_nav_routing( )` to couple the browser's Back/Forward buttons to `nav_app_call` / `nav_app_leave` and make apps bookmarkable. +By default, the browser's Back button leaves the abap2UI5 page — it does not step through the app stack. Enable [hash-based routing](/cookbook/event_navigation/routing) with `client->follow_up_action( client->cs_event-set_nav_routing )` to couple the browser's Back/Forward buttons to `nav_app_call` / `nav_app_leave` and make apps bookmarkable. ::: For Launchpad-based cross app navigation, see the [Fiori Launchpad](/configuration/launchpad) page. diff --git a/docs/cookbook/event_navigation/routing.md b/docs/cookbook/event_navigation/routing.md index 018e91af..82ff9326 100644 --- a/docs/cookbook/event_navigation/routing.md +++ b/docs/cookbook/event_navigation/routing.md @@ -11,19 +11,26 @@ By default, abap2UI5 leaves the URL hash untouched — the browser's Back and Fo ## Enabling Routing -Enable routing once per session with `client->set_nav_routing( )` — typically in the launcher app's `check_on_init` branch: +Enable routing once per session with +`client->follow_up_action( client->cs_event-set_nav_routing )` — typically in +the launcher app's `check_on_init` branch: ```abap METHOD z2ui5_if_app~main. - IF client->check_on_navigated( ). - client->set_nav_routing( ). " mode defaults to cs_nav_mode-keep + IF client->check_on_init( ). + " an empty t_arg means cs_nav_mode-keep + client->follow_up_action( client->cs_event-set_nav_routing ). view_display( ). ENDIF. ENDMETHOD. ``` +The framework remembers the mode on the app and re-sends it whenever the +frontend may not still hold it (page load, a Back/Forward restore, a +navigation hop), so one call per session is enough. + Once enabled, a forward navigation to another app (`client->nav_app_call`) pushes a new route history entry — the routing equivalent of a UI5 `navTo`. The browser Back button then returns to the calling app instead of leaving the page. ## Routing Modes @@ -38,7 +45,8 @@ The mode (see `cs_nav_mode`) decides how much of the running app the URL hash ca ```abap " route by class only - Back/reload/bookmark always restart the app fresh -client->set_nav_routing( client->cs_nav_mode-fresh ). +client->follow_up_action( val = client->cs_event-set_nav_routing + t_arg = VALUE #( ( client->cs_nav_mode-fresh ) ) ). ``` ::: tip Back restores the calling app as the user left it @@ -55,19 +63,20 @@ Restoring a `keep` route (browser Back/Forward, a reload, a bookmark) loads the ## Navigating by Route -With routing enabled, the `nav_to_route` frontend event navigates to another app by setting the hash route — without a backend roundtrip. It adds a browser history entry, so Back returns to the current app: +There is no separate route-navigation event: `client->nav_app_call( )` **is** +the navigation, and with routing enabled it pushes the same route history +entry, so Back returns to the calling app. ```abap -press = client->_event_client( - val = client->cs_event-nav_to_route - t_arg = VALUE #( ( `Z2UI5_CL_NEW_APP` ) ) ) +client->nav_app_call( NEW z2ui5_cl_new_app( ) ). ``` -The argument is the target app class (or a full `app/` route). The event is a no-op unless the session has enabled routing. +(The frontend-side `nav_to_route` event that used to do this was removed in +1.143.0 — see [Deprecations](/resources/deprecations).) ## Relation to Manual History Control -Routing coexists with the manual history methods described in [URL Handling](/cookbook/browser_interaction/url_handling) (`set_push_state`, the `history_back` event). Those manipulate the hash yourself; routing lets the framework own the hash and map it to apps. For app-to-app navigation, prefer routing — reach for manual push states only for fine-grained in-app states. +Routing coexists with the manual history control described in [URL Handling](/cookbook/browser_interaction/url_handling) — `set_push_state( )`, and a raw `history.back()` handed to `follow_up_action( )`. Those manipulate the hash yourself; routing lets the framework own the hash and map it to apps. For app-to-app navigation, prefer routing — reach for manual push states only for fine-grained in-app states. diff --git a/docs/cookbook/expert_more/statefulness.md b/docs/cookbook/expert_more/statefulness.md index 8dbc587b..0e64d985 100644 --- a/docs/cookbook/expert_more/statefulness.md +++ b/docs/cookbook/expert_more/statefulness.md @@ -10,9 +10,9 @@ For the small set of cases where a sticky backend session is needed — classic ## Stateless (default) | Phase | What happens | |---|---| -| **Request** | Browser sends the serialized app state to the backend | -| **Process** | A free work process picks it up, runs the controller | -| **Response** | New view + state are returned, the work process is released | +| **Request** | Browser sends the draft id and the model delta — not the app state, which never leaves the server | +| **Process** | A free work process picks it up, reads the app instance back from its draft row, runs the controller | +| **Response** | View, model and a NEW draft id are returned; the app state is written to that new draft and the work process is released | No work process is pinned, no enqueue is held, no `SET/GET` parameters survive. This is the recommended default for productive apps. diff --git a/docs/cookbook/model/binding.md b/docs/cookbook/model/binding.md index 9bf6534e..11c19dd3 100644 --- a/docs/cookbook/model/binding.md +++ b/docs/cookbook/model/binding.md @@ -95,7 +95,16 @@ Earlier releases split binding into a display-only `_bind` and a writable `_bind ::: ::: warning **Bound Attributes Must Be Public** -`_bind( )` accesses your class attributes from outside the controller via dynamic ASSIGN. This only works for attributes in the `PUBLIC SECTION` — `PROTECTED` and `PRIVATE` attributes are not visible to the framework and silently fail to bind: the value never reaches the frontend and edits never sync back. There is no compile-time or runtime error. +`_bind( )` resolves your class attributes from outside the controller via dynamic ASSIGN, and it only sees the `PUBLIC SECTION`. Passing a `PROTECTED` or `PRIVATE` attribute — or a local variable — is **not** silent: the framework finds no attribute for the reference and raises + +``` +BINDING_ERROR - No class attribute for binding found - +Please check if the bound values are public attributes of your class +``` + +which reaches the browser as a 500 and is shown in the error view. The +compiler cannot catch it (the call takes `TYPE data`), but the runtime says +exactly what is wrong. Always declare bound data in `PUBLIC SECTION`. This resembles the PAI/PBO logic, where data lived in global variables. See also [Life Cycle → Lifecycle Pitfalls](/cookbook/event_navigation/life_cycle#lifecycle-pitfalls). ::: @@ -133,13 +142,14 @@ ABAP and UI5 do not share a type system. When ABAP values cross to the frontend | -------------------- | ------------------ | ------------------------------------------------------ | -------------------------------------------------------------------- | | `string`, `c LENGTH n` | JSON string | `Input`, `Text` | Works without a formatter. | | `i`, `int8`, `b`, `s` | JSON number | `Input type="Number"`, `Text` | Returned as string from inputs; cast back if you need an integer. | -| `p LENGTH n DECIMALS m`, `decfloat16`, `decfloat34` | JSON string | `Input`, `Text` + `sap.ui.model.type.Float`/`Currency` | Sent as a string to preserve precision. Locale formatting needs an explicit type — see [Currency](/cookbook/model/formatter#currency). | +| `p LENGTH n DECIMALS m`, `decfloat16`, `decfloat34` | JSON number | `Input`, `Text` + `sap.ui.model.type.Float`/`Currency` | The decimals travel as written (`1234.56`), no rounding through a binary float. Locale formatting needs an explicit type — see [Currency](/cookbook/model/formatter#currency). | | `f` (binary float) | JSON number | `Input`, `Text` + `sap.ui.model.type.Float` | Binary float — prefer `p` or `decfloat34` for monetary values to avoid rounding drift. | | `n LENGTH n` | JSON string of digits | `Input` + `sap.ui.model.odata.type.String` with `isDigitSequence: true` | Without the constraint, leading zeros render literally — see [Digit Sequence](/cookbook/model/formatter#digit-sequence). | | `d` | 8-char string `YYYYMMDD` | `DatePicker` + `sap.ui.model.type.Date` | Not an ISO date — a formatter is required for explicit locale or pattern control. See [Date](/cookbook/model/formatter#date). | | `t` | 6-char string `HHMMSS` | `TimePicker` + `sap.ui.model.type.Time` | Same pattern as `d` — see [Time](/cookbook/model/formatter#time). | -| `abap_bool` (`X`/` `) | JSON string `"X"` / `""` | `CheckBox` with expression binding or ABAP-side conversion | UI5's `CheckBox` expects `true`/`false`, not `"X"` — see [Boolean](/cookbook/model/formatter#boolean). | -| `timestamp`, `timestampl`, `utclong` | JSON string (packed digits for `timestamp`/`timestampl`; ISO-like for `utclong`) | `DateTimePicker` + ABAP-side conversion or custom formatter | No built-in UI5 type reads them directly. Split into `d` + `t` or convert to a `yyyyMMddHHmmss` string — see [Timestamp](/cookbook/model/formatter#timestamp). | +| `abap_bool` (`X`/` `) | JSON `true` / `false` | `CheckBox`, `Switch` — bind it directly | The framework maps the BOOLEAN ABAP TYPES to a JSON boolean and back, so no expression and no formatter is needed. A flag typed `c LENGTH 1` instead is just a string — see [Boolean](/cookbook/model/formatter#boolean). | +| `timestamp`, `timestampl` | JSON number (the packed digits) | `DateTimePicker` + ABAP-side conversion or a formatter | No built-in UI5 type reads them directly. Split into `d` + `t` or convert to a `yyyyMMddHHmmss` string — see [Timestamp](/cookbook/model/formatter#timestamp). | +| `utclong` | JSON string, ISO-like (`2026-08-21T14:00:00Z`) | `DateTimePicker` + `Formatter.DateCreateObject` | The one timestamp type that arrives in a shape `new Date( )` parses — see [The formatters abap2UI5 ships](/cookbook/model/formatter#the-formatters-abap2ui5-ships). | | `xstring` | binary — must be base64-encoded in ABAP before binding | `Image`, `FileUploader`, `pdf_viewer` | The framework does not auto-encode. Convert with `cl_web_http_utility=>encode_x_base64( )` (or `cl_http_utility=>if_http_utility~encode_x_base64( )` on older releases) — see [PDF](/cookbook/device_capabilities/pdf) and [Upload / Download](/cookbook/device_capabilities/upload_download). | | structure | JSON object | Bind individual fields with `struct-field` | One model path per field — see [Binding to Structures](#binding-to-structures). | | internal table | JSON array | `Table`, `List`, `Tree` | One row context per item — see [Tables](/cookbook/model/tables) and [Trees](/cookbook/model/trees). | diff --git a/docs/cookbook/model/formatter.md b/docs/cookbook/model/formatter.md index eb193c5e..28261072 100644 --- a/docs/cookbook/model/formatter.md +++ b/docs/cookbook/model/formatter.md @@ -94,29 +94,45 @@ ABAP `t` is a 6-character string `HHMMSS`. Same pattern as Date, with `sap.ui.mo ## Boolean -ABAP `abap_bool` is `"X"` or `""`. UI5's `CheckBox` expects `true` / `false`. Two practical bridges: +**A boolean needs no formatter.** `abap_bool` is `"X"` or `""` in ABAP, but it +does not travel that way: the framework serializes the boolean ABAP types to a +JSON `true` / `false` and converts back on the way in. So bind the attribute +straight to the control and both directions work: -**Expression binding** — compare the bound value to `'X'` inline. Read-only: ```abap )->tag( `CheckBox` - )->a( n = `selected` v = `{= $` && client->_bind( mv_flag ) && ` === 'X' }` + )->a( n = `selected` v = client->_bind( mv_flag ) ) ``` -This resolves to `{= ${/MV_FLAG} === 'X' }`. Note that expression bindings cannot write back — checking the box will not flip the ABAP attribute. -**ABAP-side conversion** — keep a parallel `string`-typed attribute (`'true'` / `'false'`) to bind against, and translate before/after each event: -```abap -DATA flag_bool TYPE abap_bool. -DATA flag_str TYPE string. " 'true' / 'false' for the checkbox - -" before view_display: -flag_str = COND #( WHEN flag_bool = abap_true THEN 'true' ELSE 'false' ). +Inside a table row template, the same thing relative to the row: -" after the event: -flag_bool = COND #( WHEN flag_str = 'true' THEN abap_true ELSE abap_false ). +```abap +)->tag( `CheckBox` + )->a( n = `selected` v = `{CHECKBOX}` ) ``` -Then a `CheckBox` whose `selected` attribute is `client->_bind( flag_str )` works both directions. More boilerplate in the controller, simpler view. -A custom JS formatter wired through `sap.ui.model.SimpleType` is the third option — see the [samples repository](https://github.com/abap2UI5/samples). +::: warning It is the TYPE that decides, not the value +The mapping is keyed on the ABAP type: `abap_bool`, `abap_boolean`, +`xsdboolean`, `flag` and `xfeld` become a JSON boolean. A flag you declared as +`c LENGTH 1` looks identical in the debugger and travels as the **string** +`"X"` — a `CheckBox` bound to it stays unchecked, because `"X"` is not `true`. +Type the attribute `abap_bool` and the problem disappears; that is the fix, not +an expression binding. +::: + +An expression binding (`{= ${/MV_FLAG} === 'X' }`) is the wrong tool here twice +over: it compares against a value that is not on the wire, and an expression +binding cannot write back, so the box would not flip the attribute even if the +comparison held. + +::: tip A boolean written into the view is a different question +All of the above is about a **bound** value. An ABAP boolean put straight into +the XML as an attribute value — `)->a( n = `visible` v = flag )` — is +stringified, and UI5 reads any non-empty string as true, so `abap_false` +renders the control **visible**. Use the builder's boolean parameter for that: +`)->a( n = `visible` b = flag )`. The linter rule +[`unconverted-abap-boolean`](/advanced/linter) catches it. +::: ## Timestamp @@ -136,6 +152,46 @@ Conversion happens in ABAP (`WRITE timestamp TO ts_string …` or a helper); the A custom JS formatter is the third option when neither fits. +## The formatters abap2UI5 ships + +A UI5 `type` covers the `value` property, which is a string. It does not cover +a property that wants a **JavaScript `Date` object** — `DatePicker.dateValue`, +`PlanningCalendarAppointment.startDate` — and JSON has no date type, so the +model physically cannot carry one. For that, and for one text case, the +framework ships a small curated formatter module. It is a public contract: +`z2ui5/model/formatter`, also published as the `z2ui5.Formatter` global for +releases without `core:require`. + +Load it once on the view root, then name a helper in the binding string: + +```abap +view->a( n = `core:require` v = `{Formatter: 'z2ui5/model/formatter'}` ). + +... + +)->tag( `DatePicker` + )->a( n = `dateValue` v = |\{ path: '{ client->_bind( val = mv_date path = abap_true ) }', + formatter: 'Formatter.DateAbapDateToDateObject' \}| +``` + +| Helper | Takes | Returns | +|---|---|---| +| `DateAbapDateToDateObject` | an ABAP `d` on the wire (`YYYYMMDD`) | a `Date` at midnight local time | +| `DateAbapDateTimeToDateObject` | an ABAP `d` and `t` as two `parts` (the `t` may be omitted → midnight) | a `Date` with the time applied | +| `DateCreateObject` | anything the JS `Date` constructor parses (an ISO string, `utclong`) | a `Date` | +| `expandInlineIcons` | a formatted-text string carrying `%%icon:sap-icon://%%` placeholders | the same text with the theme's icon glyphs inlined — for a `MessageStrip` text | + +An **initial or empty** value yields `null`, never an `Invalid Date`. That +matters more than it sounds: an Invalid Date is truthy, so a control accepts it +and fails much later — a `sap.ui.unified` calendar throws for every rendered +day and takes the whole view down. `null` is what "no date" means to a UI5 date +property, so an optional date field in a bound row stays empty instead. + +The module grows through framework pull requests only; a helper of your own +goes in your own module and is required the same way. See +`Z2UI5_CL_SMP_APP_457` (DatePicker) and `Z2UI5_CL_SMP_APP_456` +(PlanningCalendar) in the [samples repository](https://github.com/abap2UI5/samples). + ## Full Worked Example The class below combines the Currency and Digit Sequence patterns in one app and demonstrates every `formatOptions` variant listed under [Currency](#currency): diff --git a/docs/cookbook/model/size_limit.md b/docs/cookbook/model/size_limit.md index 14a9d499..e9482a64 100644 --- a/docs/cookbook/model/size_limit.md +++ b/docs/cookbook/model/size_limit.md @@ -135,6 +135,15 @@ client->follow_up_action( t_arg = VALUE #( ( `500` ) ( client->cs_view-nested ) ) ). ``` +::: warning The main view and the two nested views share one limit +Only the popup and the popover own a model of their own. `MAIN`, `NEST` and +`NEST2` are one control tree and inherit **one** JSON model through UI5 model +propagation, so a limit set on any of them lands on that shared model and the +**largest** of the three wins. Raising it for the nested view raises it for the +main view too, and resetting the main view changes nothing while the nested one +still asks for more. Reset all three to get back to `100`. +::: + ::: tip **When to raise it** Raise the limit only as high as you actually need. Large bindings increase memory consumption on the frontend and slow down rendering. For very large datasets, prefer a server-side pattern (OData with `growing`, paging, filtering) instead of pushing everything into the model. ::: diff --git a/docs/cookbook/popup_popover/popup.md b/docs/cookbook/popup_popover/popup.md index cc9d15d7..dc4182ba 100644 --- a/docs/cookbook/popup_popover/popup.md +++ b/docs/cookbook/popup_popover/popup.md @@ -91,6 +91,14 @@ The popup has the same lifecycle as the main view: `popup_display( )` renders th For a cleaner source layout, encapsulate popups in separate classes and call them via [navigation](/cookbook/event_navigation/navigation). An example with the confirmation popup: + +::: warning The built-in popups are frozen +`Z2UI5_CL_POP_TO_CONFIRM` and its siblings live in the framework's frozen `src/99/02` package: +they still run, so existing apps keep working, but they are not maintained and +are on the [removal list](/resources/deprecations). +New code should take its popups from the separate +[popups addon](https://github.com/abap2UI5-addons/popups). +::: ```abap METHOD z2ui5_if_app~main. diff --git a/docs/cookbook/translation_messages/logging.md b/docs/cookbook/translation_messages/logging.md index c04c2765..9a0f2d72 100644 --- a/docs/cookbook/translation_messages/logging.md +++ b/docs/cookbook/translation_messages/logging.md @@ -5,6 +5,14 @@ outline: [2, 4] Logging is critical for business processes. In ABAP systems, the standard tool is the Business Application Log (BAL), available on-premise and in ABAP Cloud environments alike. With abap2UI5, use BAL like you would in classic ABAP and display logs with the framework's built-in popups. +::: warning The built-in popups are frozen +`Z2UI5_CL_POP_MESSAGES` and its siblings live in the framework's frozen `src/99/02` package: +they still run, so existing apps keep working, but they are not maintained and +are on the [removal list](/resources/deprecations). +New code should take its popups from the separate +[popups addon](https://github.com/abap2UI5-addons/popups). +::: + ## BAL Tables In classic ABAP, use the BAL function modules and show the BAL table with the popup `Z2UI5_CL_POP_MESSAGES`. In the `bal_t_msgr` structure, `msgno` is the message number within the message class (`msgid`), while `msgnumber` is the message's sequence number within the log: ```abap diff --git a/docs/cookbook/translation_messages/message.md b/docs/cookbook/translation_messages/message.md index 0cf0c2b8..7d786065 100644 --- a/docs/cookbook/translation_messages/message.md +++ b/docs/cookbook/translation_messages/message.md @@ -84,6 +84,14 @@ The framework accepts other inputs too — pass your message structure and the m ### Multi-Message Popup The message box gives you basic output. For richer detail, use the popup `Z2UI5_CL_POP_MESSAGES`: + +::: warning The built-in popups are frozen +`Z2UI5_CL_POP_MESSAGES` and its siblings live in the framework's frozen `src/99/02` package: +they still run, so existing apps keep working, but they are not maintained and +are on the [removal list](/resources/deprecations). +New code should take its popups from the separate +[popups addon](https://github.com/abap2UI5-addons/popups). +::: ```abap METHOD z2ui5_if_app~main. diff --git a/docs/cookbook/troubleshooting/common_failures.md b/docs/cookbook/troubleshooting/common_failures.md index 9d49cbdb..5b70e310 100644 --- a/docs/cookbook/troubleshooting/common_failures.md +++ b/docs/cookbook/troubleshooting/common_failures.md @@ -24,11 +24,18 @@ Binding silently drops the write-back when the path is invalid — your ABAP att ## Bound Attribute Not Public -`_bind( )` resolves attributes via dynamic `ASSIGN` and only sees the `PUBLIC SECTION`. Anything declared `PROTECTED` or `PRIVATE` is silently ignored — the binding path is generated, but no data is ever serialized for it. There is no compile-time or runtime error. +`_bind( )` resolves attributes via dynamic `ASSIGN` and only sees the `PUBLIC SECTION`. Anything declared `PROTECTED` or `PRIVATE` — or a local variable — cannot be found, and the framework says so rather than shrugging: it raises `z2ui5_cx_ui5_util_error` with + +``` +BINDING_ERROR - No class attribute for binding found - +Please check if the bound values are public attributes of your class +``` + +Nothing catches it on the way out, so the roundtrip answers 500 and the message lands in the error view. (A `_bind( val = … tab = … )` cell binding raises the same thing under `BINDING_ERROR_TAB_CELL_LEVEL`.) Where to look: -- **Symptom is identical to a binding-path mismatch.** The browser console shows the same `Binding "/path/..." was not found in model` warning, because the path was never populated on the wire. -- **Check the visibility of the attribute** in the class definition before re-reading the XML. Helper variables that never appear in a `_bind( )` call can stay private; anything you bind must move to `PUBLIC SECTION`. +- **The error text itself.** Unlike a binding-path mismatch, this failure is reported by name — if you are reading a browser-console warning, you are looking at the section above, not at this one. +- **Check the visibility of the attribute** in the class definition. Helper variables that never appear in a `_bind( )` call can stay private; anything you bind must move to `PUBLIC SECTION`. See [Binding → Bound Attributes Must Be Public](/cookbook/model/binding). @@ -106,11 +113,12 @@ Where to look: ## State Lost Between Events -Between two events the controller is serialized to the client and deserialized on the next request. Only `PUBLIC SECTION` attributes of **serializable** types survive — local variables, `DATA(...)` declarations inside an event handler, open database cursors, acquired locks, and `REF TO` references to non-serializable objects do not. +Between two events the app instance is serialized into a draft record on the **server** and read back on the next request; the browser only carries the draft id. **Attributes** of serializable types survive — at any visibility, `PROTECTED` and `PRIVATE` included. What does not survive is everything that is not an attribute or cannot be written: local variables, `DATA(...)` declarations inside an event handler, open database cursors, acquired locks, and `REF TO` references to non-serializable objects. Where to look: - **Symptom**: a value set in one event is empty on the next; a calculated value built up in `check_on_init` is gone by the time the user clicks; a singleton or "global" state appears to reset between roundtrips. -- **Fix**: move surviving state into the `PUBLIC SECTION` with concrete, serializable types. For resources that genuinely need to live server-side across events (file handles, persistent locks, expensive caches), see [Statefulness](/cookbook/expert_more/statefulness). +- **Fix**: move surviving state out of the method into an attribute with a concrete, serializable type — it does not have to be public for that; only `_bind( )` needs public. If serialization itself is the problem the roundtrip says so, with `APP_SERIALIZATION_ERROR`. For resources that genuinely need to live server-side across events (file handles, persistent locks, expensive caches), see [Statefulness](/cookbook/expert_more/statefulness). +- **The draft expires.** Four hours by default — an app left open longer starts fresh rather than restoring. --- diff --git a/docs/cookbook/view/nested_views.md b/docs/cookbook/view/nested_views.md index 5d92bbc7..bfa5603d 100644 --- a/docs/cookbook/view/nested_views.md +++ b/docs/cookbook/view/nested_views.md @@ -166,16 +166,16 @@ METHOD view_display_detail. ENDMETHOD. ``` -The layout is bound editable (`mv_layout`), so events like *full-screen mode* or *close detail* simply update `mv_layout` and call `view_model_update`. The FCL transitions itself; no view is rebuilt. +The layout is bound editable (`mv_layout`), so events like *full-screen mode* or *close detail* simply update `mv_layout` — the changed model reaches the client with the response by itself. The FCL transitions itself; no view is rebuilt. End-to-end samples: - `Z2UI5_CL_SMP_APP_097` — list master, `sap.ui.table.Table` in the detail with sort/filter/row actions. -- `Z2UI5_CL_DEMO_APP_085` — full master-detail with an `ObjectPageLayout` as the nested detail, including search, sort, and the FCL fullscreen toggle. +- `Z2UI5_CL_SMP_APP_098` — the three-column FCL: list, detail and detail-of-detail, with the navigation that opens each one. ### Refreshing After Data Changes -All bound data lives in a **single client-side model**, regardless of which view a binding was built in — `client->_bind( ... )` always writes to that one root model. One call therefore refreshes everything: after the ABAP data changes, `client->view_model_update( )` pushes the new values into every rendered view — main, nested, second nested. +All bound data lives in a **single client-side model**, regardless of which view a binding was built in — `client->_bind( ... )` always writes to that one root model. Nothing has to be pushed by hand: when the ABAP data changes, the framework sends the new values to every rendered view — main, nested, second nested — with the response of the roundtrip that changed them. ```abap DELETE t_tab2 WHERE title = ls_arg-title. @@ -231,10 +231,10 @@ Plain composition is the right starting point: keep helper methods that take a p - The anchor id must be unique in the main view. The framework calls `byId` on the rendered view to find it; duplicate ids break the lookup. - Always provide `method_destroy` when a nested slot will be replaced more than once. Forgetting it causes nested fragments to accumulate. - Build the nested view in its own method (e.g. `view_display_detail`) and call it both from the initial render and from event handlers. Two call sites, one definition. -- If a nested view does not pick up a data change, you probably need `view_model_update( )`; if a control simply isn't there, you need `nest_view_display( )` again. +- If a nested view does not pick up a data change, the value did not change on the ABAP side of the roundtrip — the push is automatic (`view_model_update( )` has been an empty method since 1.143.0). If a control simply isn't there, you need `nest_view_display( )` again. - For very large apps, look at `Z2UI5_CL_SMP_APP_104`, which loads each detail screen from a separate `z2ui5_if_app` class and renders it into the nested slot. It is an advanced pattern — start with the simpler form first. -See `Z2UI5_CL_SMP_APP_065`, `Z2UI5_CL_DEMO_APP_085`, `Z2UI5_CL_SMP_APP_097`, `Z2UI5_CL_SMP_APP_098`, and `Z2UI5_CL_SMP_APP_104` for runnable examples covering every variation above. +See `Z2UI5_CL_SMP_APP_065`, `Z2UI5_CL_SMP_APP_097`, `Z2UI5_CL_SMP_APP_098`, `Z2UI5_CL_SMP_APP_104` and `Z2UI5_CL_SMP_APP_176` for runnable examples covering every variation above. diff --git a/docs/technical/how_it_all_works.md b/docs/technical/how_it_all_works.md index fb5a426e..6343a5e7 100644 --- a/docs/technical/how_it_all_works.md +++ b/docs/technical/how_it_all_works.md @@ -194,7 +194,7 @@ But this isn't a problem for ABAP. From an ABAP perspective, it resembles older ## 16. Draft -With RAP, users can save interim results in drafts so they can pause and resume work later. The abap2UI5 architecture acts as if we send a fresh app to the frontend after every event, but we still need to keep the user's previous inputs and state. To do this, the `z2ui5_if_app` interface inherits the `if_serializable_object` interface, which lets us serialize and store all key information from every request (like the current view or its status): +With RAP, users can save interim results in drafts so they can pause and resume work later. The abap2UI5 architecture acts as if we send a fresh app to the frontend after every event, but we still need to keep the user's previous inputs and state. To do this, the `z2ui5_if_app` interface inherits the `if_serializable_object` interface, which lets us serialize and store all key information from every request (like the current view or its status). One generic table holds them all — `Z2UI5_T_01` (the diagrams below still show its working name `z2ui5_t_draft`): z2ui5_t_draft - the abap2UI5 persistence for interim results diff --git a/docs/tutorials/walkthrough/step-4.md b/docs/tutorials/walkthrough/step-4.md index 583a445e..7c53a70c 100644 --- a/docs/tutorials/walkthrough/step-4.md +++ b/docs/tutorials/walkthrough/step-4.md @@ -53,18 +53,20 @@ ENDCLASS. ## How the Value Travels -- **`DATA recipient TYPE string`** — a public attribute is the model. The - framework serializes public attributes after every roundtrip and restores - them before the next one, so your class keeps its state without any session - handling. The attribute must be in the `PUBLIC SECTION`: the framework - reads it dynamically and silently ignores private or protected ones (full - rules on the [Binding](/cookbook/model/binding) page). +- **`DATA recipient TYPE string`** — a public attribute is the model. After + every roundtrip the framework serializes the whole app instance into a draft + on the server and restores it before the next one, so your class keeps its + state without any session handling. Attributes you `_bind( )` must be in the + `PUBLIC SECTION` — the framework reaches them dynamically, and a private or + protected one fails the roundtrip with `BINDING_ERROR`. (Everything else may + stay protected; it is serialized either way. Full rules on the + [Binding](/cookbook/model/binding) page.) - **`client->_bind( recipient )`** connects the attribute to the `value` property of the input. When the button fires, the browser sends the current screen state along with the event — by the time your `ELSEIF` branch runs, `recipient` already holds what the user typed. - **Type it, press the button** — the toast greets whatever name is in the - field. Two-way, and you wrote no transfer code. + field. Both directions, and you wrote no transfer code. Next, the same binding moves a whole internal table into a list. diff --git a/package.json b/package.json index 3b8ab7d3..76395928 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,11 @@ "docs:build": "node scripts/generate-llms.mjs && vitepress build docs", "docs:preview": "vitepress preview docs", "check:examples": "node scripts/check-examples.mjs", + "check:api-names": "node scripts/check-api-names.mjs", "link:samples": "node scripts/link-samples.mjs", "check:samples": "node scripts/link-samples.mjs --check", "test": "node --test test/*.test.mjs", - "check": "npm run test && npm run check:version && npm run docs:build && npm run check:examples && npm run check:samples", + "check": "npm run test && npm run check:version && npm run docs:build && npm run check:examples && npm run check:api-names && npm run check:samples", "llms": "node scripts/generate-llms.mjs", "check:version": "node scripts/check-version.mjs" }, diff --git a/scripts/check-api-names.mjs b/scripts/check-api-names.mjs new file mode 100644 index 00000000..003a173c --- /dev/null +++ b/scripts/check-api-names.mjs @@ -0,0 +1,260 @@ +#!/usr/bin/env node +/* + * check-api-names — every `client->` name on this site has to exist in the + * release this site names. + * + * check-examples already compiles the fenced examples that are WHOLE CLASSES, + * and that is the strongest check here. It cannot see the rest of a page: a + * sentence in the prose, a two-line snippet that is not a class, the constant + * block a page reproduces for reference. That is most of what a reader + * actually copies, and it is where the API drift of 1.143.0 survived: + * + * routing.md opened with `client->set_nav_routing( )`, a METHOD deleted + * in that release, and navigated with + * `cs_event-nav_to_route`, a constant deleted with it + * navigation.md named the same method in a sentence + * frontend.md listed `nav_to_route` and `history_back` in the cs_event + * block it prints for reference + * url_handling.md announced "two client methods" for the browser history and + * showed one - the second was the deleted `history_back` + * + * Four pages, one release, nothing red. So this gate reads the SAME interface + * check-examples compiles against and asks three questions of every page: + * + * `client->NAME(` is NAME a method of z2ui5_if_client? + * `NAME = ` inside one is it a parameter of that method? (fenced ABAP only) + * `cs_GROUP-MEMBER` is MEMBER in that constant group? + * + * …and a fourth, of the same kind: a `github.com/abap2UI5/abap2UI5/blob/main/` + * link has to resolve. The user-exits page pointed at + * `src/02/z2ui5_if_exit.intf.abap` after the interface had been retired to + * `src/99` - a 404 for every reader who clicked it. Those links say `main`, so + * `main` is what they are checked against, not the release. + * + * TRUTH is the release, never main. A page is correct when it matches what the + * reader can install, and main is ahead of that by definition - judging + * against main would pass a page teaching API that does not exist yet. The + * release number comes from lib/release.mjs, the same one check-examples + * pins to, and `A2UI5_REF` overrides it for a canary run against main. + * + * When the interface cannot be fetched the run SAYS SO and passes. A + * documentation gate must not go red because github.com is unreachable, and + * must not claim to have verified something it did not. + * + * Pages whose SUBJECT is what was removed are exempt - deprecations and the + * changelog exist to name the old names, and a gate that forbids that forbids + * documenting a migration at all. + * + * node scripts/check-api-names.mjs (npm run check:api-names) + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { declaredRelease } from './lib/release.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const PAGES = path.join(ROOT, 'docs'); + +/* The pages that are ABOUT the names that went away. */ +const EXEMPT = new Set([ + 'resources/deprecations.md', + 'resources/changelog.md', + 'cookbook/event_navigation/action.md', +]); + +const REF = process.env.A2UI5_REF || declaredRelease(ROOT); +if (!REF) { + console.log('the three places naming the release DISAGREE, so there is nothing to pin to.'); + console.log('SKIPPED: run npm run check:version.'); + process.exit(0); +} + +const SOURCE = `https://raw.githubusercontent.com/abap2UI5/abap2UI5/${REF}/src/02/z2ui5_if_client.intf.abap`; + +let iface; +try { + const res = await fetch(SOURCE, { signal: AbortSignal.timeout(20000) }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + iface = await res.text(); +} catch (err) { + console.log(`z2ui5_if_client at ${REF}: not resolved (${err.message})`); + console.log('SKIPPED: nothing was verified.'); + process.exit(0); +} + +/* ------------------------------------------------------------- the contract */ + +/** method name -> Set of IMPORTING parameter names. */ +const methods = new Map(); +/** constant group (cs_event, cs_view, ...) -> Set of member names. */ +const groups = new Map(); + +{ + let method = null; + let importing = false; + const open = []; + for (const raw of iface.split(/\r?\n/)) { + // a comment can hold anything, including the names this gate looks for + const line = raw.replace(/^\s*"[!]?.*$/, '').replace(/\s"\s.*$/, '').trimEnd(); + + const begin = /^\s*BEGIN OF ([a-z_0-9]+),?\s*$/i.exec(line); + if (begin) { open.push(begin[1].toLowerCase()); groups.set(open.at(-1), new Set()); continue; } + if (/^\s*END OF ([a-z_0-9]+)/i.test(line)) { open.pop(); continue; } + if (open.length) { + // a member of every group it is nested in, so cs_device-system-phone + // resolves whether the page writes the outer or the inner name + const member = /^\s*([a-z_0-9]+)\s+TYPE\s/i.exec(line); + if (member) for (const g of open) groups.get(g).add(member[1].toLowerCase()); + continue; + } + + const decl = /^\s*METHODS\s+([a-z_0-9]+)/i.exec(line); + if (decl) { method = decl[1].toLowerCase(); methods.set(method, new Set()); importing = false; continue; } + if (!method) continue; + if (/^\s*IMPORTING\s*$/i.test(line)) { importing = true; continue; } + if (/^\s*(RETURNING|EXPORTING|CHANGING|RAISING|PREFERRED)/i.test(line)) { importing = false; continue; } + if (/^\s*(CONSTANTS|TYPES|DATA|ENDINTERFACE)/i.test(line)) { method = null; continue; } + if (!importing) continue; + const param = /^\s*(?:VALUE\()?([a-z_0-9]+)\)?\s+TYPE\s/i.exec(line); + if (param) methods.get(method).add(param[1].toLowerCase()); + } +} + +if (methods.size === 0 || !groups.has('cs_event')) { + console.log(`z2ui5_if_client at ${REF} parsed to nothing - the interface changed shape.`); + console.log('Fix the parser in scripts/check-api-names.mjs, or this gate silently stops checking.'); + process.exit(1); +} + +/* ------------------------------------------------------------------ the pages */ + +function markdownFiles(dir, out = []) { + for (const name of fs.readdirSync(dir)) { + if (name === '.vitepress' || name === 'public' || name === 'node_modules') continue; + const full = path.join(dir, name); + if (fs.statSync(full).isDirectory()) markdownFiles(full, out); + else if (full.endsWith('.md')) out.push(full); + } + return out; +} + +/** The fenced ABAP blocks of a page, as one string with everything else blanked. */ +const abapOnly = (text) => text.replace(/```(\w*)\n([\s\S]*?)```/g, (all, lang, body) => ( + /^(abap)?$/i.test(lang) ? all.replace(body, body) : ' '.repeat(all.length) +)); + +/** Blank ABAP string literals so an XML attribute inside one is not a parameter. */ +const withoutLiterals = (text) => text + .replace(/`[^`\n]*`/g, (x) => ' '.repeat(x.length)) + .replace(/\|[^|\n]*\|/g, (x) => ' '.repeat(x.length)); + +const problems = []; +let checked = 0; + +for (const file of markdownFiles(PAGES)) { + const rel = path.relative(PAGES, file).split(path.sep).join('/'); + if (EXEMPT.has(rel)) continue; + const text = fs.readFileSync(file, 'utf8'); + + // 1 + 2: `client->NAME(` anywhere; its arguments only where they are code + for (const source of [{ text, params: false }, { text: withoutLiterals(abapOnly(text)), params: true }]) { + for (const call of source.text.matchAll(/client->([a-z_0-9]+)\s*\(/gi)) { + const name = call[1].toLowerCase(); + checked += 1; + if (!methods.has(name)) { + if (!source.params) { + problems.push(`${rel}: \`client->${name}( )\` is not a method of z2ui5_if_client in ${REF}`); + } + continue; + } + if (!source.params) continue; + + // walk to the matching close paren, then read the top-level `name =` + let i = call.index + call[0].length; + let depth = 1; + while (i < source.text.length && depth > 0) { + if (source.text[i] === '(') depth += 1; + else if (source.text[i] === ')') depth -= 1; + i += 1; + } + const args = source.text.slice(call.index + call[0].length, i - 1); + let nested = 0; + for (const token of args.matchAll(/([()])|\b([a-z_0-9]+)\s*=(?![=>])/gi)) { + if (token[1] === '(') { nested += 1; continue; } + if (token[1] === ')') { nested -= 1; continue; } + if (nested !== 0) continue; + const param = token[2].toLowerCase(); + if (!methods.get(name).has(param)) { + problems.push( + `${rel}: \`client->${name}( ${param} = ... )\` - no such parameter in ${REF}\n` + + ` it takes: ${[...methods.get(name)].join(', ') || '(none)'}`, + ); + } + } + } + } + + // 3: cs_-, wherever it is written + for (const use of text.matchAll(/\b(cs_[a-z_0-9]+)-([a-z_0-9]+)/gi)) { + const group = use[1].toLowerCase(); + const member = use[2].toLowerCase(); + if (!groups.has(group)) continue; + checked += 1; + if (!groups.get(group).has(member)) { + problems.push(`${rel}: \`${use[0]}\` is not in ${group} in ${REF}`); + } + } +} + +/* 4: the source links. Distinct URLs only - the same file is linked from + * several pages - and one HEAD each, which is a handful of requests. A network + * failure here SKIPS this question and leaves the three above intact. */ +let links = 0; +{ + const seen = new Map(); + for (const file of markdownFiles(PAGES)) { + const rel = path.relative(PAGES, file).split(path.sep).join('/'); + for (const m of fs.readFileSync(file, 'utf8') + .matchAll(/https:\/\/github\.com\/abap2UI5\/abap2UI5\/blob\/main\/([^)`"\s]+)/g)) { + const target = m[1].split('#')[0]; + if (!seen.has(target)) seen.set(target, new Set()); + seen.get(target).add(rel); + } + } + links = seen.size; + const raw = (f) => `https://raw.githubusercontent.com/abap2UI5/abap2UI5/main/${f}`; + const verdicts = await Promise.all([...seen.keys()].map(async (target) => { + try { + const res = await fetch(raw(target), { method: 'HEAD', signal: AbortSignal.timeout(20000) }); + if (res.status === 404) return target; + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return null; + } catch { + return undefined; // unreachable, not absent + } + })); + if (verdicts.some((v) => v === undefined)) { + console.log('source links: not resolved (network) - that question was skipped.'); + links = 0; + } else { + for (const target of verdicts.filter(Boolean)) { + problems.push( + `${[...seen.get(target)].join(', ')}: links ${target} in abap2UI5, which is not there on main\n` + + ' the file moved or was deleted - a link every reader who clicks it gets a 404 from', + ); + } + } +} + +console.log(`check-api-names: ${checked} name(s) and ${links} source link(s) on ${markdownFiles(PAGES).length} page(s), against abap2UI5 ${REF}`); + +if (problems.length) { + console.log(`\n${problems.length} problem(s):`); + for (const p of problems) console.log(` ${p}`); + console.log('\n Something the reader cannot follow: a name their install does not'); + console.log(' have, or a link that 404s. Either the page describes a release that'); + console.log(' has not happened yet, or it describes one that is gone - see'); + console.log(' resources/deprecations.md for what replaced it.'); + process.exit(1); +} +console.log('every client-> name on the site exists in the release it names - OK');