diff --git a/AGENTS.md b/AGENTS.md
index 99d9f32..e933684 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -20,7 +20,8 @@ VitePress build. It is also what CI runs, on every pull request
"@cap2ui5/cds-plugin"` **inside a code fence** names only what the package
really exports (and two dead packages are reported, in either form: the
port's `abap2UI5/…` and the plugin's withdrawn old name `cap2ui5`),
-- every `cds.cap2ui5.` is an option the plugin defines,
+- every `cds.requires.cap2ui5. ` is an option the plugin defines (and
+ 0.1.0's `cds.cap2ui5. ` is reported as the deprecated place),
- every `1.x.y` release number is the pinned runtime release,
- every internal anchor exists.
@@ -84,16 +85,21 @@ There is **no static frontend route** and no `webapp` option either. The page
the roundtrip route answers a GET with embeds the whole UI5 component, so the
plugin serves no frontend files (`/z2ui5/webapp/index.html` answers 404, and
cap2UI5's `scripts/consumer-test.mjs` asserts it). `plugin/package.json`
-defines `apps`, `requires` and `routes` under `cds.cap2ui5`, nothing else.
+defines `apps`, `roles` and `routes` under `cds.requires.cap2ui5` (next to
+CAP's own `model`); the plugin also reads `body_parser.limit`, which has no
+default there. 0.1.0's top-level `cds.cap2ui5`, with `requires` for the
+roles, is still read with a deprecation warning — the site teaches only the
+new place.
Path conventions inside cap2UI5:
- `plugin/` — the package: `cds-plugin.js` (the route, the auth guard, the
startup lines naming each app's address), `index.cds` (the `cap2ui5.Drafts`
entity), `index.js` (what `import`/`require` of `@cap2ui5/cds-plugin`
- returns) and `lib/`
- (`define-app.js`, `define-exit.js`, `draft-store.js`, `hints.js`,
- `runtime.js`)
+ returns), `index.d.ts` (its TypeScript declarations), `bin/cap2ui5.js`
+ (`npx cap2ui5 abap2js`) and `lib/` (`abap2js.js`, `add.js`, `config.js`,
+ `define-app.js`, `define-exit.js`, `draft-store.js`, `hints.js`,
+ `runtime.js`, `view-builder.js`)
- `examples/bookshop/` — a CAP project using it, with the test suite. Its apps
are in `examples/bookshop/srv/apps/`
- `runtime/` — a stand-in for `@abap2ui5/node-runtime`. Only `package.json`
@@ -103,7 +109,7 @@ Path conventions inside cap2UI5:
What a READER's project looks like is a different thing and must not be
confused with the above: they install `@cap2ui5/cds-plugin`, write apps in
-`srv/apps/` (configurable via `cds.cap2ui5.apps`), may add packages that bring
+`srv/apps/` (configurable via `cds.requires.cap2ui5.apps`), may add packages that bring
apps (`@cap2ui5/samples` for one), and get the route (whose GET page
embeds the UI5 frontend) and the draft entity from the plugin. A project from
`cds init --nodejs` is an ES module project, so the site's examples `import`
@@ -118,9 +124,15 @@ against the cap2UI5 repository.
- The framework's own classes are **not a supported import**. Importing
`@abap2ui5/node-runtime/output/…` technically works — the package exports
`./output/*` — but it couples an app to transpiler output. A JS app imports
- `@cap2ui5/cds-plugin` and nothing else; `c.raw` is the escape hatch to the transpiled
- `z2ui5_if_client`. An app that wants the framework's ABAP API (the view
- builder, for one) is written in ABAP and transpiled — see the views guide.
+ `@cap2ui5/cds-plugin` and nothing else. Since 0.2.0 that is enough: the
+ client an app's `main( client )` receives is `z2ui5_if_client` by its own
+ method names, and the plugin exports `z2ui5_cl_ui5_view_builder` and the
+ interface's constants. `client.raw` is the escape hatch to the transpiled
+ `z2ui5_if_client`.
+- Teach the client by its ABAP names (`client.check_on_navigated()`,
+ `client._bind("name")`, `client.view_display(xml)`). The 0.1.0 names
+ (`c.isDisplay`, `c.bind`, `c.view`, …) throw since 0.2.0 and appear only
+ where a page explains migrating from 0.1.0.
- Measure before documenting a framework behaviour. The runtime is upstream's
ABAP running on open-abap, and not everything upstream does works here —
the user exit is discovered by a class-repository lookup in ABAP and had to
diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index a99fb27..02e4f2d 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -138,7 +138,7 @@ export default defineConfig({
{
text: 'API Reference',
items: [
- { text: 'c — the client facade', link: '/api/client' },
+ { text: 'client — z2ui5_if_client', link: '/api/client' },
{ text: 'App Interface', link: '/api/app-interface' }
]
}
diff --git a/docs/api/app-interface.md b/docs/api/app-interface.md
index c68488a..9d2976f 100644
--- a/docs/api/app-interface.md
+++ b/docs/api/app-interface.md
@@ -8,20 +8,24 @@ import { defineApp, defineExit, t } from "@cap2ui5/cds-plugin";
cannot be inferred from a literal; `defineExit` registers the one user exit —
it has [a page of its own](../guide/user-exit).
+The package also exports `z2ui5_cl_ui5_view_builder` — abap2UI5's view
+builder, see [Views](../guide/views) — and `z2ui5_if_client`, the client's
+constants as an ABAP app reads them (`z2ui5_if_client.cs_event.set_title`).
+
## `defineApp(name, class, opts?)`
```js
defineApp("ZCL_HELLO", class {
name = "";
- main(c) { /* … */ }
+ main(client) { /* … */ }
});
```
| | |
|---|---|
-| `name` | the app's name **on the wire** — what `?app_start=` takes and `c.navTo()` resolves. Uppercased. The file name is irrelevant |
-| `class` | a plain class with a `main(c)` method and its state as fields |
+| `name` | the app's name **on the wire** — what `?app_start=` takes and `client.nav_app_call()` resolves. Uppercased. The file name is irrelevant |
+| `class` | a plain class with a `main(client)` method and its state as fields |
| `opts.interfaces` | rarely needed; defaults to `["Z2UI5_IF_APP", "IF_SERIALIZABLE_OBJECT"]` |
It returns the wrapped class and registers it, so `defineApp` may be called
@@ -33,13 +37,16 @@ Without a `main`, it throws:
defineApp(ZCL_HELLO): the class needs a main( client ) method
```
-### `main(c)` is synchronous
+`client` is abap2UI5's `z2ui5_if_client`, by its own method names — see
+[`client`](./client).
+
+### `main(client)` is synchronous
No `async`, no `await` for anything the framework offers. Make it `async` only
when **your app** does I/O:
```js
-async main(c) {
+async main(client) {
this.books = await SELECT.from(cds.entities("my.bookshop").Books);
}
```
@@ -50,7 +57,39 @@ The wrapper awaits it either way.
Every field with an initial value becomes part of the model and survives the
roundtrip. A field the plugin cannot type is left out and **named in a
-warning**, never silently dropped.
+warning**, never silently dropped. Unlike ABAP there is no `PROTECTED
+SECTION`: every field is model.
+
+Inside `main` and inside any method it calls, fields read and write as plain
+values. A helper method that needs the client gets it as an ABAP app does —
+`this.client = client` in `main`. Assigned there rather than declared as a
+field, it is not part of the model or the draft:
+
+```js
+defineApp("ZCL_HELLO", class {
+ name = "";
+
+ view_display() {
+ this.client.view_display(`` +
+ ` `);
+ }
+
+ main(client) {
+ this.client = client;
+ if (client.check_on_navigated()) this.view_display();
+ }
+});
+```
+
+### Types in the editor
+
+The package ships TypeScript declarations. In a JavaScript app, annotate the
+client for completion and checked field names:
+
+```js
+/** @param {import("@cap2ui5/cds-plugin").Client<{ search: string }>} client */
+main(client) { /* with type checking on, client._bind("serach") is an error */ }
+```
## `t` — the type declarations
@@ -65,13 +104,17 @@ is not, declare it:
| `t.bool()` | `abap_bool`; the app sees `true`/`false` — same as `false` |
| `t.char(len)` | fixed-width character |
| `t.packed(len, dec)` | **packed decimal.** Use this for money |
+| `t.numc(len)` | ABAP's `N`: digits, kept with their leading zeros. A string |
+| `t.date()` | ABAP's `D`, `YYYYMMDD`. A string |
+| `t.time()` | ABAP's `T`, `HHMMSS`. A string |
| `t.struct({…})` | a structure. A plain object literal is one implicitly |
-| `t.table(row)` | a table; the argument is one **row** |
+| `t.table(row)` | a table; the argument is one **row**, the initial value is empty |
```js
price = t.packed(9, 2);
books = t.table({ ID: 0, title: "", price: t.packed(9, 2) });
addr = { street: "", zip: 0 }; // t.struct is implicit here
+cfg = t.struct({ mode: "list", items: [{ key: "x" }] });
```
Numbers are the ambiguity worth knowing: ABAP has `I`, `P` and `F` and they
@@ -79,8 +122,12 @@ render with different decimals, so an integer literal becomes `I`, a fractional
one `F`, and a decimal amount has to say so with `t.packed()`. Guessing would
produce a view with the wrong number of decimals and nothing to point at.
+Rows in a field initializer are its initial value — `rows = [{ id: 1, title: "first" }]`
+is a table typed from its first row, starting with those rows.
+
Structures and tables nest, to a depth of 8 — a cycle guard rather than a
-judgement. See [Data Binding](../guide/data-binding).
+judgement. Component names are UPPERCASE in the model. See
+[Data Binding](../guide/data-binding).
## `defineExit(exit)`
@@ -101,6 +148,6 @@ registered rather than discovered: [The User Exit](../guide/user-exit).
## Next
-- [**`c` — the client facade**](./client) — what `main` receives
+- [**`client` — `z2ui5_if_client`**](./client) — what `main` receives
- [**Data Binding**](../guide/data-binding) — the types in practice
- [**The User Exit**](../guide/user-exit) — `defineExit` in full
diff --git a/docs/api/client.md b/docs/api/client.md
index 6696a94..6a76c7a 100644
--- a/docs/api/client.md
+++ b/docs/api/client.md
@@ -1,89 +1,190 @@
-# API: `c` — the client facade
+# API: `client` — `z2ui5_if_client`
-The single argument `main` receives. Every member is **synchronous**: queries
-are resolved before `main` runs, commands are recorded and replayed after it.
+The single argument `main` receives. It is abap2UI5's `z2ui5_if_client`, by
+its own method names: `client->check_app_prev_stack( )` is
+`client.check_app_prev_stack()`. [abap2UI5's documentation](https://abap2ui5.github.io/docs/)
+of a method is therefore the documentation of the JavaScript one; this page
+lists them and says where JavaScript differs.
```js
defineApp("ZCL_X", class {
- main(c) { /* c is this page */ }
+ main(client) { /* client is this page */ }
});
```
-## Lifecycle
+Two rules for every method:
+
+- **Parameters.** A method's preferred parameter is its one positional
+ argument; parameters by name are **one object** with the ABAP names.
+ ``client->_event( val = `GO` t_arg = … )`` is
+ `client._event({ val: "GO", t_arg: [ … ] })`. A second positional argument,
+ or a name the method does not have, throws and lists the parameters.
+- **Synchronous.** Queries are resolved before `main` runs; commands are
+ recorded and carried out **in the order you called them** after it returns.
+ No `await` for anything the client offers.
+
+## Lifecycle and queries
+
+| | |
+|---|---|
+| `client.check_on_init()` | `true` on the first roundtrip of **this app instance**, and only that one. Seed state here |
+| `client.check_on_navigated()` | `true` on the first roundtrip **and** every time this app gets the screen back — a called app leaving, a value help closing, a bookmark restored. **Render here** |
+| `client.check_on_event(val)` | whether this roundtrip answers the event `val`; without `val`, whether it answers any event |
+| `client.check_app_prev_stack()` | `true` if there is an app to return to — for a Page's `showNavButton` |
+| `client.get_event()` | the event this roundtrip answers; `""` on an app start |
+| `client.get_event_arg(v)` | the *v*-th event argument, **1-based**, `1` by default. The first 8 are resolved up front; beyond that, `client.raw` |
+| `client.get()` | what the frontend sent with this roundtrip, as plain values under the ABAP component names: `event`, the draft ids (`s_draft`), the browser location (`s_config`), device, focus and scroll information, and `r_event_data` — what a returning app handed over |
+| `client.get_app_prev()` | the app on the other side of the last navigation — inside a called app its caller, back in the caller the app that just returned. A `defineApp` app as its fields' plain values, an ABAP app as the instance |
+| `client.get_app()` | without an id, the running app itself |
+| `client.get_app(id)` | the app behind a draft id. It is read from the draft store **after** `main`, so its fields can be written, not read; hand it to `nav_app_leave()` or `nav_app_call()` |
+| `client.app_state_get_href()` | the absolute link to this app's current state |
+
+`check_on_init()` implies `check_on_navigated()`. See [App Lifecycle](../guide/lifecycle)
+for why rendering only on `check_on_init()` produces a screen that silently
+does not refresh.
+
+## Binding
| | |
|---|---|
-| `c.isFirstRun` | `true` on the first roundtrip of **this app instance**, and only that one. Seed state here |
-| `c.isDisplay` | `true` on the first roundtrip **and** every time this app gets the screen back — a called app leaving, a value help closing, a bookmark restored. **Render here** |
-| `c.canGoBack` | `true` if there is an app to return to. Guard `navBack` with it |
-| `c.eventName` | the event this roundtrip answers; `""` on an app start |
-| `c.eventArg(i)` | the *i*-th event argument, **1-based**. The first 8 are resolved up front; beyond that use `c.raw` |
-| `c.prevApp` | the app on the other side of the last navigation, as plain values — how a called app's result is read |
+| `client._bind(name)` | the binding of a field for a view attribute: `{/NAME}`. Takes the field's **name**, since a JavaScript value cannot carry a reference. Throws, listing the known fields, if it is not one |
+| `client._bind("s_order-customer")` | a component of a structure field, named as ABAP names it (`.` works too) |
+| `client._bind({ val, path: true })` | the bare path `/NAME`, for a composed binding |
+| `client._bind({ val: column, tab, tab_index })` | one cell of a table field; `tab_index` is the row, 1-based |
+| `client._bind({ val, omit_initial, omit_initial_paths, json, switch_default_model })` | `_bind( )`'s options, as in ABAP |
+| `client._bind_path(name)` | the same as `_bind({ val: name, path: true })` |
+
+```js
+` `
+``
+` `
+```
-`isFirstRun` implies `isDisplay`. See [App Lifecycle](../guide/lifecycle) for
-why confusing them produces a screen that silently does not refresh.
+A plain `_bind(name)` answers the binding itself. A component, a cell or an
+option is registered by the framework after `main` returns, so it answers a
+**placeholder** — embed it as it is, do not parse or compare it. See
+[Data Binding](../guide/data-binding).
-## Binding and events
+## Events and front-end actions
| | |
|---|---|
-| `c.bind(field)` | the binding path for a declared field. Takes the field **name** as a string. Throws, listing the known fields, if it is not one |
-| `c.event(name, args = [])` | the wire string for an event handler. `args` travel with it and come back as `c.eventArg(1..n)` |
+| `client._event(val)` | the handler of an event, for a view attribute |
+| `client._event({ val, t_arg, arg, s_ctrl })` | with arguments: `t_arg` come back as `get_event_arg(1..n)`, `arg` is one more behind them. `s_ctrl` is `ty_s_event_control` by component name: `check_prevent_default`, `prevent_default_expr`, `check_arg_literal`, `check_queue_last`, `check_no_busy` |
+| `client._event_nav_app_leave()` | the handler that leaves this app — a Page's `navButtonPress`, with no branch in `main` |
+| `client.follow_up_action({ val, t_arg, view })` | a front-end action: `val` a `cs_event` constant, `view` the `cs_view` slot its control ids are meant in. **Embedded** in a view attribute it is a handler that runs in the browser, no roundtrip; **called on its own** it runs when this roundtrip's answer lands |
```js
-` `
-` `
+` `
+` `
+
+client.follow_up_action({ val: client.cs_event.set_title, t_arg: [this.title] });
```
-The value `c.event` returns is a handler expression — embed it, do not parse or
-compare it.
+What `_event()`, `_event_nav_app_leave()` and an embedded `follow_up_action()`
+return is a placeholder, replaced by the wire after `main` — embed it, do not
+parse or compare it. A `cs_event` or `cs_view` value the runtime does not have
+is refused with the list. See [Events](../guide/events).
+
+## Views, popups, popovers, nested views
-## Screen
+A view is XML text, a [`z2ui5_cl_ui5_view_builder`](../guide/views) chain, or
+what its `stringify()` answered.
| | |
|---|---|
-| `c.view(xml)` | render the main view. An `mvc:View` |
-| `c.popup(xml)` | a dialog on top. A `core:FragmentDefinition` |
-| `c.popupClose()` | close it |
-| `c.nest(into, xml, {insert, clear})` | render a fragment **into** a control of the main view. `into` is that control's id; `insert`/`clear` default to `addContent`/`removeAllContent` |
-| `c.nestClose()` | clear the nested slot. There is exactly one, so it takes no argument |
-| `c.messageBox(text)` | a dialog with an OK button |
-| `c.messageToast(text)` | a transient toast |
+| `client.view_display(val)` | render the main view. An `mvc:View` |
+| `client.view_destroy()` | remove it |
+| `client.popup_display(val)` | a dialog on top. A `core:FragmentDefinition` |
+| `client.popup_destroy()` | close it |
+| `client.popover_display({ xml, by_id })` | a popover anchored to the control whose id is `by_id` |
+| `client.popover_destroy()` | close it |
+| `client.nest_view_display({ val, id, method_insert, method_destroy })` | render a view **into** the control `id` of the main view, which stays as it is. `method_insert` (e.g. `addContent`) is required; `method_destroy` (e.g. `removeAllContent`) clears what was there first — without it, every call adds one more |
+| `client.nest_view_destroy()` | clear the nested slot |
+| `client.nest2_view_display({ … })`, `client.nest2_view_destroy()` | the second nested slot, with the same contract |
+
+```js
+client.nest_view_display({
+ val: `… `,
+ id: "slot",
+ method_insert: "addContent",
+ method_destroy: "removeAllContent",
+});
+```
+
+See [Views](../guide/views) and [Popups](../guide/popups).
-Commands are replayed **in the order you called them**.
+## Messages
+
+| | |
+|---|---|
+| `client.message_box_display(text)` | a dialog with an OK button. `text` may be data — an object, an array — laid out as for an ABAP structure or table |
+| `client.message_box_display({ text, type, title, styleclass, onclose, actions, emphasizedaction, initialfocus, details })` | with its options; an object with a `text` key is the parameters by name |
+| `client.message_toast_display(text)` | a transient toast |
+| `client.message_toast_display({ text, duration, onclose })` | with its options |
## Navigation
| | |
|---|---|
-| `c.navTo(app)` | show another app on top of this one. Takes a registered name, a `defineApp` class, or an instance |
-| `c.navBack(opts)` | hand the screen back. `{ event, data, app }`, all optional |
+| `client.nav_app_call(app)` | show another app on top of this one. `app` is a registered name, a `defineApp` class, an instance, or what `get_app(id)` answered |
+| `client.nav_app_call(app, fields)` | cap2UI5's own: preset fields of the called app — what an ABAP app does between `NEW` and `nav_app_call( )`. An unknown field throws |
+| `client.nav_app_leave()` | hand the screen back to the caller |
+| `client.nav_app_leave({ app, event, r_data })` | … or to `app`. `event` is what the caller finds in `get_event()`, `r_data` what it finds in `get().r_event_data` — typed, object keys lowercase |
-Both are scheduled for the end of the roundtrip.
+Both are scheduled for the end of the roundtrip, so they are usually the last
+thing a branch does. See [Navigation](../guide/navigation).
-## Escape hatch
+## Hash and app state
| | |
|---|---|
-| `c.raw` | the underlying `z2ui5_if_client`, unwrapped. **Async** — every call needs `await` |
+| `client.hash_set(val)` | push a hash onto the browser history |
+| `client.hash_replace(val)` | rewrite the hash without a history entry |
+| `client.app_state_set_active(val = true)` | keep this app's state id in the URL |
+| `client.app_state_get_href()` | the link to this state — see [Lifecycle and queries](#lifecycle-and-queries) |
-Use it for anything the facade does not cover. Its methods are the ABAP ones
-with `~` written as `$`:
+## Constants
+
+`client.cs_event`, `client.cs_view`, `client.cs_nav_mode`, `client.cs_device`
+are the interface's constant structures. The same are exported as
+`z2ui5_if_client`, as an ABAP app reads them:
```js
-await c.raw.z2ui5_if_client$set_session_stateful({ val: abap.builtin.abap_true });
+import { z2ui5_if_client } from "@cap2ui5/cds-plugin";
+
+client.follow_up_action({ val: z2ui5_if_client.cs_event.location_reload });
```
-## Removed, and why
+## Escape hatch
-Both throw an error naming the replacement rather than quietly changing meaning:
+| | |
+|---|---|
+| `client.raw` | the transpiled `z2ui5_if_client` itself. **Async** — every call needs `await` |
+
+Its methods are the ABAP ones with `~` written as `$`, and they answer ABAP
+values:
+
+```js
+const ninth = (await client.raw.z2ui5_if_client$get_event_arg({ v: 9, result: 1 })).get();
+```
+
+## Obsolete and unsupported
| | |
|---|---|
-| `c.isInitial` | it was wired to `check_on_navigated()` and named after `check_on_init()`. Use `c.isDisplay` / `c.isFirstRun` |
-| `c.modelUpdate()` | it called `view_model_update()`, which the framework declares **obsolete and does nothing** |
+| `view_model_update()`, `popup_model_update()`, `popover_model_update()`, `nest_view_model_update()`, `nest2_view_model_update()` | obsolete in `z2ui5_if_client`; they **do nothing**, here as there. Changed bound data is pushed on its own |
+| `_bind_edit()` | obsolete — `_bind()` under another name |
+| `_event_client()` | obsolete — the embedded `follow_up_action()` under another name |
+| `set_session_stateful()` | **not supported**, throws. The app's state is in its fields, which are in the draft |
+
+The names of cap2UI5 0.1.0 (`c.isDisplay`, `c.bind()`, `c.navTo()` …) throw
+an error naming the method that replaces each, rather than answering
+`undefined`. The full mapping is in the plugin's
+[CHANGELOG](https://github.com/cap2UI5/cap2UI5/blob/main/plugin/CHANGELOG.md), under 0.2.0.
## Next
-- [**App Interface**](./app-interface) — `defineApp` and `t`
+- [**App Interface**](./app-interface) — `defineApp`, `t` and `defineExit`
- [**App Lifecycle**](../guide/lifecycle) — when each branch runs
diff --git a/docs/examples/external-odata.md b/docs/examples/external-odata.md
index abbf41f..4017522 100644
--- a/docs/examples/external-odata.md
+++ b/docs/examples/external-odata.md
@@ -53,8 +53,8 @@ defineApp("ZCL_NORTHWIND", class {
products = t.table({ ProductID: 0, ProductName: "", UnitPrice: t.packed(11, 2) });
status = "";
- async main(c) {
- if (c.eventName === "LOAD") {
+ async main(client) {
+ if (client.check_on_event("LOAD")) {
try {
const nw = await cds.connect.to("Northwind");
this.products = await nw.run(SELECT.from("Products").limit(20));
@@ -62,17 +62,17 @@ defineApp("ZCL_NORTHWIND", class {
} catch (e) {
// a remote call fails in ways a local one does not
this.status = "the service did not answer";
- c.messageBox(`Northwind unreachable: ${e.message}`);
+ client.message_box_display({ text: `Northwind unreachable: ${e.message}`, type: "error" });
}
}
- if (c.isDisplay) {
- c.view(
+ if (client.check_on_navigated()) {
+ client.view_display(
`` +
`` +
- ` ` +
- ` ` +
- `` +
+ ` ` +
+ ` ` +
+ `` +
` ` +
`` +
` ` +
diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md
index fb82446..c940d1a 100644
--- a/docs/examples/hello-world.md
+++ b/docs/examples/hello-world.md
@@ -2,8 +2,11 @@
The smallest complete cap2UI5 app. This is
[`examples/bookshop/srv/apps/hello.js`](https://github.com/cap2UI5/cap2UI5/blob/main/examples/bookshop/srv/apps/hello.js)
-verbatim — it is exercised by the wire tests, the cold-restart test and the
-browser test on every CI run.
+in ES-module form, without its comments. The bookshop is a CommonJS project, so
+the file itself starts with `require("@cap2ui5/cds-plugin")`; a project from
+`cds init --nodejs` is an ES module project and `import`s the same names. The
+app is exercised by the wire tests, the cold-restart test and the browser test
+on every CI run.
```js
// srv/apps/hello.js
@@ -12,39 +15,44 @@ import { defineApp } from "@cap2ui5/cds-plugin";
defineApp("ZCL_JS_HELLO", class {
name = "";
- main(c) {
- if (c.isDisplay) {
- c.view(
+ main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(
`` +
`` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
` `);
return;
}
- if (c.eventName === "GO") c.messageBox(`Hello ${this.name}`);
+ if (client.check_on_event("GO")) client.message_box_display(`Hello ${this.name}`);
}
});
```
-Run it:
+Run it — `cds watch` prints the address, and the user to log in as:
```
-http://localhost:4004/rest/root/z2ui5?app_start=ZCL_JS_HELLO
+http://localhost:4004/sap/bc/z2ui5?app_start=ZCL_JS_HELLO
```
## Line by line
| | |
|---|---|
-| `import { … } from "@cap2ui5/cds-plugin"` | the plugin's export surface: `defineApp`, `t`, and `defineExit` for the [user exit](../guide/user-exit) |
+| `import { … } from "@cap2ui5/cds-plugin"` | what the plugin exports: `defineApp`, `t`, the view builder `z2ui5_cl_ui5_view_builder`, the `z2ui5_if_client` constants, `defineExit` for the [user exit](../guide/user-exit), and `abap2js` |
| `defineApp("ZCL_JS_HELLO", …)` | the first argument is the name **on the wire** — what `?app_start=` takes. The file name does not matter |
| `name = ""` | app state. An empty string types it as `string`, and it survives the roundtrip because the instance is written to `cap2ui5.Drafts` |
-| `main(c)` | synchronous — no `async`, no `await` |
-| `c.isDisplay` | the render branch. True on the first roundtrip **and** whenever the app gets the screen back |
-| `c.bind("name")` | the binding path — two-way, so what the user types arrives on `this.name` |
-| `c.event("GO")` | the handler expression; the next roundtrip has `c.eventName === "GO"` |
-| `c.messageBox(…)` | a dialog with an OK button |
+| `main(client)` | synchronous — no `async`, no `await`. `client` is abap2UI5's `z2ui5_if_client`, by its ABAP method names |
+| `client.check_on_navigated()` | the render branch. True on the first roundtrip **and** whenever the app gets the screen back |
+| `client._bind("name")` | the binding path — two-way, so what the user types arrives on `this.name` |
+| `client._event("GO")` | the handler expression; on the next roundtrip `client.check_on_event("GO")` is true |
+| `client.message_box_display(…)` | a dialog with an OK button |
+
+The ABAP app it corresponds to calls `client->check_on_navigated( )`,
+`client->_bind( name )`, ``client->_event( `GO` )`` and
+`client->message_box_display( … )` — the same methods. The full list is the
+[client API](../api/client).
## What happens when you click
@@ -63,27 +71,28 @@ defineApp("ZCL_JS_HELLO", class {
name = "";
count = 0;
- main(c) {
- if (c.isDisplay) {
- c.view(
+ main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(
`` +
`` +
- ` ` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
+ ` ` +
` `);
return;
}
- if (c.eventName === "GO") {
+ if (client.check_on_event("GO")) {
this.count++;
- c.messageToast(`Hello ${this.name}, click ${this.count}`);
+ client.message_toast_display(`Hello ${this.name}, click ${this.count}`);
}
}
});
```
`count` needs no re-render: changed **bound data** is pushed to the view on its
-own. You call `c.view()` again only when the view's *structure* changes.
+own. You call `client.view_display()` again only when the view's *structure*
+changes.
The counter also survives a server restart — the state is a row in your
database, not memory. See [Persistence](../guide/persistence).
@@ -91,5 +100,5 @@ database, not memory. See [Persistence](../guide/persistence).
## Next
- [**List & Detail**](./list) — a table filled from your own CDS entity
-- [**App Lifecycle**](../guide/lifecycle) — `isDisplay` vs. `isFirstRun`
+- [**App Lifecycle**](../guide/lifecycle) — `check_on_navigated()` vs. `check_on_init()`
- [**Data Binding**](../guide/data-binding) — the types
diff --git a/docs/examples/list.md b/docs/examples/list.md
index 9f5b26f..b21dc1c 100644
--- a/docs/examples/list.md
+++ b/docs/examples/list.md
@@ -2,8 +2,9 @@
A table filled from your own CDS entity, and a row that can be written back.
This is [`examples/bookshop/srv/apps/books.js`](https://github.com/cap2UI5/cap2UI5/blob/main/examples/bookshop/srv/apps/books.js)
-— the app the coexistence test drives, so both directions below are measured
-rather than sketched.
+in ES-module form (the bookshop itself is CommonJS and `require`s) — the app the
+coexistence test drives, so both directions below are measured rather than
+sketched.
```js
// srv/apps/books.js
@@ -16,37 +17,38 @@ defineApp("ZCL_JS_BOOKS", class {
hits = 0;
books = t.table({ ID: 0, title: "", author: "", price: t.packed(9, 2) });
- async main(c) {
- if (c.isDisplay) {
- c.view(
+ async main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(
`` +
`` +
- ` ` +
- `` +
+ ` ` +
+ `` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
``);
return;
}
- if (c.eventName === "SEARCH") {
- const { Books } = cds.entities("my.bookshop");
- this.books = await SELECT.from(Books).where`title like ${"%" + this.search + "%"}`;
- this.hits = this.books.length;
- c.messageToast(`${this.hits} found`);
- }
-
- if (c.eventName === "ADD") {
+ if (client.check_on_event("ADD")) {
const { Books } = cds.entities("my.bookshop");
const max = await SELECT.one.from(Books).columns("max(ID) as m");
await INSERT.into(Books).entries({
ID: (max?.m ?? 0) + 1, title: this.search, author: "the app", stock: 1, price: 1.0,
});
- c.messageToast(`added ${this.search}`);
+ client.message_toast_display(`added ${this.search}`);
+ return;
+ }
+
+ if (client.check_on_event("SEARCH")) {
+ const { Books } = cds.entities("my.bookshop");
+ this.books = await SELECT.from(Books).where`title like ${"%" + this.search + "%"}`;
+ this.hits = this.books.length;
+ client.message_toast_display(`${this.hits} found`);
}
}
});
@@ -55,20 +57,22 @@ defineApp("ZCL_JS_BOOKS", class {
## The three things worth copying
**`main` is `async` here** — because the *app* does I/O. The framework calls
-still need no `await`; `SELECT` does.
+(`client._bind()`, `client.message_toast_display()`, …) still need no
+`await`; `SELECT` does.
**The table is declared, not inferred.** `t.table({…})` names the row, and
`t.packed(9, 2)` makes `price` a decimal. An empty array carries no type, so a
bare `books = []` would be left out of the model and named in a warning.
**Assign the whole array.** `this.books = await SELECT…` replaces the table and
-the plugin rebuilds the rows. Mutating the array you read back does not write
+the plugin rebuilds the rows — and pushes them to the view without a
+re-render. Mutating the array you read back does not write
through.
## Row fields are uppercase
Inside the table's template the cells bind `{TITLE}`, `{AUTHOR}`, `{PRICE}` —
-uppercase, and relative to the row, so they take no `c.bind`. Component names
+uppercase, and relative to the row, so they take no `client._bind()`. Component names
are stored lowercase, as the transpiler does, and appear uppercase in the model.
## `cds.ql` is the whole data layer
@@ -99,8 +103,9 @@ somebody's OData service would be the worst kind of surprise.
## A detail screen
For a second screen with its own state, call another app rather than growing
-this one — see [Navigation](../guide/navigation). The callee's result comes back
-through `c.prevApp`.
+this one — see [Navigation](../guide/navigation). When the callee leaves, the caller
+reads it through `client.get_app_prev()`, or the typed `client.get().r_event_data`
+if it left with `nav_app_leave({ event, r_data })`.
## Next
diff --git a/docs/examples/selection-screen.md b/docs/examples/selection-screen.md
index 8a393f6..b4a135d 100644
--- a/docs/examples/selection-screen.md
+++ b/docs/examples/selection-screen.md
@@ -23,36 +23,24 @@ defineApp("ZCL_ORDERS", class {
rows = t.table({ ID: 0, customer: "", total: t.packed(11, 2), open: false });
hits = 0;
- async main(c) {
- if (c.eventName === "GO") {
- const { Orders } = cds.entities("my.shop");
- let q = SELECT.from(Orders);
- if (this.customer) q = q.where`customer like ${"%" + this.customer + "%"}`;
- if (this.minTotal) q = q.and`total >= ${this.minTotal}`;
- if (this.onlyOpen) q = q.and`open = ${true}`;
-
- this.rows = await q;
- this.hits = this.rows.length;
- c.messageToast(`${this.hits} orders`);
- }
-
- if (c.isDisplay) {
- c.view(
+ async main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(
`` +
`` +
`` +
`` +
- ` ` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
+ ` ` +
` ` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
- `` +
+ `` +
` ` +
` ` +
`` +
@@ -60,16 +48,30 @@ defineApp("ZCL_ORDERS", class {
`
` +
``);
+ return;
+ }
+
+ if (client.check_on_event("GO")) {
+ const { Orders } = cds.entities("my.shop");
+ let q = SELECT.from(Orders).where`customer like ${"%" + this.customer + "%"}`;
+ if (this.minTotal) q = q.and`total >= ${this.minTotal}`;
+ if (this.onlyOpen) q = q.and`open = ${true}`;
+
+ this.rows = await q;
+ this.hits = this.rows.length;
+ client.message_toast_display(`${this.hits} orders`);
}
}
});
```
-## Why the event branch comes first
+## Why `GO` does not render
-`GO` runs, *then* the render branch runs in the same roundtrip. The order
-matters: the table the view binds is the one the query just produced. Writing
-the render branch first with a `return` would show the user the previous result.
+`client.check_on_navigated()` is true on the first roundtrip and when the app
+gets the screen back — not on the `GO` roundtrip. The view stays as it is, and
+the new `rows` and `hits` are pushed into it on their own: changed bound data
+needs no `client.view_display()`. Render again only when the view's
+*structure* changes.
## The types the form needs
@@ -84,8 +86,9 @@ A `CheckBox` binds `selected`, an `Input` binds `value` — ordinary UI5.
## Building the query conditionally
-`cds.ql` composes, so the filter is plain JavaScript: add a `where` only for the
-fields the user filled. The `${…}` holes are **bound parameters**, so a customer
+`cds.ql` composes, so the filter is plain JavaScript: the customer condition is
+always there (an empty one is `like '%%'`, which matches every row), and an
+`and` is added only for the other fields the user filled. The `${…}` holes are **bound parameters**, so a customer
name containing a quote is a value and not a syntax error.
## Keeping the criteria
diff --git a/docs/examples/static-xml-view.md b/docs/examples/static-xml-view.md
index 59641aa..80b03d3 100644
--- a/docs/examples/static-xml-view.md
+++ b/docs/examples/static-xml-view.md
@@ -5,7 +5,7 @@ the middle of your logic. For a large screen, keep the XML in its own file and
read it once.
::: info Illustrative
-`c.view()` taking any string is the tested part. Loading it from disk is
+`client.view_display()` taking any string is the tested part. Loading it from disk is
ordinary Node — shown here because it is the question that comes up as soon as a
view passes a screenful.
:::
@@ -34,7 +34,7 @@ Two kinds of placeholder are in there, and the difference matters:
relative to the row, needs nothing from you, and must survive to the browser
untouched.
- `{CUSTOMER_PATH}` and `{GO_HANDLER}` are **yours** — they stand where
- `c.bind()` and `c.event()` go.
+ `client._bind()` and `client._event()` go.
## The app
@@ -49,14 +49,14 @@ defineApp("ZCL_ORDERS_FILE", class {
customer = "";
rows = t.table({ customer: "" });
- main(c) {
- if (c.isDisplay) {
- c.view(XML
- .replace("{CUSTOMER_PATH}", c.bind("customer"))
- .replace("{GO_HANDLER}", c.event("GO")));
+ main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(XML
+ .replace("{CUSTOMER_PATH}", client._bind("customer"))
+ .replace("{GO_HANDLER}", client._event("GO")));
return;
}
- if (c.eventName === "GO") { /* … */ }
+ if (client.check_on_event("GO")) { /* … */ }
}
});
```
@@ -66,6 +66,10 @@ is what you want for a placeholder that appears once. For one that repeats, use
`replaceAll` — and pick placeholder names that cannot collide with a UI5 binding
(`{GO_HANDLER}`, not `{GO}`).
+What `client._event("GO")` returns is a placeholder that becomes the event's
+wire after `main()` returns. Putting it into the string unchanged, as here, is
+what it is for; parsing or comparing it is not.
+
## Why read it at load
`readFileSync` at module scope runs once, when the plugin imports the file.
@@ -82,9 +86,9 @@ inside `main` until you are done.
|---|---|
| a screenful of XML | keep it inline. The template literal is easier to follow |
| a large form, or a view a designer edits | a file, with your editor's XML support |
-| a view assembled from repeated pieces | neither — build it with functions, see [Views](../guide/views) |
+| a view assembled from repeated pieces | neither — build it with `z2ui5_cl_ui5_view_builder`, or with functions, see [Views](../guide/views) |
## Next
-- [**Views**](../guide/views) — composing XML in JavaScript
+- [**Views**](../guide/views) — composing XML in JavaScript, and the view builder
- [**Selection Screen**](./selection-screen) — a form inline
diff --git a/docs/guide/data-binding.md b/docs/guide/data-binding.md
index 2a927c1..568aa56 100644
--- a/docs/guide/data-binding.md
+++ b/docs/guide/data-binding.md
@@ -1,19 +1,19 @@
# Data Binding
-A field of your class is a bound model field. `c.bind("name")` gives you the
-binding path to put in the view; the browser sends the value back, and the
+A field of your class is a bound model field. `client._bind("name")` gives you
+the binding path to put in the view; the browser sends the value back, and the
framework applies it to the instance before your `main` runs.
```js
defineApp("ZCL_HELLO", class {
name = "";
- main(c) {
- if (c.isDisplay) {
- c.view(` `);
+ main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(` `);
return;
}
- if (c.eventName === "GO") c.messageBox(`Hello ${this.name}`); // already applied
+ if (client.check_on_event("GO")) client.message_box_display(`Hello ${this.name}`); // already applied
}
});
```
@@ -21,21 +21,46 @@ defineApp("ZCL_HELLO", class {
No model, no manifest, no `setProperty`. Binding is two-way; there is no
separate "read-only bind".
-## `c.bind` takes a field NAME
+## `client._bind` takes a field NAME
```js
-c.bind("name") // ✅
-c.bind(this.name) // ❌ — an empty string, and every field is one
+client._bind("name") // ✅
+client._bind(this.name) // ❌ — the value "", not a field
```
-A name is used rather than a value because a value cannot identify a field: two
-empty strings are indistinguishable. A name that is not a bindable field throws
-and lists the ones that are:
+ABAP's `_bind( )` finds the attribute by reference, which a JavaScript value
+cannot carry: two empty strings are indistinguishable. So the field is named. A
+name that is not a field of the app throws and lists the ones that are:
```
-c.bind("nmae"): not a bindable field of this app — known: name, count, books
+client._bind( ): nmae is not a field of this app - in JavaScript the client takes a field's NAME, client._bind("name"), not its value. Known: name, count, books
```
+A component of a structure is named as ABAP names it, with `-`:
+`client._bind("order-customer-city")` is ABAP's `_bind( order-customer-city )`.
+
+## Options, by name
+
+`_bind`'s other parameters go by name, in one object with the ABAP names —
+`client->_bind( val = t_tab path = abap_true )` is
+`client._bind({ val: "t_tab", path: true })`:
+
+| | |
+|---|---|
+| `path: true` | the bare path (`/NAME`) a composed binding needs, instead of `{/NAME}` |
+| `tab`, `tab_index` | one cell of a table: `val` is then the column, `tab` the table field, `tab_index` the row, 1-based |
+| `omit_initial`, `omit_initial_paths` | keep initial values out of the model |
+| `json: true` | a string field spliced into the model as the JSON it holds |
+
+```js
+``
+` `
+` `
+```
+
+A component, a cell or an option makes the result a placeholder until `main`
+returns — the framework registers it then. Embed it as it is.
+
## The types
A field's ABAP type comes from its **initial value**. The mapping is narrow on
@@ -49,13 +74,16 @@ purpose and refuses rather than guesses:
| `flag = false` | `abap_bool`, and the app sees `true`/`false` |
| `price = t.packed(9, 2)` | packed decimal — **declare this one** |
| `code = t.char(3)` | fixed-width character |
-| `addr = { street: "", zip: 0 }` | a structure |
+| `matnr = t.numc(10)` | `NUMC`: digits, kept with their leading zeros |
+| `due = t.date()`, `at = t.time()` | `D` (`YYYYMMDD`) and `T` (`HHMMSS`), read and written as strings |
+| `addr = { street: "", zip: 0 }` | a structure (`t.struct({ … })` says so explicitly) |
| `rows = t.table({ sku: "", qty: 0 })` | a table whose row is that structure |
-Numbers are the real ambiguity — ABAP has `I`, `P` and `F` and they render with
-different decimals — so an integer becomes `I`, a fractional literal `F`, and a
-decimal amount has to say so with `t.packed()`. Guessing would produce views
-with the wrong number of decimals and nothing to point at.
+`t.string()`, `t.int()`, `t.float()` and `t.bool()` declare the scalars without
+an initial value. Numbers are the real ambiguity — ABAP has `I`, `P` and `F` and
+they render with different decimals — so an integer becomes `I`, a fractional
+literal `F`, and a decimal amount has to say so with `t.packed()`. Guessing
+would produce views with the wrong number of decimals and nothing to point at.
## Tables
@@ -63,17 +91,17 @@ with the wrong number of decimals and nothing to point at.
defineApp("ZCL_BOOKS", class {
books = t.table({ ID: 0, title: "", price: t.packed(9, 2) });
- async main(c) {
- if (c.isDisplay) {
- c.view(
- `` +
+ async main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(
+ ``);
return;
}
- if (c.eventName === "SEARCH") {
+ if (client.check_on_event("SEARCH")) {
this.books = await SELECT.from(cds.entities("my.bookshop").Books);
}
}
@@ -82,9 +110,9 @@ defineApp("ZCL_BOOKS", class {
Two things to note:
-- **the row fields are UPPERCASE in the view** — `{TITLE}`, `{PRICE}`. Component
- names are stored lowercase, as the transpiler does, and appear uppercase in
- the model;
+- **the model names are UPPERCASE in the view** — `{TITLE}`, `{PRICE}`, and a
+ field `name` is `/NAME`. Component names are stored lowercase, as the
+ transpiler does, and appear uppercase in the model;
- **assign the whole array.** `this.books = await SELECT…` replaces the table;
the plugin rebuilds the rows. Pushing into the array you read back does not
write through.
@@ -102,18 +130,16 @@ order = {
```
The model carries `ORDER.CUSTOMER.CITY` and `ORDER.LINES[].PRICE`, decimals
-included, through the draft and back. Read the whole tree as plain values:
+included, through the draft and back. Bind a component by its ABAP name,
+`client._bind("order-customer-city")`, and read the whole tree as plain values:
```js
const city = this.order.customer.city;
this.order = { ...this.order, customer: { name: "Ada", city: "London" } };
```
-::: info This was documented as unsupported for a while. It was not.
-Three pages said nested state could not be done. It could — the limitation was
-one guard in the plugin's own type derivation, written when only scalars had
-been tried. "Unsupported" must say whose limitation it is.
-:::
+Assigning a structure is ABAP's `s = VALUE #( … )`: what the new value leaves out
+is initial afterwards.
The depth limit is **8**, and it is a cycle guard rather than a judgement: an
object containing itself would otherwise recurse until the stack goes. A field
@@ -129,14 +155,16 @@ order.self.self.self.self.self.self.self.a is nested more than 8 levels deep.
model and **named in a warning** rather than silently dropped:
```
-[defineApp] ZCL_ORDER: these fields are NOT part of the model —
- total has no ABAP type: null, undefined and an empty array carry none.
+[cap2ui5] - defineApp ZCL_ORDER: these fields are NOT part of the model —
+ total has no ABAP type
+ Give an initial value, or declare it with t.table(…) / t.struct(…) / t.packed(…) / t.char(…). The app runs without them.
```
Give it an initial value, or declare it with `t.table(…)` / `t.packed(…)`.
## Next
-- [**Events**](./events) — `c.event` and its arguments
-- [**View Builder**](./views) — what goes inside `c.view`
+- [**Events**](./events) — `client._event` and its arguments
+- [**View Builder**](./views) — what goes inside `client.view_display`
- [**Persistence**](./persistence) — what survives the roundtrip
+- [**Client API**](../api/client) — every `_bind` option
diff --git a/docs/guide/ecosystem.md b/docs/guide/ecosystem.md
index 9660df3..d1a9e93 100644
--- a/docs/guide/ecosystem.md
+++ b/docs/guide/ecosystem.md
@@ -40,7 +40,8 @@ package `cap2ui5`, which is withdrawn from npm.
## What the plugin does and does not own
**Owns:** the CAP route and its authentication guard, `cap2ui5.Drafts` and the
-draft store over it, `defineApp` and the `c` facade, the two ABI gates.
+draft store over it, `defineApp` and the client that maps a JavaScript app
+onto `z2ui5_if_client`, the two ABI gates.
**Does not own:** views, the wire format, the model service, the lifecycle, the
UI5 shell, the frontend actions — all upstream's, running unmodified. The plugin
diff --git a/docs/guide/events.md b/docs/guide/events.md
index 934aa0e..b8665df 100644
--- a/docs/guide/events.md
+++ b/docs/guide/events.md
@@ -1,58 +1,81 @@
# Events
-`c.event("NAME")` produces the wire string a control's event attribute needs.
-The next roundtrip answers with `c.eventName === "NAME"`.
+`client._event("NAME")` produces the handler a control's event attribute needs.
+The next roundtrip answers `client.check_on_event("NAME")` with `true`.
```js
defineApp("ZCL_HELLO", class {
name = "";
count = 0;
- main(c) {
- if (c.isDisplay) {
- c.view(
- ` ` +
- ` `);
+ main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(
+ ` ` +
+ ` `);
return;
}
- if (c.eventName === "GO") {
+ if (client.check_on_event("GO")) {
this.count++;
- c.messageToast(`Hello ${this.name}, click ${this.count}`);
+ client.message_toast_display(`Hello ${this.name}, click ${this.count}`);
}
}
});
```
-`c.eventName` is `""` on an app start, so `if (c.eventName === "GO")` is safe
-without a guard.
+`client.get_event()` is the event's name, `""` on an app start, so
+`if (client.check_on_event("GO"))` is safe without a guard.
+`client.check_on_event()` without a name is true for any event.
## Arguments — how two buttons share one event
A handler cannot know which control fired unless the wire carries it. That is
-what the second parameter is for:
+what `t_arg` is for — the parameters by name are one object, with the ABAP
+names:
```js
-c.view(
- ` ` +
- ` `);
+client.view_display(
+ ` ` +
+ ` `);
// next roundtrip
-if (c.eventName === "TAKE") {
- this.colour = c.eventArg(1); // "red" or "blue"
+if (client.check_on_event("TAKE")) {
+ this.colour = client.get_event_arg(1); // "red" or "blue"
}
```
-`c.eventArg(i)` is **1-based**, like the ABAP table it reads. The first eight
-arguments are resolved up front; `c.eventArg(9)` throws and tells you to use
-`c.raw` for a longer list.
+`client.get_event_arg(i)` is **1-based**, like the ABAP table it reads. `arg`
+is one more argument behind the `t_arg`. The first eight arguments are resolved
+up front; `client.get_event_arg(9)` throws and tells you to use `client.raw` for
+a longer list.
-::: info This was missing, and the wire tests could not see it
-`c.event` took no arguments for a while, and the test suite was green: the wire
-tests play the frontend's part by hand and had been feeding the argument table
-themselves — simulating a browser that would never have sent it. The **browser**
-test found it. A facade method that composes view XML needs a browser test, not
-only a wire test.
+An argument may be a UI5 expression the browser evaluates when the event fires —
+`"${$parameters>/newValue}"` hands over what the user typed. `s_ctrl` is
+`z2ui5_if_client=>ty_s_event_control`, by component name:
+
+```js
+`/newValue}"],
+ s_ctrl: { check_queue_last: true, check_no_busy: true },
+})}"/>`
+```
+
+| `s_ctrl` | |
+|---|---|
+| `check_queue_last` | keep the last firing while a roundtrip runs, instead of dropping it — for `liveChange` |
+| `check_no_busy` | no busy indicator for this event |
+| `check_arg_literal` | every argument is quoted, so none is read as a binding or expression |
+| `check_prevent_default` | cancel the control's default for this event |
+
+`client._event("TAKE", ["red"])` — a second positional argument — is refused:
+the one positional argument is `val`, everything else goes by name.
+
+::: info The argument has to be on the wire
+Only the browser shows whether it is. A test that plays the frontend's part by
+hand and feeds the argument table itself simulates a browser that may never
+send it. A method that composes view XML needs a browser test, not only a wire
+test.
:::
## Where an event string goes
@@ -60,28 +83,53 @@ only a wire test.
Anywhere UI5 takes an event handler:
```js
-` `
-` `
-`
`
+` `
+` `
+`
`
```
The value you get back is a handler expression, not a plain name — embed it,
-do not parse or compare it. Between `c.event(…)` and the end of `main` it is in
-fact a placeholder token that is substituted for the real wire string on the
-way out; embedding is what it is for.
+do not parse or compare it. Between `client._event(…)` and the end of `main` it
+is in fact a placeholder token that is substituted for the real wire string on
+the way out; embedding is what it is for.
+
+Two more handlers go into view attributes the same way:
+
+- **`client._event_nav_app_leave()`** leaves the app — a `Page`'s
+ `navButtonPress`, with `showNavButton="${client.check_app_prev_stack()}"`.
+ `main` needs no branch for it. See [Navigation](./navigation).
+- **`client.follow_up_action({ val, t_arg })`** is a front-end action: `val` a
+ `z2ui5_if_client.cs_event` constant. Embedded in a view attribute it runs in
+ the browser, **with no roundtrip**; called on its own it runs when this
+ roundtrip's answer lands.
+
+```js
+import { defineApp, z2ui5_if_client } from "@cap2ui5/cds-plugin";
+
+// in the view: focus the search field, no roundtrip
+` `
+
+// in an event branch: set the browser tab's title with the answer
+if (client.check_on_event("TITLE")) {
+ client.follow_up_action({ val: z2ui5_if_client.cs_event.set_title, t_arg: [this.title] });
+}
+```
+
+`client.cs_event.set_focus` is the same constant, as ABAP allows both.
## Dispatching
With more than a few events, a switch reads better than a chain:
```js
-main(c) {
- switch (c.eventName) {
- case "SEARCH": return this.search(c);
- case "ADD": return this.add(c);
+main(client) {
+ switch (client.get_event()) {
+ case "SEARCH": return this.search(client);
+ case "ADD": return this.add(client);
case "": break; // an app start
}
- if (c.isDisplay) c.view(/* … */);
+ if (client.check_on_navigated()) client.view_display(/* … */);
}
```
@@ -91,6 +139,7 @@ Changed **bound data** needs no re-render; it is pushed on its own.
## Next
-- [**Data Binding**](./data-binding) — `c.bind` and the types
-- [**Popups & Toasts**](./popups) — `messageBox`, `messageToast`, `popup`
+- [**Data Binding**](./data-binding) — `client._bind` and the types
+- [**Popups & Toasts**](./popups) — `message_box_display`, `message_toast_display`, `popup_display`
- [**Navigation**](./navigation) — events that hand the screen to another app
+- [**Client API**](../api/client) — every method with its parameters
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 40e01dd..1f8e2b1 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -105,33 +105,39 @@ defineApp("HELLO", class {
name = "";
count = 0;
- main(c) {
- if (c.isDisplay) {
- c.view(
+ main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(
`` +
`` +
- ` ` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
+ ` ` +
` `);
return;
}
- if (c.eventName === "GO") {
+ if (client.check_on_event("GO")) {
this.count++;
- c.messageToast(`Hi, ${this.name}! You clicked ${this.count}x.`);
+ client.message_toast_display(`Hi, ${this.name}! You clicked ${this.count}x.`);
}
}
});
```
+`client` is abap2UI5's `z2ui5_if_client`, under its ABAP method names:
+`client->check_on_navigated( )` in an ABAP app is
+`client.check_on_navigated()` here. The [Client API](../api/client) lists
+every method.
+
::: warning `import`, not `require` — in this project
`cds init --nodejs` creates an **ES module** project (`"type": "module"` in
`package.json`), so a `.js` file there must `import`. A `require("@cap2ui5/cds-plugin")`
-in it does not fail on its own line: it fails the **whole runtime boot** —
-the log says `[cap2ui5] runtime failed to boot: ReferenceError: require is not
-defined in ES module scope`, and every roundtrip, for every app, answers 500
-"roundtrip failed".
+in it fails the **start**: the plugin loads the app modules before the server
+listens, and a module that fails to load stops it, as a broken service
+implementation does. `cds watch` then never prints the app addresses, and the
+log names the file with `ReferenceError: require is not defined in ES module
+scope`. See [Troubleshooting](./troubleshooting#the-server-does-not-start-require-is-not-defined-in-es-module-scope).
In a CommonJS project (no `"type": "module"`), or in a file ending in `.cjs`,
`const { defineApp } = require("@cap2ui5/cds-plugin")` is correct and works the same way.
@@ -139,18 +145,31 @@ The examples on this site use `import`, because that is what `cds init` gives
you.
:::
+::: details Or let `cds add cap2ui5` write a first app
+`cds add cap2ui5` creates `srv/apps/hello.js` in a project that has no apps
+yet: abap2UI5's hello world, `HELLO`, with its view built by
+`z2ui5_cl_ui5_view_builder` (see [Views](./views)). Plugin 0.3.0 writes that
+file with `require`, so in a project from `cds init --nodejs` rename it to
+`hello.cjs`, or change its `require` line to
+`import { defineApp, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin";`.
+It takes the app name `HELLO`, so use it instead of the file above, not beside it.
+:::
+
Four things worth knowing, and each of them is a rule rather than a style:
-- **`main` is synchronous.** No `async`, no `await`. Make it `async` only when
- *your app* does I/O — reading your own entities with `await SELECT.from(Books)`.
-- **`c.isDisplay` is the render branch, not `c.isFirstRun`.** `isDisplay` is
- also true every time the app gets the screen back from a navigation or a
- value help. An app that renders only on `isFirstRun` works perfectly until
- something navigates back into it, and then leaves the previous screen
- standing with no error anywhere. `isFirstRun` is for seeding state once.
+- **`main` is synchronous.** No `async`, no `await` — the client's methods
+ need none. Make it `async` only when *your app* does I/O — reading your own
+ entities with `await SELECT.from(Books)`.
+- **`client.check_on_navigated()` is the render branch, not
+ `client.check_on_init()`.** `check_on_navigated()` is also true every time
+ the app gets the screen back from a navigation or a value help. An app that
+ renders only on `check_on_init()` works perfectly until something navigates
+ back into it, and then leaves the previous screen standing with no error
+ anywhere. `check_on_init()` is for seeding state once.
- **State is ordinary fields.** `name = ""` and `count = 0` are the app's
- model; `c.bind("name")` binds one into the view. They survive the roundtrip
- because the plugin stores the instance in `cap2ui5.Drafts`.
+ model; `client._bind("name")` binds one into the view — by its **name**, as
+ a string. They survive the roundtrip because the plugin stores the instance
+ in `cap2ui5.Drafts`.
- **The first argument of `defineApp` is the app's name on the wire** — it is
what `?app_start=` takes. The file name does not matter.
@@ -164,28 +183,14 @@ Once the server listens, the plugin prints every app with the address that
starts it, and the user to log in as:
```
-[cap2ui5] HELLO http://localhost:4004/sap/bc/z2ui5?app_start=HELLO
-[cap2ui5] development login: alice (empty password)
-```
-
-Open that address. Those lines are the entry point: CAP's index page at
-`http://localhost:4004/` lists the HTML files under `app/` and your CDS
-services, not this route.
-
-::: details Tip: a link to the app on CAP's index page
-The index page lists every `index.html` under `app/`, so a one-line redirect
-puts the app there. Create `app/hello/index.html`:
-
-```html
-
-
+[cap2ui5] - HELLO http://localhost:4004/sap/bc/z2ui5?app_start=HELLO
+[cap2ui5] - development login: alice (empty password)
```
-Then **stop `cds watch` and start it again.** A new folder under `app/` does
-not restart the server, and the index page is built once per start: until the
-restart, `/hello/` answers 404 and "Web Applications" still says "none". After
-it, `/hello` is listed there and opens the app.
-:::
+Open that address. In development, CAP's start page at
+`http://localhost:4004/` lists the same addresses under "Web Applications",
+next to your CDS services — so the app is one click away from there too.
+Neither the lines nor the list appear in production.
The address is the **roundtrip route**, not a static page: the framework
answers a GET on it with a page that embeds the whole UI5 component — every
@@ -231,17 +236,17 @@ defineApp("BOOKS", class {
search = "";
books = t.table({ ID: 0, title: "", author: "" });
- async main(c) {
- if (c.isFirstRun) {
+ async main(client) {
+ if (client.check_on_init()) {
this.books = await SELECT.from("CatalogService.Books");
}
- if (c.isDisplay) {
- c.view(`
+ if (client.check_on_navigated()) {
+ client.view_display(`
-
-
+
+
@@ -260,10 +265,10 @@ defineApp("BOOKS", class {
`);
return;
}
- if (c.eventName === "SEARCH") {
+ if (client.check_on_event("SEARCH")) {
this.books = await SELECT.from("CatalogService.Books")
.where`title like ${"%" + this.search + "%"}`;
- c.messageToast(`${this.books.length} found`);
+ client.message_toast_display(`${this.books.length} found`);
}
}
});
@@ -272,14 +277,14 @@ defineApp("BOOKS", class {
`cds watch` restarts by itself when you save, and the startup lines gain one:
```
-[cap2ui5] BOOKS http://localhost:4004/sap/bc/z2ui5?app_start=BOOKS
+[cap2ui5] - BOOKS http://localhost:4004/sap/bc/z2ui5?app_start=BOOKS
```
Open it: five books. Search for `Raven` and one row is left, with a toast
"1 found". Three things the example shows:
-- **`main` is `async`** because the app does I/O — and `isFirstRun` seeds the
- table once, before the first render.
+- **`main` is `async`** because the app does I/O — and `check_on_init()`
+ seeds the table once, before the first render.
- **`t.table({…})` describes one row**, not the table: the field starts empty,
and the object only fixes the columns and their types.
- **Column names are UPPERCASE in the view** — `{TITLE}`, not `{title}`. The
@@ -320,7 +325,7 @@ SQLite file is in
| You see | It is |
|---|---|
-| `require is not defined in ES module scope` | an app file uses `require` — write `import`, or name the file `.cjs` |
+| the server does not start: `require is not defined in ES module scope` | an app file uses `require` — write `import`, or name the file `.cjs` |
| `NO_DRAFT_ENTRY_OF_PREVIOUS_REQUEST_FOUND` after a code change | the restart emptied the database — reload the tab |
| the browser asks for a login | log in as `alice` and leave the password empty |
| `port 4004 is already in use` | another `cds watch` still runs — stop it, or start with `cds watch --port 4005` |
@@ -333,8 +338,9 @@ The details for each are in [Troubleshooting](./troubleshooting).
## Next steps
- [**Project Structure**](./project-structure) — what the plugin adds, and what stays yours
-- [**App Lifecycle**](./lifecycle) — `isFirstRun` vs. `isDisplay`, events, navigation
-- [**Data Binding**](./data-binding) — `c.bind`, tables, structures
+- [**App Lifecycle**](./lifecycle) — `check_on_init()` vs. `check_on_navigated()`, events, navigation
+- [**Data Binding**](./data-binding) — `client._bind()`, tables, structures
+- [**Client API**](../api/client) — every method of `z2ui5_if_client`
- [**Persistence**](./persistence) — `cap2ui5.Drafts` and the owner binding
- [**Configuration**](../reference/configuration) — routes, the auth default, the apps directory
- [**Deployment**](../reference/deployment) — to BTP: `cap2ui5.Drafts` becomes an HDI table in the HANA build, and the approuter needs one extra route
diff --git a/docs/guide/lifecycle.md b/docs/guide/lifecycle.md
index 22c2fc8..1c3998b 100644
--- a/docs/guide/lifecycle.md
+++ b/docs/guide/lifecycle.md
@@ -1,7 +1,7 @@
# App Lifecycle
An app is a class. Each roundtrip rebuilds an instance of it from the draft,
-applies what the browser sent, and calls `main(c)` exactly once.
+applies what the browser sent, and calls `main(client)` exactly once.
```js
import { defineApp } from "@cap2ui5/cds-plugin";
@@ -9,87 +9,94 @@ import { defineApp } from "@cap2ui5/cds-plugin";
defineApp("ZCL_HELLO", class {
name = "";
- main(c) {
+ main(client) {
// called on EVERY roundtrip — the branches below decide what happens
}
});
```
+`client` is abap2UI5's `z2ui5_if_client`, by its ABAP method names:
+`client->check_on_navigated( )` is `client.check_on_navigated()`.
+
`main` is **synchronous**. Make it `async` only when your app does I/O; the
-framework calls need no `await` either way.
+client's calls need no `await` either way.
## The two predicates, and the one that trips people
```js
-main(c) {
- if (c.isFirstRun) {
+main(client) {
+ if (client.check_on_init()) {
// the first roundtrip of THIS app instance, and only that one.
// Seed state here.
}
- if (c.isDisplay) {
+ if (client.check_on_navigated()) {
// the first roundtrip AND every time this app gets the screen back:
// a called app leaving, a value help closing, a bookmark restored.
// RENDER here.
- c.view(/* … */);
+ client.view_display(/* … */);
return;
}
- if (c.eventName === "GO") { /* … */ }
+ if (client.check_on_event("GO")) { /* … */ }
}
```
-::: danger Render on `isDisplay`, not on `isFirstRun`
-`isFirstRun` implies `isDisplay`, so `if (c.isDisplay)` is the whole display
-condition — no `||`.
+::: danger Render on `check_on_navigated()`, not on `check_on_init()`
+`check_on_init()` implies `check_on_navigated()`, so
+`if (client.check_on_navigated())` is the whole display condition — no `||`.
-An app that renders only on `isFirstRun` works perfectly until something
+An app that renders only on `check_on_init()` works perfectly until something
navigates back into it, and then **leaves the previous screen standing with no
error anywhere**. Nothing throws, nothing logs, the user just sees the wrong
page. It is the framework's most common app bug, and `z2ui5_if_client`'s own
documentation says so.
:::
-Underneath they are `check_on_init()` and `check_on_navigated()`. The facade
-renames them because the original names suggest the opposite of what they do —
+The names are abap2UI5's, and they mislead in the same way there:
`check_on_navigated` reads like "arrived by navigation" and is in fact also true
on the very first run.
## The full surface
-Everything `c` offers, which is everything an app needs before reaching for
-`c.raw`:
+What `client` offers, grouped — the [client reference](../api/client) lists
+every method with its parameters:
| | |
|---|---|
-| **lifecycle** | `isFirstRun`, `isDisplay`, `canGoBack`, `eventName`, `eventArg(i)`, `prevApp` |
-| **binding** | `bind(field)`, `event(name, [args])` |
-| **screen** | `view(xml)`, `popup(xml)` / `popupClose()`, `nest(into, xml, opts)` / `nestClose()` |
-| **messages** | `messageBox(text)`, `messageToast(text)` |
-| **navigation** | `navTo(app)`, `navBack({event, data, app})` |
-| **escape hatch** | `raw` — the underlying async client |
+| **lifecycle** | `check_on_init()`, `check_on_navigated()`, `check_app_prev_stack()`, `get_event()`, `check_on_event(name)`, `get_event_arg(i)`, `get_app_prev()`, `get()` |
+| **binding** | `_bind(field)`, `_event(name)`, `_event_nav_app_leave()`, `follow_up_action(…)` |
+| **screen** | `view_display(xml)`, `popup_display(xml)` / `popup_destroy()`, `popover_display({ xml, by_id })`, `nest_view_display({ … })` / `nest_view_destroy()`, `nest2_*` |
+| **messages** | `message_box_display(text)`, `message_toast_display(text)` |
+| **navigation** | `nav_app_call(app)`, `nav_app_leave({ event, r_data })`, `get_app(id)`, `hash_set()`, `app_state_set_active()` |
+| **escape hatch** | `raw` — the transpiled `z2ui5_if_client` itself, asynchronous |
## A typical app
```js
+import cds from "@sap/cds";
+import { defineApp, t } from "@cap2ui5/cds-plugin";
+
+const { SELECT } = cds.ql;
+
defineApp("ZCL_ORDER", class {
customer = "";
lines = t.table({ sku: "", qty: 0 });
loaded = false;
- async main(c) {
- if (c.isFirstRun) {
- this.customer = c.raw ? "" : ""; // seed once
+ async main(client) {
+ if (client.check_on_init()) {
+ this.customer = "ACME"; // seed once
}
- if (c.eventName === "LOAD") {
+ if (client.check_on_event("LOAD")) {
const { Orders } = cds.entities("my.shop");
this.lines = await SELECT.from(Orders);
this.loaded = true;
}
- if (c.isDisplay || c.eventName === "LOAD") {
- c.view(/* … */);
+ if (client.check_on_navigated() || client.check_on_event("LOAD")) {
+ client.view_display(/* … */);
}
}
});
@@ -99,18 +106,41 @@ Note the last branch: after an event that changed what is on screen you render
again. Changed **bound data** is pushed on its own — you only re-render when the
view's *structure* changes.
-## Two members that used to exist and now throw
+## Helper methods
-Both throw an error naming the replacement rather than quietly changing meaning:
+An abap2UI5 app of any size splits `main` into helpers, and keeps the client
+for them in `main` as an ABAP app does with `me->client = client`:
-| gone | why |
+```js
+main(client) {
+ this.client = client;
+ if (client.check_on_navigated()) this.view_display();
+}
+
+view_display() {
+ this.client.view_display(/* … */);
+}
+```
+
+`this.client` is not declared as a field, so it is not part of the model or the
+draft; `main` sets it again on every roundtrip.
+
+## What throws, and what does nothing
+
+A name the client does not have throws an error naming the replacement, rather
+than answering `undefined` — `if (client.isDisplay)` would otherwise be false on
+every roundtrip and the app would never render:
+
+| | |
|---|---|
-| `c.isInitial` | it was wired to `check_on_navigated()` and named after `check_on_init()`. Use `c.isDisplay` to render, `c.isFirstRun` to seed |
-| `c.modelUpdate()` | it called `view_model_update()`, which the framework declares **obsolete and does nothing**. Changed bound data is pushed automatically — to an open popup and a nested view too |
+| `client.isFirstRun`, `client.isDisplay`, `client.eventName`, `client.bind(…)`, … | the pre-0.2.0 names. The error names the `z2ui5_if_client` method that replaces each |
+| `client.isInitial` | it was wired to `check_on_navigated()` and named after `check_on_init()`. Use `check_on_navigated()` to render, `check_on_init()` to seed |
+| `client.set_session_stateful()` | not supported: the app's state is in its fields, which are in the draft |
+| `view_model_update()` and the other `*_model_update()` | obsolete in `z2ui5_if_client`, and they **do nothing**, as in ABAP. Changed bound data is pushed automatically — to an open popup, popover and nested view too |
## Next
-- [**Data Binding**](./data-binding) — `c.bind`, tables, structures
-- [**Events**](./events) — `c.event`, arguments, `c.eventName`
-- [**Navigation**](./navigation) — `navTo`, `navBack`, `prevApp`
+- [**Data Binding**](./data-binding) — `client._bind`, tables, structures
+- [**Events**](./events) — `client._event`, arguments, `check_on_event`
+- [**Navigation**](./navigation) — `nav_app_call`, `nav_app_leave`, `get_app_prev`
- [**Persistence**](./persistence) — why the instance survives at all
diff --git a/docs/guide/migration-from-abap2ui5.md b/docs/guide/migration-from-abap2ui5.md
index 2f965fd..84c541f 100644
--- a/docs/guide/migration-from-abap2ui5.md
+++ b/docs/guide/migration-from-abap2ui5.md
@@ -2,8 +2,9 @@
If you know abap2UI5, you already know cap2UI5. Same pattern, same roundtrip,
same frontend, same framework — literally the same framework, since the plugin
-runs upstream's own runtime. What changes is the **language your app is written
-in** and **where its state is stored**.
+runs upstream's own runtime. And the same client: what `main( client )`
+receives is `z2ui5_if_client`, under its ABAP method names. What changes is
+the **language your app is written in** and **where its state is stored**.
## The mental model does not change
@@ -21,11 +22,9 @@ CLASS zcl_my_app DEFINITION PUBLIC.
ENDCLASS.
METHOD z2ui5_if_app~main.
- IF client->check_on_init( ).
+ IF client->check_on_navigated( ).
client->view_display( ... ).
- RETURN.
- ENDIF.
- IF client->get( )-event = 'GO'.
+ ELSEIF client->check_on_event( `GO` ).
client->message_box_display( |Hello { name }| ).
ENDIF.
ENDMETHOD.
@@ -38,16 +37,22 @@ import { defineApp } from "@cap2ui5/cds-plugin";
defineApp("ZCL_MY_APP", class {
name = "";
- main(c) {
- if (c.isDisplay) {
- c.view(/* … */);
- return;
+ main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(/* … */);
+ } else if (client.check_on_event("GO")) {
+ client.message_box_display(`Hello ${this.name}`);
}
- if (c.eventName === "GO") c.messageBox(`Hello ${this.name}`);
}
});
```
+Line for line. `client->method( )` is `client.method()`; a method's preferred
+parameter is its one positional argument, and parameters by name are one
+object with the ABAP names. [abap2UI5's documentation](https://abap2ui5.github.io/docs/)
+of a method is therefore the documentation of the JavaScript one; the
+[Client API](../api/client) lists them all.
+
## The translation table
| abap2UI5 | cap2UI5 |
@@ -55,48 +60,64 @@ defineApp("ZCL_MY_APP", class {
| `CLASS … INTERFACES z2ui5_if_app` | `defineApp("NAME", class { … })` |
| `DATA name TYPE string` | `name = ""` |
| `DATA amount TYPE p LENGTH 9 DECIMALS 2` | `amount = t.packed(9, 2)` |
+| `TYPE n`, `d`, `t` | `t.numc(n)`, `t.date()`, `t.time()` |
| `DATA rows TYPE ty_t_row` | `rows = t.table({ … })` |
-| `z2ui5_if_app~main` | `main(c)` |
-| `client->check_on_init( )` | `c.isFirstRun` |
-| `client->check_on_navigated( )` | `c.isDisplay` ← **render on this one** |
-| `client->get( )-event` | `c.eventName` |
-| `client->get_event_arg( 1 )` | `c.eventArg(1)` |
-| `client->_bind( name )` | `c.bind("name")` — a **name**, not a value |
-| `client->_bind_edit( name )` | `c.bind("name")` — binding is two-way; there is no separate variant |
-| `client->_event( 'GO' )` | `c.event("GO")` |
-| `client->view_display( xml )` | `c.view(xml)` |
-| `client->popup_display( xml )` | `c.popup(xml)` |
-| `client->message_box_display( t )` | `c.messageBox(t)` |
-| `client->message_toast_display( t )` | `c.messageToast(t)` |
-| `client->nav_app_call( app )` | `c.navTo(app)` |
-| `client->nav_app_leave( )` | `c.navBack({ event, data })` |
-| `client->get_app_prev( )` | `c.prevApp` |
-| anything else on `z2ui5_if_client` | `c.raw.z2ui5_if_client$( … )`, async |
-
-## The three differences that actually bite
+| `z2ui5_if_app~main` | `main(client)` |
+| `client->check_on_init( )` | `client.check_on_init()` |
+| `client->check_on_navigated( )` | `client.check_on_navigated()` ← **render on this one** |
+| ``client->check_on_event( `GO` )`` | `client.check_on_event("GO")` |
+| `client->get_event_arg( 1 )` | `client.get_event_arg(1)` |
+| `client->_bind( name )`, `_bind( s_order-customer )` | `client._bind("name")`, `client._bind("s_order-customer")` — a **name**, not a value |
+| `client->_bind( val = t_tab path = abap_true )` | `client._bind({ val: "t_tab", path: true })` |
+| ``client->_event( val = `GO` t_arg = VALUE #( ( `x` ) ) )`` | `client._event({ val: "GO", t_arg: ["x"] })` |
+| `client->follow_up_action( val = z2ui5_if_client=>cs_event-set_title t_arg = … )` | `client.follow_up_action({ val: z2ui5_if_client.cs_event.set_title, t_arg: [ … ] })` |
+| `client->view_display( view->stringify( ) )` | `client.view_display(view.stringify())` |
+| ``client->message_box_display( text = … type = `error` )`` | `client.message_box_display({ text: …, type: "error" })` |
+| `client->nav_app_call( NEW zcl_other( ) )` | `client.nav_app_call("ZCL_OTHER")` |
+| `client->nav_app_leave( event = … r_data = … )` | `client.nav_app_leave({ event, r_data })` |
+| `client->get( )-r_event_data` | `client.get().r_event_data` |
+| `z2ui5_cl_ui5_view_builder=>factory( )->ele( … )` | `z2ui5_cl_ui5_view_builder.factory().ele(…)` |
+
+Every method of the interface is there under its name — the plugin's tests
+hold the client to the interface, so none is missing and none is invented.
+`z2ui5_if_client` and `z2ui5_cl_ui5_view_builder` are imported from
+`@cap2ui5/cds-plugin`.
+
+## What is JavaScript's own
**`_bind` takes a name, not a value.** In ABAP, `client->_bind( name )` passes
-the attribute and the framework matches it by reference. JavaScript cannot do
-that — two empty strings are indistinguishable — so the facade takes the field
-name and resolves the binding for you.
-
-**Render on `isDisplay`, not `isFirstRun`.** The same trap as in ABAP, with
-clearer names: `check_on_init( )` is this instance's first roundtrip only, and
-`check_on_navigated( )` is also every return from a navigation. The JS facade
-renames them to say which is which.
-
-**Views are XML strings.** abap2UI5's fluent builder exists in the runtime, but
-the facade does not expose it: in JavaScript a template literal is shorter than
-a chain. See [Views](./views).
-
-```abap
-client->view_display( z2ui5_cl_ui5_view_builder=>factory(
- )->ele( `Page` )->tag( `Input` )->a( ... )->stringify( ) ).
-```
-
-```js
-c.view(` `);
-```
+the attribute and the framework matches it by reference. A JavaScript value
+cannot carry one — two empty strings are indistinguishable — so the field is
+named: `client._bind("name")`. A cell of a table is
+`{ val: column, tab: "t_tab", tab_index: 2 }`.
+
+**Some answers come after `main()`.** What `_event()`, a `_bind()` with
+options and `follow_up_action()` in a view attribute return is a placeholder
+that becomes the wire after `main()` returns — embed it as it is. `main()`
+stays synchronous; make it `async` only for your own I/O.
+
+**`client.get_app( id )`** answers the app behind a draft id as a handle whose
+fields can be written — `app.backend_event = "…"`, then
+`client.nav_app_leave(app)` — but not read. Reading the other app is
+`client.get_app_prev()`, as plain values.
+
+**`client.nav_app_call( app, fields )`** presets the called app's fields —
+what an ABAP app does between `NEW` and `nav_app_call( )`.
+
+**Every field is model.** There is no `PROTECTED SECTION`: every field with an
+initial value is part of the model. A helper method that needs the client gets
+it as in ABAP, `this.client = client` in `main()`, without declaring it as a
+field.
+
+**What is not there.** `client.set_session_stateful()` throws — the app's
+state is in its fields, which are in the draft. The obsolete
+`*_model_update()` do nothing, as they do in ABAP, and `_bind_edit()` is
+`_bind()`. `client.raw` is the transpiled `z2ui5_if_client` itself,
+asynchronous, for what an app should never need.
+
+**Render on `check_on_navigated()`, not `check_on_init()`.** The same trap as
+in ABAP: `check_on_init()` is this instance's first roundtrip only, and
+`check_on_navigated()` is also every return from a navigation.
## What is genuinely different
@@ -107,18 +128,46 @@ c.view(` `);
| data access | Open SQL | `cds.ql` — and CAP's remote services |
| deployment | abapGit into a system | `npm i` into a CAP project |
-## Porting an existing app
+## Translate it: `npx cap2ui5 abap2js`
+
+Because the client and the view builder are abap2UI5's own, an app class
+translates line for line — and the plugin does it:
+
+```bash
+npx cap2ui5 abap2js src/zcl_my_app.clas.abap --out srv/apps
+```
+
+`zcl_my_app.clas.abap` becomes `srv/apps/zcl_my_app.js`, registered as
+`ZCL_MY_APP`, so `?app_start=` is the same on both sides. A view chain keeps
+one call per line, `VALUE #( )` one row per line, and comments and texts come
+along untouched. A directory translates every class in it; `--check` writes
+nothing and fails when a module is missing or would change, for CI.
+
+It knows the part of ABAP an abap2UI5 app is written in — attributes and
+`TYPES`, `VALUE #( )`, `COND`/`SWITCH`, string templates, `IF`/`CASE`/`DO`,
+the client's and the view builder's calls — and **refuses everything else**
+with file, row and column rather than guess:
+
+```
+refused: z2ui5_cl_x.clas.abap:41:7 - LOOP AT ... ASSIGNING / REFERENCE INTO writes through the row - not supported yet
+```
-There is no automatic converter, and the honest reason is that the interesting
-part — Open SQL to `cds.ql`, ABAP types to declared fields — is exactly the part
-a converter would get wrong. The table above covers the framework calls; the
-rest is your business logic, which you are better placed to translate.
+What it refuses is typically your business logic — Open SQL to `cds.ql`, a
+field-symbol, a `sy-` field — and that part you are better placed to
+translate. The options and the details of the translation are in the
+plugin's [README](https://github.com/cap2UI5/cap2UI5/tree/main/plugin#an-abap-app-translated-npx-cap2ui5-abap2js).
-Start with the smallest app you have. The structure carries over almost
-untouched, and the first one takes an afternoon.
+How far "line for line" goes is measured, not claimed: the
+[`@cap2ui5/samples`](https://github.com/cap2UI5/samples) package is 71 of
+abap2UI5's samples as cap2UI5 apps — 69 of them written by `abap2js`, two
+ported by hand — and a differential test serves each beside its transpiled
+ABAP original and compares every roundtrip. Add it with
+`npm add -D @cap2ui5/samples`, and each sample starts under its ABAP class
+name.
## Next
- [**App Lifecycle**](./lifecycle) — the predicates, in detail
- [**Data Binding**](./data-binding) — the type declarations
+- [**Client API**](../api/client) — every method of `z2ui5_if_client`
- [**cap2UI5 vs. abap2UI5**](./vs-abap2ui5) — when to use which
diff --git a/docs/guide/navigation.md b/docs/guide/navigation.md
index 3571d34..2ab855c 100644
--- a/docs/guide/navigation.md
+++ b/docs/guide/navigation.md
@@ -7,17 +7,26 @@ the screen back and can read what the callee produced.
## Calling an app
```js
-// ZCL_PICK
-if (c.eventName === "CHOOSE") {
- c.navTo("ZCL_PICK_ONE");
+// ZCL_JS_PICK
+if (client.check_on_event("CHOOSE")) {
+ client.nav_app_call("ZCL_JS_PICK_ONE");
return;
}
```
-`c.navTo` takes the name you gave `defineApp`, a `defineApp` class, or an
-instance you built yourself. A name that resolves to nothing is refused **here**,
-where you can see which name it was, rather than as a `NAV_APP_TARGET_NOT_BOUND`
-later.
+`client.nav_app_call` takes the name you gave `defineApp`, a `defineApp` class,
+or an instance you built yourself — ABAP's `nav_app_call( NEW zcl_other( ) )`
+is `client.nav_app_call("ZCL_OTHER")`. A name that resolves to nothing is
+refused **here**, where you can see which name it was, rather than as a
+`NAV_APP_TARGET_NOT_BOUND` later.
+
+A second argument presets the called app's fields — what an ABAP app does
+between `NEW` and `nav_app_call( )`. They win over the called app's own initial
+values, and a name it does not have is refused, naming the ones it has:
+
+```js
+client.nav_app_call("ZCL_JS_HANDOVER_FORM", { product: "Notebook", quantity: 2, mode: "edit" });
+```
Navigation is scheduled for the end of the roundtrip, so it is usually the last
thing a branch does.
@@ -25,55 +34,105 @@ thing a branch does.
## Coming back
```js
-// ZCL_PICK_ONE
-if (c.eventName === "TAKE") {
- this.colour = c.eventArg(1);
- if (c.canGoBack) c.navBack({ event: "PICKED" });
+// ZCL_JS_PICK_ONE
+if (client.check_on_event("TAKE")) {
+ this.colour = client.get_event_arg(1);
+ if (client.check_app_prev_stack()) client.nav_app_leave({ event: "PICKED" });
return;
}
```
-`c.navBack(opts)` hands the screen back. Guard it with `c.canGoBack` — there may
-be nothing to go back to.
+`client.nav_app_leave(…)` hands the screen back. Guard it with
+`client.check_app_prev_stack()` — there may be nothing to go back to.
-| option | |
+| parameter | |
|---|---|
-| `event` | the event the caller's `main` sees on its next run |
-| `data` | a value for the caller; a string goes as is, anything else is JSON |
-| `app` | leave to a *different* app than the one that called |
+| `event` | the event the caller's `main` sees on its next run: `client.get_event()`, `client.check_on_event(…)` |
+| `r_data` | a value for the caller, which reads it as `client.get().r_event_data` |
+| `app` | leave to a *different* app than the one that called; `client.nav_app_leave(app)` positionally |
+
+A page's back button needs no branch at all:
+`navButtonPress="${client._event_nav_app_leave()}"`, with
+`showNavButton="${client.check_app_prev_stack()}"`.
## Reading what the callee produced
-Back in the caller, `c.prevApp` is the app on the other side of the last
-navigation — the instance that just returned, with its fields as plain values:
+Back in the caller, `client.get_app_prev()` is the app on the other side of the
+last navigation — the instance that just returned, with its fields as plain
+values:
```js
-// ZCL_PICK again, after the callee left
-if (c.eventName === "PICKED" && c.prevApp) {
- this.chosen = c.prevApp.colour ?? "";
+// ZCL_JS_PICK again, after the callee left
+if (client.check_on_event("PICKED") && client.get_app_prev()) {
+ this.chosen = client.get_app_prev().colour ?? "";
this.picks += 1;
}
-if (c.isDisplay) {
- c.view(/* … shows this.chosen … */);
+if (client.check_on_navigated()) {
+ client.view_display(/* … shows this.chosen … */);
}
```
-::: danger This is where `isDisplay` earns its name
-When the callee leaves, the caller's `main` runs again with **`isDisplay` true
-and `isFirstRun` false**. An app that renders only on `isFirstRun` shows the
-user its *old* screen — the pick never appears, and nothing anywhere reports an
-error.
+Or the callee hands over data with `r_data`, and the caller reads it from
+`client.get()`, abap2UI5's `client->get( )-r_event_data`:
+
+```js
+// the callee
+client.nav_app_leave({ event: "CONFIRMED", r_data: { product: this.product, quantity: this.quantity } });
+
+// the caller
+if (client.check_on_navigated()) {
+ if (client.check_on_event("CONFIRMED")) {
+ this.result = client.get().r_event_data; // { product: "Notebook", quantity: 5 }
+ }
+ client.view_display(/* … */);
+}
+```
+
+`r_data` arrives **typed** — an ABAP caller could `ASSIGN` it as a structure —
+so a JavaScript caller gets the object back with its keys lowercase, as ABAP
+names components.
+
+::: danger This is where `check_on_navigated()` earns its name
+When the callee leaves, the caller's `main` runs again with
+**`check_on_navigated()` true and `check_on_init()` false**. An app that renders
+only on `check_on_init()` shows the user its *old* screen — the pick never
+appears, and nothing anywhere reports an error.
That is the single most common way to get a screen that does not refresh. See
[App Lifecycle](./lifecycle).
:::
+## Writing into the caller: `get_app(id)`
+
+The callee can also set a field of the caller before it leaves, as abap2UI5's
+sample 025 does. `client.get_app(id)` answers the app behind a draft id:
+
+```js
+const app_back = client.get_app(client.get().s_draft.id_prev_app_stack);
+app_back.backend_event = "FORM_LEFT";
+client.nav_app_leave(app_back);
+```
+
+That app is read from the draft store after `main` returns, so what
+`get_app(id)` answers is a **handle whose fields can be written, not read** —
+reading one throws and points to `client.get_app_prev()`. Without an id,
+`client.get_app()` is the running app itself.
+
+## The URL
+
+| | |
+|---|---|
+| `client.hash_set("/detail/1")` | push a hash onto the browser history |
+| `client.hash_replace("/detail/2")` | rewrite the hash without a history entry |
+| `client.app_state_set_active()` | keep this app's state id in the URL |
+| `client.app_state_get_href()` | the absolute link to this app's current state |
+
## The stack is in the database
-`c.navTo` does not keep a call stack in memory. The draft rows carry it —
-`id_prev`, `id_prev_app`, `id_prev_app_stk` — which is why navigation survives a
-restart.
+`client.nav_app_call` does not keep a call stack in memory. The draft rows carry
+it — `id_prev`, `id_prev_app`, `id_prev_app_stk` — which is why navigation
+survives a restart.
Measured: a server is **SIGKILLed while inside the called app**, and a fresh
process takes the callee's event, unwinds a stack it never built, runs the
@@ -88,14 +147,15 @@ RESULT: the app STACK survived the restart
| | |
|---|---|
-| a dialog that belongs to this app's state | [`c.popup`](./popups) |
-| a screen with its own state, reusable from several places | `c.navTo` |
+| a dialog that belongs to this app's state | [`client.popup_display`](./popups) |
+| a screen with its own state, reusable from several places | `client.nav_app_call` |
A value help is usually the second: it is an app, and its result comes back
-through `c.prevApp`.
+through `client.get_app_prev()` or `r_data`.
## Next
-- [**App Lifecycle**](./lifecycle) — `isDisplay` vs. `isFirstRun`
+- [**App Lifecycle**](./lifecycle) — `check_on_navigated()` vs. `check_on_init()`
- [**Popups & Toasts**](./popups) — the lighter alternatives
- [**Persistence**](./persistence) — why the stack survives
+- [**Client API**](../api/client) — every navigation method
diff --git a/docs/guide/persistence.md b/docs/guide/persistence.md
index f7beb1c..1898803 100644
--- a/docs/guide/persistence.md
+++ b/docs/guide/persistence.md
@@ -15,7 +15,7 @@ browser ──POST /rest/root/z2ui5 {id, event, model}──▶ CAP
│ load draft
│ rebuild the app instance
│ apply the model the browser sent
- │ call your main(c)
+ │ call your main(client)
│ write a NEW draft, new id
◀──{S_FRONT:{ID:…}, actions, model}────────────┘
```
@@ -35,7 +35,7 @@ defineApp("ZCL_ORDER", class {
customer = { name: "", city: "" };
lines = t.table({ sku: "", qty: 0, price: t.packed(9, 2) });
- main(c) { /* … */ }
+ main(client) { /* … */ }
});
```
@@ -47,9 +47,9 @@ A field the plugin cannot type is **left out of the model and named in a
warning** rather than silently dropped:
```
-[defineApp] ZCL_ORDER: these fields are NOT part of the model —
- total has no ABAP type: null, undefined and an empty array carry none.
- Give it a value, or declare it with t.table(…) / t.packed(…).
+[cap2ui5] - defineApp ZCL_ORDER: these fields are NOT part of the model —
+ total has no ABAP type
+ Give an initial value, or declare it with t.table(…) / t.struct(…) / t.packed(…) / t.char(…). The app runs without them.
```
`null`, `undefined` and `[]` carry no type, so declare them: `t.table({…})` for
@@ -61,6 +61,10 @@ Anything that is not a declared field. A value stashed on `this` inside `main`
with no initializer at construction time is not part of the model and will be
gone on the next roundtrip. If you want it to survive, declare it.
+That is also what makes `this.client = client` in `main()` safe: a helper
+method reaches the client the way it does in an ABAP app, and the client is
+never persisted.
+
## It survives a restart — measured
`cold-test.mjs` is not a unit test. It starts a server, does a roundtrip,
diff --git a/docs/guide/popups.md b/docs/guide/popups.md
index 5ec4ccc..8d450fb 100644
--- a/docs/guide/popups.md
+++ b/docs/guide/popups.md
@@ -1,94 +1,131 @@
# Popups & Toasts
-Four ways to put something on the screen that is not the main view.
+Five ways to put something on the screen that is not the main view.
## Messages
```js
-c.messageToast("saved"); // transient, bottom of the screen
-c.messageBox("Hello " + this.name); // a dialog with an OK button
+client.message_toast_display("saved"); // transient, bottom of the screen
+client.message_box_display("Hello " + this.name); // a dialog with an OK button
```
Both are recorded and replayed in the order you wrote them, so two toasts arrive
-in that order.
+in that order. Their other parameters go by name, in one object with the ABAP
+names:
+
+```js
+client.message_box_display({ text: "Delete it?", type: "confirm", actions: ["DELETE", "CANCEL"],
+ emphasizedaction: "DELETE", onclose: "BOX_CLOSED" });
+client.message_toast_display({ text: "saved", duration: 5000 });
+```
+
+`text` may be data — an object or an array is laid out as abap2UI5 lays out an
+ABAP structure or table.
## A popup — a fragment on top of the view
```js
-if (c.eventName === "HELP") {
- c.popup(
+if (client.check_on_event("HELP")) {
+ client.popup_display(
`` +
`` +
` ` +
- ` ` +
+ ` ` +
` `);
return;
}
-if (c.eventName === "HELP_CLOSE") {
- c.popupClose();
+if (client.check_on_event("HELP_CLOSE")) {
+ client.popup_destroy();
return;
}
```
A popup is a **`FragmentDefinition`**, not an `mvc:View` — that is the shape UI5
-expects here. It shares the main view's model, so `c.bind()` and `c.event()`
-work inside it exactly as they do outside.
+expects here. It shares the main view's model, so `client._bind()` and
+`client._event()` work inside it exactly as they do outside.
+
+Close it with `client.popup_destroy()`. Changed bound data is pushed into an
+open popup automatically; you do not re-render it to refresh a value.
+
+## A popover — a fragment anchored to a control
+
+```js
+if (client.check_on_event("POPOVER")) {
+ client.popover_display({
+ xml: `` +
+ ` ` +
+ `` +
+ ` `,
+ by_id: "more", // the id of a control in the main view
+ });
+ return;
+}
+
+if (client.check_on_event("POPOVER_CLOSE")) client.popover_destroy();
+```
-Close it with `c.popupClose()`. Changed bound data is pushed into an open popup
-automatically; you do not re-render it to refresh a value.
+`by_id` names the control it opens at — here ` `.
-## A nested view — a fragment INSIDE the main view
+## A nested view — a view INSIDE the main view
For a region of the page that changes while the rest stays put:
```js
-c.view(
+client.view_display(
`` +
`` +
- ` ` +
+ ` ` +
` ` + // ← the receiving control
` `);
// later
-if (c.eventName === "DETAIL") {
- c.nest("slot",
- `` +
- ` ` +
- ` `);
+if (client.check_on_event("DETAIL")) {
+ client.nest_view_display({
+ val: `` +
+ ` ` +
+ ` `,
+ id: "slot",
+ method_insert: "addContent",
+ method_destroy: "removeAllContent",
+ });
return;
}
-if (c.eventName === "DETAIL_HIDE") c.nestClose();
+if (client.check_on_event("DETAIL_HIDE")) client.nest_view_destroy();
```
-The main view stays as it is; only the fragment re-renders on the next `nest`.
+The main view stays as it is; only the nested view re-renders on the next
+`nest_view_display`.
-`c.nest(into, xml, opts)` takes the receiving control's `id`, and `opts` names
-the UI5 mutators for its aggregation:
+`id` is the receiving control's `id`, and the other two parameters name the UI5
+mutators for its aggregation — there are no defaults:
-| | default | when to change it |
-|---|---|---|
-| `insert` | `addContent` | `addItem` for a `List`, etc. |
-| `clear` | `removeAllContent` | `removeAllItems` for a `List` |
+| | required | for a `Page` or `VBox` | for a `List` |
+|---|---|---|---|
+| `method_insert` | yes | `addContent` | `addItem` |
+| `method_destroy` | no | `removeAllContent` | `removeAllItems` |
-**Without `clear`, every call adds one more fragment.** The defaults fit a
-`Page` or a `VBox`.
+**Without `method_destroy`, nothing is cleared first, and every call adds one
+more view.**
-There is exactly **one** nested slot, and `c.nestClose()` takes no argument: it
-clears that slot rather than a named one.
+There are **two** nested slots: `nest2_view_display({ val, id, method_insert,
+method_destroy })` and `nest2_view_destroy()` are the second, with the same
+contract. Each `*_destroy()` takes no argument: it clears its slot.
## Which one do I want?
| | |
|---|---|
-| a short confirmation | `messageToast` |
-| something the user must acknowledge | `messageBox` |
-| a modal that takes input or a decision | `popup` |
-| a region of the page that updates independently | `nest` |
+| a short confirmation | `message_toast_display` |
+| something the user must acknowledge | `message_box_display` |
+| a modal that takes input or a decision | `popup_display` |
+| a few details next to the control that asked | `popover_display` |
+| a region of the page that updates independently | `nest_view_display` |
| a whole second screen with its own state | [navigation](./navigation) — a separate app |
## Next
- [**Navigation**](./navigation) — when a popup is really another app
- [**Events**](./events) — the handlers the buttons above use
+- [**Client API**](../api/client) — every parameter of these methods
diff --git a/docs/guide/project-structure.md b/docs/guide/project-structure.md
index 691b731..46cc143 100644
--- a/docs/guide/project-structure.md
+++ b/docs/guide/project-structure.md
@@ -40,9 +40,16 @@ vendored folder, no sync pipeline. Upgrading is `npm update @cap2ui5/cds-plugin`
## `srv/apps/` — the one directory that is yours
-Every `.js`, `.mjs` or `.cjs` file in it is loaded once the runtime is up. A
-file is not special in any way: it just calls `defineApp`, and it may call it
-more than once.
+Every `.js`, `.mjs` or `.cjs` file in it is loaded once the runtime is up and
+CAP has served the model, so an app module may call `cds.entities()` at its
+top. A file is not special in any way: it just calls `defineApp`, and it may
+call it more than once. A module that fails to load fails the start, as a
+service implementation does.
+
+`cds add cap2ui5` creates a first one, `srv/apps/hello.js`, when the directory
+has none. In 0.3.0 that file uses `require`, so in an ES module project — what
+`cds init --nodejs` creates — rename it to `hello.cjs`, or change its first
+line to `import { defineApp, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin";`.
```js
// srv/apps/pick.js — two apps in one file is fine
@@ -53,12 +60,12 @@ defineApp("ZCL_PICK_ONE", class { /* … */ });
```
The **first argument to `defineApp` is the name on the wire** — what
-`?app_start=` takes and what `c.navTo()` resolves. The file name is irrelevant.
+`?app_start=` takes and what `client.nav_app_call()` resolves. The file name is irrelevant.
To put apps somewhere else, point the plugin at it:
```json
-{ "cds": { "cap2ui5": { "apps": "srv/my-apps" } } }
+{ "cds": { "requires": { "cap2ui5": { "apps": "srv/my-apps" } } } }
```
See [Configuration](../reference/configuration) for the rest of the knobs.
@@ -98,7 +105,7 @@ In `node_modules`, in two packages, and you own neither:
| | |
|---|---|
| `@cap2ui5/cds-plugin` | the plugin — about 1,050 lines in 0.1.0, of which about 640 are code. `cds-plugin.js`, `index.cds`, `index.js`, `lib/` |
-| `@abap2ui5/node-runtime` | abap2UI5 itself: upstream's ABAP, downported and transpiled over open-abap, with the UI5 frontend embedded in the page its GET answers with. the plugin pins one exact release |
+| `@abap2ui5/node-runtime` | abap2UI5 itself: upstream's ABAP, downported and transpiled over open-abap, with the UI5 frontend embedded in the page its GET answers with. The plugin pins one exact release |
That split is the whole design. The plugin is a **host**: it mounts a route,
implements the draft store over a CDS entity, and turns a JavaScript class into
diff --git a/docs/guide/roadmap.md b/docs/guide/roadmap.md
index 273ea8a..316e72b 100644
--- a/docs/guide/roadmap.md
+++ b/docs/guide/roadmap.md
@@ -16,6 +16,15 @@ limitation this page used to carry — and it retired the pipelines, the vendore
core and 82,804 lines of generated code with it. The reasoning and the
measurements are in [Where cap2UI5 Comes From](./where-it-comes-from).
+Since then the JavaScript side closed its own gaps. 0.2.0 made the client an
+app receives abap2UI5's `z2ui5_if_client`, every method under its ABAP name,
+exported `z2ui5_cl_ui5_view_builder` and shipped TypeScript declarations;
+0.3.0 added `npx cap2ui5 abap2js`, which translates an abap2UI5 app class
+into a cap2UI5 app line for line, and apps that come from a package —
+[`@cap2ui5/samples`](https://github.com/cap2UI5/samples) is 71 of abap2UI5's
+samples that way. The details are in the plugin's
+[CHANGELOG](https://github.com/cap2UI5/cap2UI5/blob/main/plugin/CHANGELOG.md).
+
## Known limits today
### Behind an approuter, the roundtrip path needs its own route
@@ -28,20 +37,22 @@ is refused with `403` until the roundtrip path gets a route of its own with
abap2UI5/abap2UI5#2802 teaches the frontend the token handshake; it is merged,
but no abap2UI5 release carries it yet.
-### The facade does not cover everything
+### The apps are JavaScript, and so are the errors
-`c` covers views, popups, nested views, navigation, messages, event arguments
-and nested state. Everything else lives behind `c.raw`, which is the full
-`z2ui5_if_client` — async, and under its original ABAP names. That is a real
-escape hatch rather than a placeholder, but a method used often enough deserves
-a facade member.
+A mistyped field name throws at runtime, not at edit time:
+`client._bind("nmae")` names the app's known fields, and an untypeable field
+is named in a warning. The package ships TypeScript declarations, so an
+annotated client —
+`/** @param {import("@cap2ui5/cds-plugin").Client<{ name: string }>} client */`
+— gets completion and checked field names in the editor; nothing checks an
+unannotated app.
-### The apps are JavaScript, and so are the errors
+### `abap2js` translates part of ABAP
-A mistyped field name throws at runtime, not at edit time. `c.bind("nmae")`
-tells you the known fields, and an untypeable field is named in a warning — but
-there is no type checking across your app, and the TypeScript story is
-unwritten.
+`npx cap2ui5 abap2js` knows the ABAP an abap2UI5 app is written in and
+refuses the rest — a field-symbol, `SELECT`, a `sy-` field — with file, row
+and column. What it refuses you translate by hand. Of abap2UI5's 129
+samples, it translates 69 today.
### UI5 comes from the CDN, and only from the CDN
@@ -69,18 +80,16 @@ background pages may still carry the port's mechanics; if a page contradicts
upstream release, pin the plugin to it, and the route CAP generates works
unchanged.
-**Grow the facade where use shows it is needed** — driven by real apps rather
-than by completing a table.
+**Teach `abap2js` more ABAP.** Some of its refusals say "not supported yet".
-**Finish this documentation**, including a page on writing an app against
-`c.raw` when the facade does not reach.
+**Finish this documentation.**
## What is deliberately not planned
-**A cap2UI5 view builder.** Views are UI5 XML strings; a template literal is
-shorter and clearer than a fluent chain in JavaScript. abap2UI5's builder runs
-in the runtime, and an app written in ABAP can use it — see
-[The ABAP view builder](./views#the-abap-view-builder).
+**A view builder of cap2UI5's own.** The builder a JavaScript app uses is
+abap2UI5's `z2ui5_cl_ui5_view_builder`, rendered by the transpiled class in
+the runtime — see [The view builder](./views#the-view-builder). A UI5 XML
+string in a template literal works as well.
**A second implementation of anything upstream owns.** The whole point of the
current design is that there is one implementation of the framework. A feature
diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md
index 952f225..c0003df 100644
--- a/docs/guide/troubleshooting.md
+++ b/docs/guide/troubleshooting.md
@@ -42,13 +42,13 @@ Another `cds watch` still runs, usually in a different terminal. Stop it with
That is expected: the route requires a user, and `cds watch` uses CAP's
mocked authentication. Log in as **`alice` with an empty password** — the
-startup line names it: `[cap2ui5] development login: alice (empty password)`.
+startup line names it: `[cap2ui5] - development login: alice (empty password)`.
Only cancelling the dialog is refused, with a `401`, and the next attempt asks
again.
Mocked authentication lets other names in as well, but a draft belongs to the
user who created it — log in as someone else and a running session starts
-over. The details are under *401 on the roundtrip* below.
+over. The details are under *401 or 403 on the roundtrip* below.
## "App with name X not found"
@@ -58,12 +58,12 @@ Apps are registered by **`defineApp`**, not found by file name:
`defineApp("ZCL_HELLO", …)` is started with `?app_start=ZCL_HELLO`. The file
name is irrelevant, and one file may register several apps.
2. **The file must be in the scanned directory** — `srv/apps` by default, or
- whatever `cds.cap2ui5.apps` points at. Every `.js`, `.mjs` and `.cjs` in it
+ whatever `cds.requires.cap2ui5.apps` points at. Every `.js`, `.mjs` and `.cjs` in it
is imported once the runtime is up.
3. **The plugin says how many it loaded.** On startup:
```
- [cap2ui5] 5 app module(s) loaded from srv/apps
+ [cap2ui5] - 5 app module(s) loaded from srv/apps
```
A count of `0`, or no line at all, is a path problem rather than a code
@@ -73,24 +73,27 @@ Apps are registered by **`defineApp`**, not found by file name:
`main( client )` method throws at registration, and the message names the
app.
-`c.navTo()` on an unknown name is refused where you called it, listing what is
-registered — rather than failing later as `NAV_APP_TARGET_NOT_BOUND`.
+`client.nav_app_call()` with a name no app is registered under is refused
+where you called it — rather than failing later as `NAV_APP_TARGET_NOT_BOUND`.
-## Every roundtrip answers 500 "roundtrip failed"
+## The server does not start: `require is not defined in ES module scope`
-For every app, from the first click — look at the server log above the
-startup lines:
+`cds watch` stops before it listens, and the error names an app file:
```
-[cap2ui5] runtime failed to boot: ReferenceError: require is not defined in ES module scope, you can use import instead
+ReferenceError: require is not defined in ES module scope, you can use import instead
```
The project is an **ES module project** (`"type": "module"` in
`package.json`, which is what `cds init --nodejs` creates), and an app file in
-it uses `require("@cap2ui5/cds-plugin")`. The plugin imports the apps as part of booting
-the runtime, so one such file fails the boot, and the route has nothing to
-answer with. Write `import { defineApp } from "@cap2ui5/cds-plugin"` instead — or rename
-the file to `.cjs`, where `require` stays valid.
+it uses `require("@cap2ui5/cds-plugin")`. The plugin loads the apps before the
+server listens, and an app module that fails to load fails the start — as a
+service implementation does. Write
+`import { defineApp } from "@cap2ui5/cds-plugin"` instead, or rename the file
+to `.cjs`, where `require` stays valid.
+
+The same holds for any other error an app module throws while it loads: the
+start fails and the log names the module.
## The draft cannot be restored
@@ -159,16 +162,21 @@ absent in 1.71 and UI5 renders nothing rather than complaining.
exactly like nothing happening. **Roundtrips → Response** shows whether a
view came back.
-## 401 on the roundtrip
+## 401 or 403 on the roundtrip
-The route requires an authenticated user by default — `cds.cap2ui5.requires` is
-`"authenticated-user"`. In development CAP's mocked auth applies, so any
+**401** means the caller is not logged in. The route requires an authenticated
+user by default — `cds.requires.cap2ui5.roles` is `["authenticated-user"]`. In development CAP's mocked auth applies, so any
configured user works (`alice` with an empty password in a stock project). In
BTP the approuter must forward the token (`HTML5.ForwardAuthToken`).
+**403** from the plugin means the user is logged in but has none of the roles
+in `cds.requires.cap2ui5.roles`; the error names the roles. (A 403 that carries
+`x-csrf-token: Required` comes from the approuter instead — see the next
+section.)
+
The decision happens **before** the body is read, so an unauthenticated POST is
refused without the payload being buffered. To open the route deliberately, set
-`requires` to `null` — and read what that costs in
+`roles` to `any` — and read what that costs in
[Configuration](../reference/configuration).
## 403 on the roundtrip behind the approuter
@@ -182,14 +190,14 @@ is safe, are in [Deployment](../reference/deployment#the-approuter-needs-one-ext
## Two users see each other's state
-Almost always one cause: **`cds.cap2ui5.requires` is `null`.** Every caller is
+Almost always one cause: **`cds.requires.cap2ui5.roles` is `any` or `null`.** Every caller is
then CAP's anonymous user, and the draft store binds a session to
`cds.context.user.id` — so all anonymous visitors share one owner and therefore
each other's sessions. That is the documented consequence of turning
authentication off, not a defect.
With authentication on, a draft answers to its creator and to nobody else. If
-you see otherwise with `requires` set, that is a bug worth reporting — check
+you see otherwise with `roles` set, that is a bug worth reporting — check
the `owner` column of `cap2ui5.Drafts` first:
```sql
diff --git a/docs/guide/user-exit.md b/docs/guide/user-exit.md
index 85b8852..bdd74c4 100644
--- a/docs/guide/user-exit.md
+++ b/docs/guide/user-exit.md
@@ -199,7 +199,7 @@ messages carry entity names, SQL fragments and deployment paths.
App state belongs on the app instance; see [App Lifecycle](./lifecycle).
- **Not a request filter.** It shapes config, it does not accept or reject
requests. Authentication and authorisation belong on the CAP service
- (`@requires`, `@restrict`) and on `cds.cap2ui5.requires` — see
+ (`@requires`, `@restrict`) and on `cds.requires.cap2ui5.roles` — see
[Deployment](../reference/deployment).
- **Not a place for per-user secrets.** The object is registered once and
shared by every request in the process; only the context argument is per
diff --git a/docs/guide/views.md b/docs/guide/views.md
index 9c608c7..9a26697 100644
--- a/docs/guide/views.md
+++ b/docs/guide/views.md
@@ -1,28 +1,32 @@
# Views
-A view is **UI5 XML, as a string**, handed to `c.view()`.
+A view is **UI5 XML, as a string**, handed to `client.view_display()`.
```js
-c.view(
+client.view_display(
`` +
`` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
` `);
```
-That is the whole API. There is no builder to learn, no control catalogue to
-wait for, and nothing between you and UI5: anything you can write in a UI5 XML
-view, you can write here, including controls the framework has never heard of.
+There is no control catalogue to wait for, and nothing between you and UI5:
+anything you can write in a UI5 XML view, you can write here, including
+controls the framework has never heard of. abap2UI5's own view builder is
+there as well, for a view built the way an ABAP app builds one — see
+[The view builder](#the-view-builder).
## The two holes you fill
| | |
|---|---|
-| `${c.bind("field")}` | a binding path to one of your fields |
-| `${c.event("NAME")}` | a handler expression; `["arg"]` as a second parameter travels with it |
+| `${client._bind("field")}` | a binding path to one of your fields |
+| `${client._event("NAME")}` | a handler expression; `client._event({ val: "NAME", t_arg: ["arg"] })` sends arguments with it |
-Everything else is plain UI5.
+Everything else is plain UI5. What `_event()` — and a `_bind()` with options —
+returns is a placeholder that becomes the real expression after `main()`:
+embed it as it is, and do not cut or rebuild it.
## The shape
@@ -42,8 +46,8 @@ The view is a string, so composing it is ordinary JavaScript:
```js
const column = (t) => ` `;
-c.view(
- `` +
+client.view_display(
+ `` +
`${["Title", "Author", "Price"].map(column).join("")} ` +
`` +
` ` +
@@ -51,41 +55,100 @@ c.view(
```
Row fields inside a table's template are **uppercase** — `{TITLE}` — and are
-bound relative to the row, so they need no `c.bind`.
+bound relative to the row, so they need no `client._bind()`.
::: warning Interpolating user input into markup is an injection
-`${c.bind(…)}` and `${c.event(…)}` are framework-produced and safe. A value a
-user typed is not:
+`${client._bind(…)}` and `${client._event(…)}` are framework-produced and
+safe. A value a user typed is not:
```js
-c.view(` `); // ❌ the user writes the markup
-c.view(` `); // ✅ bound, escaped by UI5
+client.view_display(` `); // ❌ the user writes the markup
+client.view_display(` `); // ✅ bound, escaped by UI5
```
Bind it. If you genuinely need a value in the markup rather than in the model,
-escape it yourself first.
+escape it yourself first — or build the view with the view builder, whose
+`a({ n, t })` renders a text literally.
:::
## When to re-render
- **changed bound data** → nothing to do. It is pushed to the view, and to an
- open popup or nested view, on its own.
+ open popup, popover or nested view, on its own.
- **changed view structure** — a column appears, a button becomes visible → call
- `c.view()` again.
+ `client.view_display()` again.
-There is no `modelUpdate()`. It existed, called a framework method documented as
-*obsolete and does nothing*, and now throws an error saying so.
+`client.view_model_update()` and the other `*_model_update()` methods are
+declared obsolete in `z2ui5_if_client` and do nothing, here as in ABAP: an app
+that calls one ports unchanged, and loses nothing by dropping the call.
-## The ABAP view builder
+## The view builder
+
+abap2UI5's `z2ui5_cl_ui5_view_builder` is exported by the plugin, under its
+own name and called as the client is — one positional argument, or the
+parameters by name as one object:
+
+```js
+import { defineApp, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin";
+
+defineApp("HELLO_BUILDER", class {
+ name = "";
+
+ main(client) {
+ if (client.check_on_navigated()) {
+ const view = z2ui5_cl_ui5_view_builder.factory()
+ .ele({ n: "View", ns: "mvc" })
+ .a({ n: "xmlns", v: "sap.m" })
+ .a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" })
+ .a({ n: "displayBlock", b: true })
+ .ele("Shell")
+ .ele("Page")
+ .a({ n: "title", v: "Hello" })
+ .tag("Input")
+ .a({ n: "value", v: client._bind("name") })
+ .tag("Text")
+ .a({ n: "text", t: "{shown as typed}" })
+ .tag("Button")
+ .a({ n: "text", v: "Go" })
+ .a({ n: "press", v: client._event("GO") });
+ client.view_display(view.stringify());
+ return;
+ }
+ if (client.check_on_event("GO")) client.message_box_display(`Hello ${this.name}`);
+ }
+});
+```
+
+| | |
+|---|---|
+| `z2ui5_cl_ui5_view_builder.factory()` | an empty root; open the `mvc:View` and declare its `xmlns` yourself |
+| `ele(n)`, `ele({ n, ns })` | add a child element and descend into it |
+| `tag(n)`, `tag({ n, ns })` | add a child element and stay: the form for a leaf |
+| `a({ n, v })`, `a({ n, b })`, `a({ n, t })` | an attribute on the child just added (or on the node itself while it has none): `v` as it is — bindings, events, constant text; `b` a boolean; `t` text rendered literally, so a `{` is shown rather than read as a binding |
+| `end()` | ascend to the parent |
+| `stringify()` | the XML — rendered after `main()`, so hand it to `view_display()` as it is |
+
+The chain is recorded while `main()` runs and rendered after it by the
+transpiled `z2ui5_cl_ui5_view_builder` the runtime carries. The XML, its
+escaping and its refusals — an `end()` past the root, a duplicate attribute —
+are exactly those of the same chain in an ABAP app, and `_bind()` and
+`_event()` placeholders come through its escaping intact. `view_display()`,
+`popup_display()`, `popover_display()` and the nested views take a builder's
+`stringify()` as well as XML text. `ViewBuilder` is the same class under a
+JavaScript name.
+
+Template literal or builder is a matter of taste in a new app. The builder is
+what an app ported from ABAP already has — and what
+[`npx cap2ui5 abap2js`](./migration-from-abap2ui5#translate-it-npx-cap2ui5-abap2js)
+writes.
-abap2UI5 ships `z2ui5_cl_ui5_view_builder`, a fluent builder for the same XML.
-It runs in the hosted runtime — the shipped `Z2UI5_CL_UI5_APP_HI_WORLD` is
-built with it and works under cap2UI5. The facade does not expose it, and
-`c.raw` does not reach it either: `z2ui5_if_client` has no reference to the
-builder. In JavaScript a template literal is shorter and clearer than a builder
-chain, so a JS app keeps `c.view` with `c.bind` and `c.event`.
+## The ABAP view builder
-If you want the builder, write the app in ABAP.
+An app can also stay in ABAP: a `z2ui5_if_app` class, transpiled against the
+runtime the plugin hosts, runs beside the JavaScript apps. That is the route
+for a class you would rather not translate — the
+[translation](./migration-from-abap2ui5#translate-it-npx-cap2ui5-abap2js) is
+the other one.
### An app in ABAP
@@ -190,26 +253,8 @@ roundtrips like any app's. The transpile type-checks against the framework, so
a misspelled builder method fails there rather than at runtime. The startup
lines list only the apps registered with `defineApp`, not ABAP apps.
-### From a JavaScript app
-
-Reaching the transpiled class through the runtime's global,
-`abap.Classes["Z2UI5_CL_UI5_VIEW_BUILDER"]`, or through the package's
-`./output/*` export works, but it is awkward and unsupported: every call is
-async, every result is dereferenced with `.get()`, and the app is coupled to
-transpiler output.
-
-::: warning `c.event()` does not survive the builder in cap2ui5 0.1.0
-In the released cap2ui5 0.1.0, the placeholder `c.event()` returns does not
-survive the builder's XML escaping: the response then carries a raw NUL and is
-not valid JSON. With the builder, the event string has to come from `c.raw`
-(`z2ui5_if_client$_event`). cap2UI5/cap2UI5#81 fixes it on `main`; no release
-carries the fix yet.
-:::
-
-Keep template literals with `c.bind` and `c.event` in a JavaScript app.
-
## Next
-- [**Data Binding**](./data-binding) — the types behind `c.bind`
-- [**Events**](./events) — the handlers behind `c.event`
+- [**Data Binding**](./data-binding) — the types behind `client._bind()`
+- [**Events**](./events) — the handlers behind `client._event()`
- [**Popups & Toasts**](./popups) — fragments and nested views
diff --git a/docs/guide/vs-abap2ui5.md b/docs/guide/vs-abap2ui5.md
index d895d01..717ce37 100644
--- a/docs/guide/vs-abap2ui5.md
+++ b/docs/guide/vs-abap2ui5.md
@@ -16,7 +16,11 @@ Not "compatible" — identical, because it is the same code:
- **the same wire.** One `PROTOCOL` version, stamped by the same code that
reads it;
- **the same concepts.** Roundtrips, the draft chain, the app stack, value
- helps as apps, one class per app.
+ helps as apps, one class per app;
+- **the same API.** The client an app's `main( client )` receives is
+ `z2ui5_if_client` under its ABAP method names, and the view builder is
+ `z2ui5_cl_ui5_view_builder`. abap2UI5's documentation of a method is the
+ documentation of the JavaScript one.
The browser cannot tell which side answers.
@@ -38,8 +42,10 @@ implementation to keep in step. See
| data access | Open SQL | `cds.ql`, and CAP's remote services |
| session state | `Z2UI5_T_01` | `cap2ui5.Drafts`, a CDS entity in your database |
| identity | `sy-uname` | `cds.context.user.id` |
-| view | the fluent builder | a UI5 XML string — the builder is there, the facade just does not need it |
-| lifecycle predicates | `check_on_init( )` / `check_on_navigated( )` | `c.isFirstRun` / `c.isDisplay` |
+| client calls | `client->check_on_navigated( )` | `client.check_on_navigated()` |
+| parameters by name | ``_event( val = `GO` t_arg = … )`` | one object: `_event({ val: "GO", t_arg: [ … ] })` |
+| binding | `_bind( name )` — the attribute, by reference | `_bind("name")` — the field, by name |
+| view | `z2ui5_cl_ui5_view_builder` | the same builder, or a UI5 XML string |
The full mapping is in
[Migrating from abap2UI5](./migration-from-abap2ui5#the-translation-table).
@@ -51,9 +57,7 @@ The full mapping is in
METHOD z2ui5_if_app~main.
IF client->check_on_navigated( ).
client->view_display( ... ).
- RETURN.
- ENDIF.
- IF client->get( )-event = 'GO'.
+ ELSEIF client->check_on_event( `GO` ).
client->message_box_display( |Hello { name }| ).
ENDIF.
ENDMETHOD.
@@ -61,18 +65,24 @@ ENDMETHOD.
```js
// cap2UI5
-main(c) {
- if (c.isDisplay) {
- c.view(/* … */);
- return;
+main(client) {
+ if (client.check_on_navigated()) {
+ client.view_display(/* … */);
+ } else if (client.check_on_event("GO")) {
+ client.message_box_display(`Hello ${this.name}`);
}
- if (c.eventName === "GO") c.messageBox(`Hello ${this.name}`);
}
```
-Structure for structure. The languages part company in two places: ABAP names
-its arguments where JavaScript passes an object, and `_bind( name )` becomes
-`c.bind("name")` because JavaScript cannot match a value by reference.
+Line for line. The languages part company in two places: ABAP names its
+arguments where JavaScript passes one object with the same names, and
+`_bind( name )` becomes `client._bind("name")` because JavaScript cannot match
+a value by reference. That is close enough for a machine to do it:
+`npx cap2ui5 abap2js` translates an abap2UI5 app class into a cap2UI5 app,
+line for line, and refuses what it does not know rather than guess — see
+[Migrating from abap2UI5](./migration-from-abap2ui5#translate-it-npx-cap2ui5-abap2js).
+[`@cap2ui5/samples`](https://github.com/cap2UI5/samples) is 71 of abap2UI5's
+samples as cap2UI5 apps, 69 of them translated that way.
## Which one do I want?
@@ -92,11 +102,14 @@ Not structurally. The runtime is a dependency: a new abap2UI5 release is a
version bump, and it brings the backend and the frontend together. There is no
port to catch up, no pipeline to re-run, nothing to re-transpile by hand.
-What can lag is the **facade** — `c` covers the common surface, and something
-newly added upstream may need a member here before it is convenient. `c.raw`
-reaches it in the meantime, under its original name.
+The client covers every method of `z2ui5_if_client` — the plugin's tests hold
+it to the interface, so a method upstream adds shows up there as a failing
+test — the test reads the interface from the runtime it boots — rather than
+as a gap an app finds. `client.raw`, the transpiled
+interface itself, remains the escape hatch.
## Next
-- [**Migrating from abap2UI5**](./migration-from-abap2ui5) — the translation table
+- [**Migrating from abap2UI5**](./migration-from-abap2ui5) — the translation table, and `abap2js`
+- [**Client API**](../api/client) — every method of `z2ui5_if_client`
- [**Where cap2UI5 Comes From**](./where-it-comes-from) — why this is a host and not a port
diff --git a/docs/guide/what-is-cap2ui5.md b/docs/guide/what-is-cap2ui5.md
index 180337e..dbf0bea 100644
--- a/docs/guide/what-is-cap2ui5.md
+++ b/docs/guide/what-is-cap2ui5.md
@@ -68,43 +68,47 @@ defineApp("ZCL_HELLO", class {
name = ""; // ← app state, persisted automatically
- main(c) { // ← synchronous: no async, no await
- if (c.isDisplay) {
- c.view(
+ main(client) { // ← synchronous: no async, no await
+ if (client.check_on_navigated()) {
+ client.view_display(
`` +
`` +
- ` ` +
- ` ` +
+ ` ` +
+ ` ` +
` `);
return;
}
- if (c.eventName === "GO") {
+ if (client.check_on_event("GO")) {
// the button was clicked; this.name already holds what the user typed
- c.messageBox(`Hello, ${this.name}!`);
+ client.message_box_display(`Hello, ${this.name}!`);
}
}
});
```
That is the whole app. No `manifest.json`, no `Component.js`, no controller
-file, no i18n setup. The class fields are your state, `main(c)` is your logic,
-and the view is UI5 XML with two holes in it: `c.bind()` for a field and
-`c.event()` for a handler.
-
-::: tip Render on `isDisplay`
-`c.isDisplay` is true on the first roundtrip **and** whenever the app gets the
-screen back — from a called app, a value help, a restored bookmark. Rendering
-only on `c.isFirstRun` produces an app that silently shows its previous screen
-after a navigation. See [App Lifecycle](./lifecycle).
+file, no i18n setup. The class fields are your state, `main(client)` is your
+logic, and the view is UI5 XML with two holes in it: `client._bind()` for a
+field and `client._event()` for a handler. abap2UI5's view builder,
+`z2ui5_cl_ui5_view_builder`, is there too, for a view built as an ABAP app
+builds one — see [Views](./views).
+
+::: tip Render on `check_on_navigated()`
+`client.check_on_navigated()` is true on the first roundtrip **and** whenever
+the app gets the screen back — from a called app, a value help, a restored
+bookmark. Rendering only on `client.check_on_init()` produces an app that
+silently shows its previous screen after a navigation. See
+[App Lifecycle](./lifecycle).
:::
::: tip About the names
-`ZCL_HELLO`, and `z2ui5_if_client` behind `c.raw` — the framework running under
-the plugin *is* ABAP, so its identifiers are ABAP's. The facade renames the
-handful you meet daily (`c.isDisplay`, `c.bind`, `c.event`); everything else
-keeps its original name, which is what lets every abap2UI5 sample and document
-still map onto what you are doing.
+`ZCL_HELLO`, `check_on_navigated`, `_bind` — the framework running under the
+plugin *is* ABAP, and `client` is its `z2ui5_if_client`, under the interface's
+own method names: `client->check_on_navigated( )` in ABAP is
+`client.check_on_navigated()` here. That is what lets every abap2UI5 sample
+and document map onto what you are doing — an ABAP app ports line by line.
+The full list is the [Client API](../api/client).
:::
## The gap this closes {#the-gap}
diff --git a/docs/guide/where-it-comes-from.md b/docs/guide/where-it-comes-from.md
index e1a1f9a..96d2495 100644
--- a/docs/guide/where-it-comes-from.md
+++ b/docs/guide/where-it-comes-from.md
@@ -53,7 +53,7 @@ abap2UI5 (the real ABAP sources)
@abap2ui5/node-runtime the backend AND the UI5 frontend, one package, one commit
│
▼
-cap2ui5 (this plugin) mounts the route, implements the draft store over a
+@cap2ui5/cds-plugin mounts the route, implements the draft store over a
CDS entity, turns a JS class into something the
runtime can call — and contains no framework logic
```
@@ -96,10 +96,13 @@ archived. Nothing consumes their output any more: `@cap2ui5/cds-plugin` depends
## Why the ABAP names remain
`ZCL_HELLO`, `z2ui5_if_client`, `check_on_init` — the runtime *is* ABAP, so its
-identifiers are ABAP's. The plugin's facade renames the handful an app author
-meets every day (`c.isDisplay`, `c.bind`, `c.event`), and `c.raw` reaches the
-rest under their original names. Every abap2UI5 sample and document therefore
-still maps onto what you are doing.
+identifiers are ABAP's. The plugin keeps them: the client an app's
+`main( client )` receives is `z2ui5_if_client` under its own method names —
+`client->check_on_navigated( )` is `client.check_on_navigated()` — and the
+view builder is `z2ui5_cl_ui5_view_builder`. Every abap2UI5 sample and
+document therefore maps onto what you are doing, and an ABAP app ports line
+by line; `npx cap2ui5 abap2js` does the porting (see
+[Migrating from abap2UI5](./migration-from-abap2ui5#translate-it-npx-cap2ui5-abap2js)).
## Next
diff --git a/docs/guide/why-cap2ui5.md b/docs/guide/why-cap2ui5.md
index 93f3258..46a27f9 100644
--- a/docs/guide/why-cap2ui5.md
+++ b/docs/guide/why-cap2ui5.md
@@ -39,7 +39,7 @@ With cap2UI5 the structure shrinks to:
```
my-cap-project/
-├── package.json # ← + 1 dependency: cap2ui5
+├── package.json # ← + 1 dependency: @cap2ui5/cds-plugin
└── srv/
└── apps/
└── my_app.js # ← your app. One file.
@@ -60,7 +60,7 @@ You spend the entire time in **JavaScript** (or TypeScript, if you prefer). No X
```bash
cds watch
# → CAP server runs on :4004, and the plugin prints each app's address:
-# → [cap2ui5] ZCL_MY_APP http://localhost:4004/sap/bc/z2ui5?app_start=ZCL_MY_APP → open it, done
+# → [cap2ui5] - ZCL_MY_APP http://localhost:4004/sap/bc/z2ui5?app_start=ZCL_MY_APP → open it, done
```
### 2. Server state = app state
@@ -68,30 +68,30 @@ cds watch
A cap2UI5 app is a **class with fields**. These fields are your state:
```js
-class CustomerEdit extends z2ui5_if_app {
+defineApp("ZCL_CUSTOMER_EDIT", class {
customer_id = "";
- customer_data = {};
+ customer_data = { name: "", email: "" };
is_dirty = false;
validation = { name: "None", email: "None" };
- async main(c) { /* ... */ }
-}
+ async main(client) { /* ... */ }
+});
```
-After every roundtrip the entire instance is **persisted automatically in the CDS entity `cap2ui5.Drafts`**. On the next roundtrip it is rebuilt, the browser's model is applied, and `main(c)` runs again. You don't need to manage a JSONModel, write a reducer, or build a "service worker" for offline state — the server holds everything.
+After every roundtrip the entire instance is **persisted automatically in the CDS entity `cap2ui5.Drafts`**. On the next roundtrip it is rebuilt, the browser's model is applied, and `main(client)` runs again. You don't need to manage a JSONModel, write a reducer, or build a "service worker" for offline state — the server holds everything.
### 3. Bindings without a model
This is the core pattern that makes cap2UI5 (and abap2UI5) lightweight code in the first place:
```js
-` `
+` `
```
-`c.bind("name")` returns the UI5 binding path for that field. Binding is two-way: when the user types, the value arrives on `this.name` **before** your next `main(c)` runs. No JSONModel, no property mapping, no sync code.
+`client._bind("name")` returns the UI5 binding path for that field. Binding is two-way: when the user types, the value arrives on `this.name` **before** your next `main(client)` runs. No JSONModel, no property mapping, no sync code.
-(In abap2UI5 the ABAP call passes the attribute itself and the framework matches it by reference. JavaScript cannot do that — two empty strings are indistinguishable — so the facade takes the field name instead.)
+(In abap2UI5 the ABAP call passes the attribute itself and the framework matches it by reference. JavaScript cannot do that — two empty strings are indistinguishable — so cap2UI5's `_bind()` takes the field name instead — the one place where the JavaScript call differs from `client->_bind( name )`.)
→ Details under [Data Binding](./data-binding).
@@ -115,8 +115,10 @@ You use CDS entities where it **makes business sense** (master data, business da
Inside `main()` you can do **anything** Node.js allows — including, of course, CAP connections:
```js
+customers = t.table({ CustomerID: "", CompanyName: "" }); // a field: part of the model
+
async main(client) {
- if (c.isDisplay) {
+ if (client.check_on_navigated()) {
const northwind = await cds.connect.to("northwind");
this.customers = await northwind.run(SELECT.from("Customers"));
/* ... view ... */
@@ -138,7 +140,7 @@ The UI5 bundle is loaded once. After that every roundtrip returns only **a bit o
## Where it gets unfair
-The trade-offs are listed on [What is cap2UI5?](./what-is-cap2ui5#the-gap) — offline, pixel-perfect design systems, read-heavy filtering. One of them is worth a second sentence here, because it is the one that bites in a CAP project: a **live search filter over millions of rows** sends every keystroke's filter change to the server, where a Fiori Elements list filters locally in the JSONModel or pages server-side through the OData driver. If that is your screen, use the OData model (see [set_odata_model](../examples/external-odata#where-the-data-goes)) or build that one screen with Fiori Elements.
+The trade-offs are listed on [What is cap2UI5?](./what-is-cap2ui5#the-gap) — offline, pixel-perfect design systems, read-heavy filtering. One of them is worth a second sentence here, because it is the one that bites in a CAP project: a **live search filter over millions of rows** sends every keystroke's filter change to the server, where a Fiori Elements list filters locally in the JSONModel or pages server-side through the OData driver. If that is your screen, use the OData model — the front-end action `z2ui5_if_client.cs_event.set_odata_model`, run with [`client.follow_up_action()`](../api/client#events-and-front-end-actions) — or build that one screen with Fiori Elements.
For **UI-centric back-office apps**, which are the typical CAP use case, cap2UI5 is almost always the more ergonomic choice.
diff --git a/docs/index.md b/docs/index.md
index 446bbf9..626afd3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -30,9 +30,11 @@ features:
- title: Apps are plain JavaScript
icon: 🟨
details: >-
- defineApp("ZCL_HELLO", class { name = ""; main(c) { … } }) — synchronous,
- no async, no await, no ABAP. State is ordinary fields, and c.bind("name")
- binds one into the view.
+ defineApp("ZCL_HELLO", class { name = ""; main(client) { … } }) — synchronous,
+ no async, no await, no ABAP. State is ordinary fields, and
+ client._bind("name") binds one into the view. The client is abap2UI5's
+ z2ui5_if_client under its own method names — client.check_on_navigated(),
+ client._event("GO") — so an ABAP app ports line by line.
- title: It IS abap2UI5, not a copy of it
icon: 🔗
details: >-
diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md
index 5773120..d0d1857 100644
--- a/docs/reference/architecture.md
+++ b/docs/reference/architecture.md
@@ -12,14 +12,19 @@ project used to be — see [Where cap2UI5 Comes From](../guide/where-it-comes-fr
├── srv/apps/*.js your apps ← you write this
├── db/, srv/*.cds your model ← you write this
└── node_modules/
- ├── cap2ui5 the plugin ← about 640 lines of code
+ ├── @cap2ui5/cds-plugin the plugin
│ ├── cds-plugin.js mounts the route behind CAP's middlewares
│ ├── index.cds cap2ui5.Drafts
- │ ├── index.js defineApp, defineExit, t
+ │ ├── index.js defineApp, defineExit, t, z2ui5_cl_ui5_view_builder,
+ │ │ z2ui5_if_client, abap2js
│ └── lib/
- │ ├── define-app.js a JS class → something the runtime can call
+ │ ├── define-app.js a JS class → something the runtime can call,
+ │ │ and the client main( ) receives
+ │ ├── define-exit.js the user exit, registered
+ │ ├── view-builder.js records a view builder chain for upstream's class
│ ├── draft-store.js the draft store, over a CDS entity
- │ └── runtime.js locate and boot the runtime
+ │ ├── runtime.js locate and boot the runtime
+ │ └── abap2js.js npx cap2ui5 abap2js
└── @abap2ui5/node-runtime abap2UI5 itself, one exact release
├── output/ upstream's ABAP, downported + transpiled —
│ the GET page embeds the UI5 frontend, from
@@ -27,8 +32,10 @@ project used to be — see [Where cap2UI5 Comes From](../guide/where-it-comes-fr
└── setup/ the one hook output/ imports
```
-The plugin contains **no framework logic**. No view builder, no wire format, no
-lifecycle, no model service — all of that is upstream's code running unmodified.
+The plugin contains **no framework logic**. No wire format, no lifecycle, no
+model service — all of that is upstream's code running unmodified. Even the
+view builder a JavaScript app calls is upstream's `z2ui5_cl_ui5_view_builder`:
+the plugin records the chain and the transpiled class renders it.
## Why that removes a whole class of bug
@@ -47,14 +54,14 @@ mismatch loudly. See [HTTP Protocol](./protocol).
POST /rest/root/z2ui5
│
├─ cds.middlewares.before ← context, auth: cds.context.user now exists
- ├─ guard ← cap2ui5.requires, before the body is read
- ├─ express.raw ← up to 10 MB
+ ├─ guard ← cds.requires.cap2ui5.roles, before the body is read
+ ├─ express.raw ← up to body_parser.limit, 10 MB by default
└─ cl_express_icf_shim.run ← upstream's own express adapter
│
├─ load the draft ← ZCL_CDS_DRAFT_STORE → cap2ui5.Drafts
├─ rebuild the app instance
├─ apply the browser's model
- ├─ call your main(c) ← defineApp's wrapper
+ ├─ call your main(client) ← defineApp's wrapper
├─ compose the response ← upstream's handler
└─ write the next draft
```
@@ -79,12 +86,19 @@ that:
- it **boxes** each declared field at construction and derives the RTTI schema
from the same pass, because `_bind()` matches a value by *identity* among the
- object's attributes — there is no name parameter;
-- it hands `main` a **Proxy** whose reads unwrap the boxes and whose writes write
- through, so your code sees plain values while the framework keeps its boxes;
+ object's attributes — there is no name parameter. That is why
+ `client._bind("name")` takes a field's name: the wrapper resolves it to the
+ field's box;
+- it hands `main` — and every method `main` calls — a **Proxy** whose reads
+ unwrap the boxes and whose writes write through, so your code sees plain
+ values while the framework keeps its boxes;
+- it hands `main` the **client**: `z2ui5_if_client` under its own method
+ names, over the transpiled, asynchronous one (still reachable as
+ `client.raw`);
- it makes `main` **synchronous**: queries are resolved before it runs, commands
- are recorded and replayed after, and event tokens are substituted once the
- async call can be awaited.
+ are recorded and replayed after, and the placeholders `_event()`, a
+ `_bind()` with options and a view builder chain stand for are substituted
+ once the async calls can be awaited.
## The hazards, and what guards each
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
index 5eb43c7..738bf10 100644
--- a/docs/reference/configuration.md
+++ b/docs/reference/configuration.md
@@ -1,8 +1,9 @@
# Configuration
-Everything the plugin reads lives under `cds.cap2ui5` and is a plain CAP
-configuration value: `package.json#cds`, a `.cdsrc.json`, an environment
-variable, a profile — whatever you already use.
+Everything the plugin reads lives under `cds.requires.cap2ui5` and is a plain
+CAP configuration value: `package.json#cds`, a `.cdsrc.json`, a
+`CDS_REQUIRES_CAP2UI5_*` environment variable, a profile — whatever you
+already use. That is where SAP's own plugins keep their settings.
## The defaults
@@ -11,10 +12,12 @@ These ship in the plugin's own `package.json` and apply until you override one:
```json
{
"cds": {
- "cap2ui5": {
- "apps": "srv/apps",
- "requires": "authenticated-user",
- "routes": ["/sap/bc/z2ui5", "/rest/root/z2ui5"]
+ "requires": {
+ "cap2ui5": {
+ "apps": "srv/apps",
+ "roles": ["authenticated-user"],
+ "routes": ["/sap/bc/z2ui5", "/rest/root/z2ui5"]
+ }
}
}
}
@@ -23,8 +26,18 @@ These ship in the plugin's own `package.json` and apply until you override one:
| key | what it does |
|---|---|
| `apps` | the directory scanned for app modules, relative to `cds.root`. Every `.js`/`.mjs`/`.cjs` in it is imported once the runtime is up. A project without the directory simply has no JavaScript apps of its own. Apps a dependency brings come on top — see [Apps from a package](../guide/project-structure#apps-from-a-package) |
-| `requires` | the role the route demands. `null` lets anonymous callers in — read the box below first |
+| `roles` | who may call: a role, or a list of roles any one of which lets the user in, as with CAP's `@requires`. `any` or `null` lets anonymous callers in — read the box below first |
| `routes` | the paths the roundtrip answers on. Both defaults exist so that a frontend or a bookmark written for either name works. A GET on a route answers with the page that embeds the whole UI5 frontend — there is no separate static route to configure |
+| `body_parser.limit` | the largest roundtrip body, a larger one gets 413. No default of its own: CAP's `cds.server.body_parser.limit` applies, else `10mb`. A roundtrip carries the app's whole model, so a table of a few thousand rows is an ordinary request |
+
+`"cap2ui5": false` under `cds.requires` switches the plugin off: no route, and
+no `cap2ui5.Drafts` table in the model.
+
+::: info Coming from 0.1.0
+0.1.0 read a top-level `cds.cap2ui5`, with `requires` for the roles. Those
+settings still apply, and the log warns and names the new place: move them
+under `cds.requires.cap2ui5`, and rename `requires` to `roles`.
+:::
## Overriding
@@ -33,9 +46,12 @@ In your project's `package.json`:
```json
{
"cds": {
- "cap2ui5": {
- "apps": "srv/ui",
- "routes": ["/ui5"]
+ "requires": {
+ "cap2ui5": {
+ "apps": "srv/ui",
+ "routes": ["/ui5"],
+ "body_parser": { "limit": "20mb" }
+ }
}
}
}
@@ -44,24 +60,28 @@ In your project's `package.json`:
or per profile, the usual CAP way:
```json
-{ "cds": { "[production]": { "cap2ui5": { "requires": "MyUi5Role" } } } }
+{ "cds": { "[production]": { "requires": { "cap2ui5": { "roles": ["MyUi5Role"] } } } } }
```
-## Authentication — and what `null` costs
+## Authentication — and what opening the route costs
The route runs **behind CAP's own middleware chain**, so whatever
`cds.requires.auth` is configured to has already identified the caller by the
time the plugin's guard decides. That is not a detail: `cds.context`, and with
it `cds.context.user`, only exists where CAP's middlewares ran.
-The guard is one line: the caller must satisfy `cap2ui5.requires`.
-
-::: warning Setting `requires: null` opens more than the door
-With `null`, every caller is CAP's anonymous user — and the draft store binds
-each session to `cds.context.user.id`. So *all* anonymous visitors share one
-owner and therefore each other's sessions. That is the documented consequence
-of turning authentication off, not a defect, but a public demo and a shared
-staging system are very different things.
+The guard decides the way CAP decides for a service annotated with
+`@requires`: a user with any one of `cap2ui5.roles` is let in. A caller who is
+not logged in gets **401** with the auth strategy's login challenge; a
+logged-in user without the role gets **403**. Both are answered by CAP's own
+error middleware, in CAP's error format.
+
+::: warning Setting `roles` to `any` or `null` opens more than the door
+Then every caller who has not logged in is CAP's anonymous user — and the
+draft store binds each session to `cds.context.user.id`. So *all* anonymous
+visitors share one owner and therefore each other's sessions. That is the
+documented consequence of turning authentication off, not a defect, but a
+public demo and a shared staging system are very different things.
:::
Verified for **every** auth kind, not just the development ones — the chain is
@@ -85,11 +105,12 @@ run behind it under `xsuaa` and `ias` and not only under `mocked`.
for 0.3.0 — so `npm add @cap2ui5/cds-plugin` already gives you one known
runtime release. There is nothing to add to your own `package.json`.
-The plugin resolves the runtime from **your project** (`cds.root`) first and
-only then from its own location, and logs what it found at startup:
+The runtime resolves as the plugin's own dependency, at the pinned version.
+To load another release, use npm `overrides` in your `package.json`. The log
+names what was loaded at startup:
```
-[cap2ui5] @abap2ui5/node-runtime 1.145.0 from …/node_modules/@abap2ui5/node-runtime
+[cap2ui5] - @abap2ui5/node-runtime 1.145.0 from …/node_modules/@abap2ui5/node-runtime
```
Backend, UI5 frontend and wire protocol version come from that one package,
@@ -100,7 +121,7 @@ which is what makes a frontend/backend mismatch impossible. See
Everything the **framework** decides about a response — the UI5 bootstrap URL,
the Content-Security-Policy, the security headers, the theme, the draft expiry
-and the CSRF gate — is not a `cds.cap2ui5` option. It comes from the user
+and the CSRF gate — is not a `cds.requires.cap2ui5` option. It comes from the user
exit, registered with `defineExit`:
```js
diff --git a/docs/reference/deployment.md b/docs/reference/deployment.md
index 6fce5bc..2edb7e5 100644
--- a/docs/reference/deployment.md
+++ b/docs/reference/deployment.md
@@ -117,7 +117,7 @@ catch-all, with the approuter's token check off:
```
Add the same route for `rest/root/z2ui5` if your frontend or bookmarks use that
-path, and adjust both if you changed `cds.cap2ui5.routes`.
+path, and adjust both if you changed `cds.requires.cap2ui5.routes`.
This is not an open door. Authentication still applies — the route sets no
`authenticationType`, so the approuter's default applies, and the plugin's own
diff --git a/docs/reference/protocol.md b/docs/reference/protocol.md
index a8d5bbc..c77ae70 100644
--- a/docs/reference/protocol.md
+++ b/docs/reference/protocol.md
@@ -49,7 +49,7 @@ configurable — see [Configuration](./configuration).
| | |
|---|---|
| `ID` | which draft to continue. Empty starts a new app |
-| `EVENT`, `T_EVENT_ARG` | what the user did, and the arguments the control carried — `c.eventName` and `c.eventArg(i)` |
+| `EVENT`, `T_EVENT_ARG` | what the user did, and the arguments the control carried — `client.get_event()` and `client.get_event_arg(i)` |
| `MODEL` | the bound data as the browser has it; applied to the app instance before `main` runs |
## Response
@@ -76,8 +76,8 @@ Measured against the example, a start of `ZCL_JS_HELLO`:
| `S_ACTION.T_CUSTOM` | app-issued frontend actions |
| `MODEL` | the bound data as the server has it after `main` |
-`S_ACTION` is an **ordered list**, which is why two `c.messageToast()` calls
-arrive in the order you wrote them.
+`S_ACTION` is an **ordered list**, which is why two `client.message_toast_display()`
+calls arrive in the order you wrote them.
## `PROTOCOL` — the wire carries its own version
@@ -101,10 +101,14 @@ For a cap2UI5 project the check is belt and braces — both halves come from one
An unhandled error answers `500` with `roundtrip failed ()`.
The detail goes to the server log under the same id; see
-[Configuration](./configuration).
+[Configuration](./configuration). An app module that fails to load does not
+get that far: it fails the server's start.
Authentication is decided **before** the body is read: an unauthenticated
-caller gets `401` without the server buffering the payload.
+caller gets `401` with the auth strategy's login challenge, an authenticated
+user who lacks the configured role `403`, both without the server buffering
+the payload. A body over the limit gets `413`. These three are answered by
+CAP's error middleware, in CAP's error format.
## Next
diff --git a/scripts/verify-refs.mjs b/scripts/verify-refs.mjs
index ae14b2a..954139e 100644
--- a/scripts/verify-refs.mjs
+++ b/scripts/verify-refs.mjs
@@ -56,7 +56,7 @@
* lands on a file that exists. Two dead packages are reported by name,
* in either form: "abap2UI5/…", the port's package, and "cap2ui5", the
* plugin's name up to 0.2.0, withdrawn from npm.
- * 5. every plugin option named as `cds.cap2ui5.` is a key the plugin
+ * 5. every plugin option named as `cds.requires.cap2ui5.` is a key the plugin
* really defines, every three-part release number (1.x.y) is the runtime
* release the checkout pins or an allowlisted historical number, and a
* `"@cap2ui5/cds-plugin": "^x.y.z"` a page tells a reader to write is a
@@ -159,12 +159,17 @@ const PLUGIN_EXPORTS = (() => {
return inner ? new Set(inner[1].split(",").map((s) => s.trim()).filter(Boolean)) : null;
})();
-/** the plugin's own configuration keys, from package.json#cds.cap2ui5 */
+/** the plugin's own configuration keys, from package.json#cds.requires.cap2ui5
+ * (where they live since 0.2.0). `model` there is CAP's own key - the entry's
+ * CDS model - and not a setting of the plugin. Keys the plugin reads without a
+ * default in package.json (body_parser) are in .verify-refs-ignore. */
const PLUGIN_OPTIONS = (() => {
if (!haveApp) return null;
try {
const pkg = JSON.parse(fs.readFileSync(path.join(APP, "plugin", "package.json"), "utf8"));
- return new Set(Object.keys(pkg.cds?.cap2ui5 ?? {}));
+ const own = pkg.cds?.requires?.cap2ui5;
+ if (!own || typeof own !== "object") return null;
+ return new Set(Object.keys(own).filter((k) => k !== "model"));
} catch { return null; }
})();
@@ -276,10 +281,12 @@ const DESTRUCTURE_RE = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\(\s*["'`]@
// a file.
const IMPORT_RE = /\bimport\s+(?:[^"'`;]*?\s+from\s+)?["'`](@cap2ui5\/cds-plugin|cap2ui5|abap2UI5)(?:\/([^"'`]+))?["'`]/gi;
const IMPORT_NAMES_RE = /\bimport\s*\{([^}]*)\}\s*from\s*["'`]@cap2ui5\/cds-plugin["'`]/g;
-// `cds.cap2ui5.apps`, `cap2ui5.routes` in prose or a config block. Case
-// matters and the flag is deliberately absent: `cap2ui5.Drafts` is the CDS
-// ENTITY, which the docs name constantly and which is not an option at all.
-const OPTION_RE = /`(?:cds\.)?cap2ui5\.([a-z][a-z_]*)`/g;
+// `cds.requires.cap2ui5.apps`, `cap2ui5.routes` in prose or a config block.
+// Case matters and the flag is deliberately absent: `cap2ui5.Drafts` is the
+// CDS ENTITY, which the docs name constantly and which is not an option at all.
+// `cds.cap2ui5.` is 0.1.0's place - still read, with a deprecation
+// warning - so it is reported as the old place rather than checked.
+const OPTION_RE = /`(cds\.(?:requires\.)?)?cap2ui5\.([a-z][a-z_]*)`/g;
// which app ids a PAGE defines itself: a page teaching an app may name it
const DEFINES_RE = /defineApp\(\s*["'`]([A-Za-z0-9_]+)["'`]/g;
@@ -392,10 +399,15 @@ for (const file of markdownFiles(DOCS)) {
for (const m of line.matchAll(OPTION_RE)) {
if (!PLUGIN_OPTIONS) continue;
- const key = m[1].toLowerCase();
+ if (m[1] === "cds.") {
+ add(file, n, `cds.cap2ui5.${m[2]} is 0.1.0's place, deprecated - `
+ + `the settings are under cds.requires.cap2ui5`);
+ continue;
+ }
+ const key = m[2].toLowerCase();
if (PLUGIN_OPTIONS.has(key) || IGNORE.has(`cap2ui5.${key}`)) continue;
- add(file, n, `cap2ui5.${m[1]} is not a plugin option `
- + `(package.json#cds.cap2ui5 defines ${[...PLUGIN_OPTIONS].join(", ")})`);
+ add(file, n, `cap2ui5.${m[2]} is not a plugin option `
+ + `(package.json#cds.requires.cap2ui5 defines ${[...PLUGIN_OPTIONS].join(", ")})`);
}
for (const m of line.matchAll(CLASS_RE)) {