Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<option>` is an option the plugin defines,
- every `cds.requires.cap2ui5.<option>` is an option the plugin defines (and
0.1.0's `cds.cap2ui5.<option>` is reported as the deprecated place),
- every `1.x.y` release number is the pinned runtime release,
- every internal anchor exists.

Expand Down Expand Up @@ -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`
Expand All @@ -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`
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
]
}
Expand Down
65 changes: 56 additions & 9 deletions docs/api/app-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
```
Expand All @@ -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(`<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m">` +
`<Page title="Hello"><Input value="${this.client._bind("name")}"/></Page></mvc:View>`);
}

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

Expand All @@ -65,22 +104,30 @@ 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
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)`

Expand All @@ -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
Loading
Loading