diff --git a/docs/public/api/client-api.json b/docs/public/api/client-api.json index f8a46752..0c420894 100644 --- a/docs/public/api/client-api.json +++ b/docs/public/api/client-api.json @@ -551,6 +551,7 @@ "**cs_event-keyboard_shortcut** - bind a key combination to a named backend event, the declarative equivalent of a sap.ui.core.CommandExecution shortcut, t_arg = combination, event name: ``client->follow_up_action( val = client->cs_event-keyboard_shortcut t_arg = VALUE #( ( `Ctrl+S` ) ( `SAVE` ) ) )``. The combination is spelled like the UI5 one (`Ctrl+S`, `Ctrl+Shift+D`, `F2`; ctrl/shift/alt/meta in any order, cmd/command/option/control accepted as aliases). Pressing it fires the event exactly like a button press and suppresses the browser's own default for the combination. Registering the same combination again rebinds it; an empty event name removes it. The registrations belong to the running app and are dropped when another app takes over. An optional THIRD t_arg SCOPES the shortcut: the scoped registration wins while its scope is OPEN and the unscoped one applies otherwise, which is how a UI5 CommandExecution in a Popover's dependents shadows the page-level one for the same command. A scope is either a view slot (cs_view-popover/popup/nested/nested2/main) or the ID OF A CONTROL that can be open or closed - a Popover/Dialog declared in the view and opened with control_by_id openBy, which never enters a framework slot. A control scope beats a slot scope (it is the more specific statement), then the innermost open slot wins. An empty event name removes the registration of THAT scope only.", "**cs_event-hash_attach_changed** - APP-OWNED hash routing (HashChanger#attachHashChanged), the 1:1 counterpart of a UI5 router's own hash (`#/Page2`) for an app that does NOT use hash_routing, t_arg = a backend event name: ``client->follow_up_action( val = client->cs_event-hash_attach_changed t_arg = VALUE #( ( `HASH_CHANGED` ) ) )``. From then on hash_set( `/Page2` ) writes that value as the whole app hash (a pushed history entry), hash_replace( ) the same without a new entry, and a hash change the app did not write itself - browser Back/Forward, a manual URL edit - fires the registered event; the hash the browser now stands on arrives with that request (and with every other one, a fresh deep-link start included) in get( )-s_config-hash, so the app decides what to show. While registered, the framework leaves the hash entirely alone. Calling it without t_arg unregisters. The registration dies with an app switch - register it in view_display( ), so every render (a draft restore included) re-asserts it. Mutually exclusive with hash_routing (a routed app's hash belongs to the router) and with app_state_set_active (both claim the whole hash).", "**cs_event-hash_back** - the UI5 onNavBack pattern: without t_arg one real step back in the browser history (`window.history.go(-1)` - the step is CONSUMED, and the resulting hash change fires the registered event). With t_arg = a fallback hash it guards the cold deep link the way UI5's recommended onNavBack does: when this page load never pushed an app hash, there is no in-app step to take, so the fallback is written as a REPLACE instead of falling out of the app - and the change fires the registered event, which shows the fallback route: ``client->follow_up_action( val = client->cs_event-hash_back t_arg = VALUE #( ( `/` ) ) )``.", + "**cs_event-store_data** - write a structure into the browser's local or session storage, the write half of the invisible z2ui5:Storage control that reads it back. t_arg = the structure holding TYPE (`local`/`session`), PREFIX, KEY and VALUE - handed over as the BINDING of that structure, so the frontend takes the current value: ``client->follow_up_action( val = client->cs_event-store_data t_arg = VALUE #( ( client->_bind( ms_storage ) ) ) )``. It works from a view wire and from a handler alike: on a wire UI5 resolves the binding when the view is built, and a follow-up action queued in a handler carries the model PATH, which the frontend resolves when it runs. An empty VALUE removes the key.", "**cs_event-binding_call** - apply a declarative filter or sorter to an aggregation binding, the client-side equivalent of the UI5 controller pattern getBinding('items').filter(...); the model data stays untouched. t_arg = id, aggregation, method, params. Method `filter`: params = path, operator, value1, value2 (empty values clear the filter); method `sort`: params = path, descending, group (abap_bool as `X`/``): ``client->follow_up_action( val = client->cs_event-binding_call t_arg = VALUE #( ( `tab` ) ( `items` ) ( `filter` ) ( `NAME` ) ( `Contains` ) ( `ab` ) ) )``." ], "parameters": [ diff --git a/docs/resources/api.md b/docs/resources/api.md index dcd575f3..9b3fa760 100644 --- a/docs/resources/api.md +++ b/docs/resources/api.md @@ -207,6 +207,8 @@ Every one of them also works roundtrip-free when WIRED IN THE VIEW: write the sa **cs_event-hash_back** - the UI5 onNavBack pattern: without t_arg one real step back in the browser history (`window.history.go(-1)` - the step is CONSUMED, and the resulting hash change fires the registered event). With t_arg = a fallback hash it guards the cold deep link the way UI5's recommended onNavBack does: when this page load never pushed an app hash, there is no in-app step to take, so the fallback is written as a REPLACE instead of falling out of the app - and the change fires the registered event, which shows the fallback route: ``client->follow_up_action( val = client->cs_event-hash_back t_arg = VALUE #( ( `/` ) ) )``. +**cs_event-store_data** - write a structure into the browser's local or session storage, the write half of the invisible z2ui5:Storage control that reads it back. t_arg = the structure holding TYPE (`local`/`session`), PREFIX, KEY and VALUE - handed over as the BINDING of that structure, so the frontend takes the current value: ``client->follow_up_action( val = client->cs_event-store_data t_arg = VALUE #( ( client->_bind( ms_storage ) ) ) )``. It works from a view wire and from a handler alike: on a wire UI5 resolves the binding when the view is built, and a follow-up action queued in a handler carries the model PATH, which the frontend resolves when it runs. An empty VALUE removes the key. + **cs_event-binding_call** - apply a declarative filter or sorter to an aggregation binding, the client-side equivalent of the UI5 controller pattern getBinding('items').filter(...); the model data stays untouched. t_arg = id, aggregation, method, params. Method `filter`: params = path, operator, value1, value2 (empty values clear the filter); method `sort`: params = path, descending, group (abap_bool as `X`/``): ``client->follow_up_action( val = client->cs_event-binding_call t_arg = VALUE #( ( `tab` ) ( `items` ) ( `filter` ) ( `NAME` ) ( `Contains` ) ( `ab` ) ) )``. | Parameter | Type | Default | Description | diff --git a/docs/resources/deprecations.md b/docs/resources/deprecations.md index 381fdd45..7d854cb6 100644 --- a/docs/resources/deprecations.md +++ b/docs/resources/deprecations.md @@ -168,8 +168,8 @@ replacement has a sample that proves it: | What you used AJSON for | What to write instead | Sample | |---|---|---| -| Drop initial fields so the UI5 default applies (`create_empty_filter`) | `omit_initial`, or `omit_initial_paths` for single columns | [`Z2UI5_CL_SMP_APP_507`](https://github.com/abap2UI5/samples/blob/main/src/00/z2ui5_cl_smp_app_507.clas.abap) | -| Get a model **node** instead of a quoted string — under keys no ABAP component could be named after | `json = abap_true` | [`Z2UI5_CL_SMP_APP_509`](https://github.com/abap2UI5/samples/blob/main/src/00/z2ui5_cl_smp_app_509.clas.abap) | +| Drop initial fields so the UI5 default applies (`create_empty_filter`) | `omit_initial`, or `omit_initial_paths` for single columns | [`Z2UI5_CL_SMP_APP_507`](https://github.com/abap2UI5/samples/blob/main/src/01/z2ui5_cl_smp_app_507.clas.abap) | +| Get a model **node** instead of a quoted string — under keys no ABAP component could be named after | `json = abap_true` | [`Z2UI5_CL_SMP_APP_509`](https://github.com/abap2UI5/samples/blob/main/src/01/z2ui5_cl_smp_app_509.clas.abap) | | Anything else | Shape the value in ABAP before you bind it | — | ```abap @@ -378,14 +378,68 @@ still work; treat them as stable-but-closed rather than as something to migrate away from today. No page of this documentation uses them any more: what the examples reached them for is SAP standard — `cl_web_http_utility` for base64 (`cl_http_utility=>if_http_utility~encode_x_base64( )` on older releases), and -a failing library raises its own exception instead of being wrapped. JSON is -built and read by hand: compose it as a string in ABAP and bind it with -`_bind( val = ... json = abap_true )` on the way out, and read the one field -you need with `find` / `substring_before` on the way in — the payloads that -reach an app are written by the framework and are flat. There is deliberately -no released parser. +a failing library raises its own exception instead of being wrapped. For the +JSON they were also reached for, see the section below. ::: +### JSON is built and read by hand + +**There is deliberately no released JSON parser or writer, and there is no +substitute to reach for.** `/ui2/cl_json` is not released for ABAP Cloud, +`xco_cp_json` is missing on 7.02, and the bundled [ajson](/technical/tools/ajson) +is the framework's model engine — a mirrored copy of an external project, which +the abap2UI5 linter reports as `non-released-api` when an app names it, and it +is right to. + +There was a released reader, `z2ui5_cl_ui5_json`, for two weeks in September +2026. It was removed again before it had shipped in any release, so no +installation ever had it and nothing has to be migrated away from it. + +**Outbound — a control property that must receive an object.** A +`sap.ui.integration` Card manifest is the case this exists for: its keys +(`sap.app`, `sap.card`) are not valid ABAP field names, so no typed ABAP value +can be that object, and UI5 reads a *string* manifest as a manifest URL. Keep +the JSON in a `string` attribute, compose it in ABAP, and bind it as a node: + +```abap +client->_bind( val = mv_card_manifest json = abap_true ) +``` + +A string that does not parse raises rather than shipping broken JSON to the +frontend. This direction is outbound only — the attribute is not read back, so +ABAP stays the single author of it. Where the payload has a URL instead, bind +that; it needs no flag. + +**Inbound — an event argument that arrives as JSON.** Write the few lines that +read the field you need. `Z2UI5_CL_SMP_APP_327` +([`json_get_value`](https://github.com/abap2UI5/samples/blob/main/src/01/z2ui5_cl_smp_app_327.clas.abap), +one field of a flat object) and `Z2UI5_CL_SMP_APP_197` +([`json_get_values`](https://github.com/abap2UI5/samples/blob/main/src/01/z2ui5_cl_smp_app_197.clas.abap), +one property across an array of objects) are the pattern to copy: find +`"":"` and take what stands up to the next quote. + +Two limits worth knowing before you copy it: + +- **It is a reader for FLAT payloads.** A control-valued event parameter is + marshaled as an object of the control's `ID` plus its public properties, and + a property whose value is itself an object or an array travels as one — the + frontend passes such a value through rather than flattening it. A `find` walk + answers the flat case; a nested one is a sign to bind the value into the model + instead of parsing it out of an event argument, which is what a two-way bound + attribute does for you with no parsing at all. +- **A payload composed from user input needs escaping on both ends.** The + samples above read what the *framework* wrote. If your own app also writes the + JSON — publishing into an AMC channel, say — then a quote the user typed + reaches the reader as `\"`, and a `substring_before` on the next quote ends + the value early. + `Z2UI5_CL_SMPS_APP_489` + ([samples-stack](https://github.com/abap2UI5/samples-stack/blob/main/src/07/z2ui5_cl_smps_app_489.clas.abap)) + is that case written out: a writer that escapes and a reader that walks the + value resolving escapes. + +An app parsing genuinely arbitrary, nested JSON is doing something this +framework does not hand it a tool for. + ### Invisible custom controls Earlier versions of abap2UI5 needed an invisible helper control in the view for diff --git a/docs/technical/tools/ajson.md b/docs/technical/tools/ajson.md index a1a2e673..2e90fd86 100644 --- a/docs/technical/tools/ajson.md +++ b/docs/technical/tools/ajson.md @@ -13,18 +13,20 @@ The solution: [ajson](https://github.com/sbcgua/ajson), a solid open-source JSON - **Developer-Friendly** — Clear API with thorough documentation - **Active Maintenance** — Responsive issue resolution and ongoing improvement -## Integration +## It is the framework's engine, not an API for your app -ajson plugs directly into the `z2ui5` namespace and installs with abap2UI5 automatically. Use it anywhere in your code: -```abap -DATA(input) = z2ui5_cl_ajson=>parse( json_string ). +ajson is what serializes the model on every roundtrip and what reads the delta back — `z2ui5_cl_ui5_srv_model` is built on it, and so is `_bind( json = abap_true )`. That is where it belongs. -DATA(output) = z2ui5_cl_ajson=>create_empty( )->set( - iv_path = `/name` - iv_val = `value` -)->stringify( ). -``` +What ships under `z2ui5_cl_ajson` is a **mirrored copy** of an external project (`src/00/01`), not a contract abap2UI5 owns. It sits outside the released API, it is resynced from upstream automatically (see below), and a resync is free to change it. An app that calls it binds itself to whatever the mirror looks like today — which is why the abap2UI5 linter reports such a call as `non-released-api`, correctly. + +::: warning Do not call it from app code +Nothing in an app class should name `z2ui5_cl_ajson`, `z2ui5_cx_ajson_error`, `z2ui5_if_ajson_mapping` or `z2ui5_if_ajson_filter`. There is no released JSON parser to use instead, and that is deliberate — see [JSON is built and read by hand](/resources/deprecations#json-is-built-and-read-by-hand) for what to write in both directions. +::: + +Nor is there a substitute to reach for: `/ui2/cl_json` is not released for ABAP Cloud and `xco_cp_json` is missing on 7.02 — the two reasons the framework vendored a library in the first place. ## Automatic Updates Every ajson update and bug fix flows into abap2UI5 automatically via GitHub Actions and the [mirror-ajson](https://github.com/abap2UI5/mirror-ajson) repository. You always run the latest stable version — no manual steps needed. + +That automation is the other half of the argument above: the copy in your system moves without anybody in this project reviewing the diff for app-facing impact, because nothing app-facing is supposed to depend on it.