diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3d59588..5d2a298 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,13 +1,45 @@
+# Why this file exists, and what it has to keep moving.
+#
+# Every workflow in this repository pins its actions to a commit SHA rather
+# than to `@v7`. A tag is a pointer its owner can move, so a job pinned to one
+# runs whatever it points at that day; a commit cannot change under you. The
+# cost of that safety is that nothing bumps a pin on its own — a pinned action
+# quietly becomes a stale action, which is exactly what happened here: the
+# deploy workflow sat two majors behind the rest of the organisation until
+# somebody read the two files side by side. This is the PR that says so.
+#
+# Two ecosystems, matching the rest of the organisation:
+#
+# npm vitepress. It builds the site, and the build is half of
+# `npm run check` — the gate every pull request passes.
+# github-actions the deploy and check workflows. Dependabot updates a SHA
+# pin in place and keeps the version comment right, so pinned
+# does not become stale.
version: 2
updates:
- # The other repos in the ecosystem pin their actions to commit SHAs, which
- # means nothing bumps them without a PR — this is that PR.
- - package-ecosystem: github-actions
- directory: /
+ - package-ecosystem: npm
+ directory: "/"
schedule:
interval: weekly
+ day: monday
+ time: "06:23"
+ timezone: Etc/UTC
+ open-pull-requests-limit: 5
+ groups:
+ # One PR for the toolchain rather than one per package: they move
+ # together, and what has to be verified is a site that still builds, not
+ # each package alone.
+ toolchain:
+ patterns: ["*"]
- - package-ecosystem: npm
- directory: /
+ - package-ecosystem: github-actions
+ directory: "/"
schedule:
interval: weekly
+ day: monday
+ time: "06:23"
+ timezone: Etc/UTC
+ open-pull-requests-limit: 5
+ groups:
+ actions:
+ patterns: ["*"]
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..a280ede
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,56 @@
+# What a pull request against this documentation gets checked for.
+#
+# There was nothing before this. verify-refs existed, but it ran only inside
+# deploy.yml — which runs on push to main, so the one gate that decides whether
+# the prose is still true about the code first spoke AFTER the merge, on the
+# published site. That is how thirteen pages could go on teaching a class the
+# framework had removed: nothing failed, because nothing ran in time.
+#
+# The job runs `npm run check` — the same command AGENTS.md tells a contributor
+# to run locally, so a green machine and a green CI mean the same thing.
+name: check
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+concurrency:
+ group: check-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ # The documented target. verify-refs checks every path, class, sample id
+ # and `require("abap2UI5/…")` on this site against a real checkout, so
+ # without it the run proves only that the site builds — it skips itself
+ # and exits 0 when the checkout is missing, which is exactly the silent
+ # pass this job exists to prevent.
+ - name: Check out cap2UI5 (reference target)
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ repository: cap2UI5/cap2UI5
+ ref: main
+ path: .cap2ui5-ref
+ fetch-depth: 1
+
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: '22'
+ cache: 'npm'
+ - run: npm ci
+
+ # verify-refs + vitepress build, in that order: a dead reference is
+ # reported even when the site would have built fine around it.
+ - name: npm run check
+ run: npm run check
+ env:
+ CAP2UI5_DIR: .cap2ui5-ref
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index fd420b0..926dd2b 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -19,42 +19,42 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Setup Node
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
- name: Setup Pages
- uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
+ uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Install dependencies
run: npm ci
# The docs make concrete claims about the cap2UI5 repository — paths,
- # class names, sample ids. Nothing else checks them: `vitepress build`
- # validates internal links and stops there, so every rename upstream
- # silently rots the prose. Verify against the real app.
+ # class names, sample ids, imports. Nothing else checks them:
+ # `vitepress build` validates internal links and stops there, so every
+ # rename upstream silently rots the prose. Verify against the real app.
- name: Check out cap2UI5 (reference target)
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: cap2UI5/cap2UI5
path: .cap2ui5-ref
- - name: Verify documented references
- run: node scripts/verify-refs.mjs
+ # verify-refs + vitepress build, through the same `npm run check` a
+ # contributor runs locally and check.yml runs on the pull request — one
+ # command, so the three cannot drift into checking different things.
+ - name: Check (verify-refs + build)
+ run: npm run check
env:
CAP2UI5_DIR: .cap2ui5-ref
- - name: Build with VitePress
- run: npm run docs:build
-
- name: Upload artifact
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: docs/.vitepress/dist
@@ -67,4 +67,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
+ uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
diff --git a/AGENTS.md b/AGENTS.md
index c71ff59..8e14c6c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,12 +8,19 @@ The VitePress documentation site for cap2UI5 (`docs/` holds the content,
`docs/.vitepress/config.mjs` the nav/sidebar). Build locally with
`npm ci && npx vitepress build docs`; dev server via `npx vitepress dev docs`.
-Before committing, run `npm run check` — that is `verify-refs` (every path,
-class and `?app_start=` named in the prose must resolve in a real cap2UI5
-checkout, and every internal anchor must exist) followed by the VitePress
-build. The verifier needs a checkout: `CAP2UI5_DIR=/path/to/cap2UI5`, or a
-sibling clone. It skips itself when there is none, so a green run without a
-checkout proves only that the site builds.
+Before committing, run `npm run check` — that is `verify-refs` followed by the
+VitePress build. It is also what CI runs, on every pull request
+(`.github/workflows/check.yml`) and on deploy. verify-refs checks that
+
+- every path, class and `?app_start=` named in the prose resolves in a real
+ cap2UI5 checkout,
+- every `require("abap2UI5/…")` **inside a code fence** resolves through the
+ exports map of `core/package.json` and onto a file that exists,
+- every internal anchor exists.
+
+The verifier needs a checkout: `CAP2UI5_DIR=/path/to/cap2UI5`, or a sibling
+clone. It skips itself when there is none, so a green run without a checkout
+proves only that the site builds.
Exceptions — placeholder class names, paths in other repos — go in
`docs/.verify-refs-ignore`, **with a reason**. An unexplained entry there is
@@ -34,9 +41,16 @@ against the repos, don't guess):
Path conventions inside the app repo:
-- framework classes: `core/srv/z2ui5/` (layers `00/` utils, `01/` core
- plumbing — including the shipped apps in `01/04/` since the 2026-08
- upstream rename, `02/` public API, `99/` add-ons like the pop helpers)
+- framework classes: `core/srv/z2ui5/` — exactly three layers: `00/` utils,
+ `01/` core plumbing (including the shipped apps in `01/04/` since the
+ 2026-08 upstream rename) and `02/` public API. There is no `99/`: upstream's
+ frozen legacy package is deliberately not carried into the port, so
+ `z2ui5_cl_xml_view`, `z2ui5_cl_xml_view_cc` and the `z2ui5_cl_pop_*` popups
+ do not exist here. The one view builder is `z2ui5_cl_ui5_view_builder`.
+- the vendored release is pinned: `z2ui5_if_app.version` says which one
+ (1.142.0 today). On it `_bind` is one-way and `_bind_edit` two-way —
+ upstream merged the two in 1.143.0, so upstream material can disagree with
+ what this core does.
- bundled demo samples (pipeline-owned, flat): `core/srv/app/samples/`
- user apps: `srv/app/` (or any folder via `Z2UI5_APP_DIRS` /
`require("abap2UI5/register-apps")(dir)`)
diff --git a/README.md b/README.md
index a09593b..535eeb9 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
# cap2UI5 — Documentation
-VitePress documentation for [**cap2UI5**](https://github.com/cap2UI5/cap2UI5) — the CAP / Node.js port of the [abap2UI5](https://github.com/abap2UI5/abap2UI5) concept. A zero-install playground of the framework runs at [cap2ui5.github.io/web-cap2UI5-build](https://cap2ui5.github.io/web-cap2UI5-build/) (built by [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) into [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build)).
+VitePress documentation for [**cap2UI5**](https://github.com/cap2UI5/cap2UI5) — the CAP / Node.js port of the [abap2UI5](https://github.com/abap2UI5/abap2UI5) concept. Published at **[cap2ui5.github.io/docs](https://cap2ui5.github.io/docs/)**.
+
+A zero-install playground of the framework runs at [cap2ui5.github.io/web-cap2UI5-build](https://cap2ui5.github.io/web-cap2UI5-build/) (built by [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) into [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build)).
## Develop locally
```bash
-npm install
+npm ci
npm run docs:dev
```
@@ -18,22 +20,23 @@ npm run docs:build # → docs/.vitepress/dist
npm run docs:preview # → preview server
```
-## Structure
+## Check before you commit
+```bash
+CAP2UI5_DIR=/path/to/cap2UI5 npm run check
```
-.
-├── docs/
-│ ├── .vitepress/
-│ │ ├── config.mjs # VitePress configuration
-│ │ └── theme/ # Custom theme (red brand color)
-│ ├── index.md # Landing page
-│ ├── guide/ # Concepts, quickstart, lifecycle, bindings, …
-│ ├── examples/ # End-to-end example apps
-│ ├── api/ # API reference (client, View Builder, App Interface)
-│ └── reference/ # Architecture, protocol, DB, deployment
-├── package.json
-└── README.md
-```
+
+`npm run check` is `verify-refs` followed by the VitePress build, and it is exactly what CI runs — on every pull request (`.github/workflows/check.yml`) and again on deploy. It is the only gate this repository has on whether the prose is still true about the code:
+
+- every path, class and `?app_start=` named in the docs must exist in a real [cap2UI5](https://github.com/cap2UI5/cap2UI5) checkout,
+- every `require("abap2UI5/…")` in a code example must resolve through the exports map of `core/package.json`,
+- every internal anchor must exist.
+
+`verify-refs` needs that checkout — pass `CAP2UI5_DIR`, or clone cap2UI5 next to this repository. **Without one it skips itself and exits 0**, so a green run with no checkout proves only that the site builds. Deliberate exceptions live in `docs/.verify-refs-ignore`, each with a reason.
+
+## Structure
+
+The folder scheme, the ground truth about the cap2UI5 repo layout and the rules for linking into it are in **[AGENTS.md](AGENTS.md)** — read it before making any change.
## License
diff --git a/docs/.verify-refs-ignore b/docs/.verify-refs-ignore
index 422542d..c9d2a68 100644
--- a/docs/.verify-refs-ignore
+++ b/docs/.verify-refs-ignore
@@ -18,4 +18,13 @@ app/customer-list # a hypothetical Fiori elements app, used for contrast
z2ui5_cl_app_xyz # stands for "your app class" in the interface description
my_first_app # the class the getting-started walkthrough has you write
my_app # same, in the why-cap2ui5 pitch
+my_app_name # stands for "your class" in the ?app_start= URL on the navigation page
ClassName # literal placeholder in the URL-parameter description
+
+# --- classes that exist UPSTREAM and deliberately not here -----------------
+# cap2UI5 does not carry abap2UI5's frozen src/99 (see guide/vs-abap2ui5). The
+# names are named there precisely to say they are absent, so the checker must
+# not read the mention as a claim that they exist.
+z2ui5_cl_xml_view # upstream's retired view builder — replaced here by z2ui5_cl_ui5_view_builder
+z2ui5_cl_xml_view_cc # its custom-control decorator, retired with it
+z2ui5_cl_pop_bal # one of upstream's frozen built-in popups, superseded by the popups add-on
diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index a8800cf..f252efe 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -1,5 +1,12 @@
import { defineConfig } from 'vitepress'
+// Where the site is actually served from. Link previews (LinkedIn, Slack,
+// WhatsApp, X) only accept ABSOLUTE urls in og:image — a relative
+// "/docs/logo.jpeg" is silently dropped and the preview falls back to the
+// grey placeholder card.
+const SITE_URL = 'https://cap2ui5.github.io/docs'
+const OG_IMAGE = `${SITE_URL}/logo.jpeg`
+
export default defineConfig({
title: 'cap2UI5',
description: 'Bringing the abap2UI5 concept to CAP / Node.js — server-driven UI5 apps written in pure JavaScript',
@@ -20,10 +27,24 @@ export default defineConfig({
},
head: [
+ ['link', { rel: 'icon', type: 'image/jpeg', href: '/docs/logo.jpeg' }],
+ ['link', { rel: 'apple-touch-icon', href: '/docs/logo.jpeg' }],
['meta', { name: 'theme-color', content: '#d03c4a' }],
['meta', { property: 'og:type', content: 'website' }],
+ ['meta', { property: 'og:site_name', content: 'cap2UI5' }],
+ ['meta', { property: 'og:url', content: `${SITE_URL}/` }],
['meta', { property: 'og:title', content: 'cap2UI5 — Server-driven UI5 for CAP' }],
- ['meta', { property: 'og:description', content: 'Build full UI5 applications from your CAP backend in JavaScript — no separate frontend project, no XML hand-crafting.' }]
+ ['meta', { property: 'og:description', content: 'Build full UI5 applications from your CAP backend in JavaScript — no separate frontend project, no XML hand-crafting.' }],
+ ['meta', { property: 'og:image', content: OG_IMAGE }],
+ ['meta', { property: 'og:image:type', content: 'image/jpeg' }],
+ // The logo is square (790x790), so the preview is a thumbnail card, not a
+ // wide banner — declaring the real size is what keeps it from being
+ // cropped. A 1200x630 banner would earn the large card; there is none yet.
+ ['meta', { property: 'og:image:width', content: '790' }],
+ ['meta', { property: 'og:image:height', content: '790' }],
+ ['meta', { property: 'og:image:alt', content: 'cap2UI5 — server-driven UI5 for CAP' }],
+ ['meta', { name: 'twitter:card', content: 'summary' }],
+ ['meta', { name: 'twitter:image', content: OG_IMAGE }]
],
themeConfig: {
@@ -52,9 +73,6 @@ export default defineConfig({
text: 'Getting Started',
items: [
{ text: 'What is cap2UI5?', link: '/guide/what-is-cap2ui5' },
- { text: 'Server-Driven UI, Explained', link: '/guide/server-driven-ui' },
- { text: 'Where cap2UI5 Comes From', link: '/guide/where-it-comes-from' },
- { text: 'The Ecosystem', link: '/guide/ecosystem' },
{ text: 'Why cap2UI5?', link: '/guide/why-cap2ui5' },
{ text: 'Try It in the Browser', link: '/guide/playground' },
{ text: 'Quickstart', link: '/guide/getting-started' },
@@ -86,6 +104,13 @@ export default defineConfig({
{ text: 'cap2UI5 vs. Fiori Elements', link: '/guide/vs-fiori-elements' },
{ text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' }
]
+ },
+ {
+ text: 'Background',
+ items: [
+ { text: 'Where cap2UI5 Comes From', link: '/guide/where-it-comes-from' },
+ { text: 'The Ecosystem', link: '/guide/ecosystem' }
+ ]
}
],
diff --git a/docs/api/client.md b/docs/api/client.md
index f59d2fb..8bec161 100644
--- a/docs/api/client.md
+++ b/docs/api/client.md
@@ -17,13 +17,22 @@ The `client` object is the only interface your app has to the outside world duri
| Method | Returns | Description |
|---|---|---|
-| `_bind(value, opts?)` | `string` | One-way binding → `{/path}` |
-| `_bind_edit(value, opts?)` | `string` | Two-way binding → `{/XX/path}` |
-| `_bind_local(value)` | `string` | Local binding without an app property |
+| `_bind(value, opts?)` | `string` | One-way binding → `{/PATH}` |
+| `_bind_edit(value, opts?)` | `string` | Two-way binding → `{/XX/PATH}` |
+| `_bind_local(value)` | `string` | Local binding without an app property → `{/__local_N}` |
+
+Paths are uppercased (`this.user_name` → `/XX/USER_NAME`) and mapped back onto the real property case-insensitively when the delta returns.
+
+::: info The two are one method upstream — but not here
+On the framework release cap2UI5 pins (**1.142.0**, `z2ui5_if_app.version`) these are genuinely two bindings: `_bind` writes into the model root and is read-only on the frontend, `_bind_edit` writes into the `XX` namespace the frontend can write back through. That is what `z2ui5_cl_ui5_srv_bind` does in the shipped code, and what this documentation describes throughout.
+
+abap2UI5 resolved the split in **1.143.0**: there `_bind_edit` is an alias of `_bind`, and the reverse-formatter options are accepted but ignored. If you are reading upstream material, that is the difference you are looking at. → [cap2UI5 vs. abap2UI5](../guide/vs-abap2ui5)
+:::
**`opts`** for `_bind` / `_bind_edit`:
- `path: true` → returns the bare path without `{...}`
- `path: "name"` → explicit path, no reference lookup
+- `name: "s_screen-city"` → resolve a member inside a bound structure by name
- `custom_mapper: ".fmt"` → formatter function name
- `custom_mapper_back: ".fmtBack"` → reverse formatter (only `_bind_edit`)
- `custom_filter: ".f"` / `custom_filter_back: ".fb"` → aliases
diff --git a/docs/examples/external-odata.md b/docs/examples/external-odata.md
index 3dd753d..298d7be 100644
--- a/docs/examples/external-odata.md
+++ b/docs/examples/external-odata.md
@@ -28,9 +28,9 @@ The CSN model is generated as usual with `cds import https://services.odata.org/
```js
// srv/app/read_odata.js
-const cds = require("@sap/cds");
-const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
-const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
+const cds = require("@sap/cds");
+const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
+const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
class read_odata extends z2ui5_if_app {
@@ -42,19 +42,25 @@ class read_odata extends z2ui5_if_app {
const northwind = await cds.connect.to("northwind");
this.customers = await northwind.run(SELECT.from("Customers").limit(50));
- const view = z2ui5_cl_xml_view.factory();
- const page = view.Page({ title: "Northwind - Customers" });
+ 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` });
- const tab = page.Table({ items: client._bind_edit(this.customers) });
- const cols = tab.columns();
- cols.Column().Text({ text: "CompanyName" });
- cols.Column().Text({ text: "ContactName" });
- cols.Column().Text({ text: "Country" });
+ const tab = view.ele(`Shell`).ele(`Page`)
+ .a({ n: `title`, v: `Northwind - Customers` })
+ .ele(`Table`)
+ .a({ n: `items`, v: client._bind_edit(this.customers) });
- tab.items().ColumnListItem().cells()
- .Input({ value: "{CompanyName}", enabled: true })
- .Input({ value: "{ContactName}", enabled: true })
- .Text({ text: "{Country}" });
+ const cols = tab.ele(`columns`);
+ cols.ele(`Column`).tag(`Text`).a({ n: `text`, v: `CompanyName` });
+ cols.ele(`Column`).tag(`Text`).a({ n: `text`, v: `ContactName` });
+ cols.ele(`Column`).tag(`Text`).a({ n: `text`, v: `Country` });
+
+ tab.ele(`items`).ele(`ColumnListItem`).ele(`cells`)
+ .tag(`Input`).a({ n: `value`, v: `{COMPANYNAME}` }).a({ n: `enabled`, b: true })
+ .tag(`Input`).a({ n: `value`, v: `{CONTACTNAME}` }).a({ n: `enabled`, b: true })
+ .tag(`Text`).a({ n: `text`, v: `{COUNTRY}` });
client.view_display(view.stringify());
@@ -83,12 +89,13 @@ The same code you would write in a `srv/z2ui5-service.js` handler. **Anything No
### 2. Two-way on a list
```js
-.Table({ items: client._bind_edit(this.customers) })
-.items().ColumnListItem().cells()
- .Input({ value: "{CompanyName}", enabled: true })
+const tab = page.ele(`Table`).a({ n: `items`, v: client._bind_edit(this.customers) });
+
+tab.ele(`items`).ele(`ColumnListItem`).ele(`cells`)
+ .tag(`Input`).a({ n: `value`, v: `{COMPANYNAME}` }).a({ n: `enabled`, b: true });
```
-Because the array is two-way bound via `_bind_edit`, UI5 writes user edits **on every item property** back into the XX delta. On the next roundtrip `this.customers` holds the modified state.
+Because the array is two-way bound via `_bind_edit`, UI5 writes user edits **on every item property** back into the XX delta. On the next roundtrip `this.customers` holds the modified state. The item-relative paths are uppercase (`{COMPANYNAME}`, not `{CompanyName}`): the model is written with uppercase names and mapped back onto your properties case-insensitively.
### 3. Persistence caveat
@@ -104,11 +111,16 @@ async main(client) {
if (client.check_on_init()) {
client.set_odata_model("/odata/v4/admin/NorthwindCustomers");
- const view = z2ui5_cl_xml_view.factory();
- view.Page({ title: "Customers (OData)" })
- .Table({ items: "{/NorthwindCustomers}" })
- // ↑ no _bind_edit, but a static OData path binding
- .columns().Column().Text({ text: "Company" });
+ 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` });
+
+ view.ele(`Shell`).ele(`Page`).a({ n: `title`, v: `Customers (OData)` })
+ .ele(`Table`)
+ .a({ n: `items`, v: `{/NorthwindCustomers}` })
+ // ↑ no _bind_edit, but a static OData path binding
+ .ele(`columns`).ele(`Column`).tag(`Text`).a({ n: `text`, v: `Company` });
client.view_display(view.stringify());
}
diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md
index 3b67645..650c6ad 100644
--- a/docs/examples/hello-world.md
+++ b/docs/examples/hello-world.md
@@ -4,31 +4,46 @@ The simplest variation of a cap2UI5 app: an input field, a button, a confirmatio
## Code
+This is `core/srv/z2ui5/01/04/z2ui5_cl_ui5_app_hi_world.js`, the app the framework ships and the one `?app_start=z2ui5_cl_ui5_app_hi_world` starts — quoted as it stands:
+
```js
-// core/srv/z2ui5/01/04/z2ui5_cl_ui5_app_hi_world.js (shipped with the framework)
-const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
-const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
+const z2ui5_cl_ui5_view_builder = require("../../02/z2ui5_cl_ui5_view_builder");
+const z2ui5_if_app = require("../../02/z2ui5_if_app");
class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app {
-
- name = "";
+ name = ``;
async main(client) {
-
if (client.check_on_init()) {
- const view = z2ui5_cl_xml_view.factory()
- .Shell()
- .Page({ title: "abap2UI5 - Hello World" })
- .SimpleForm({ editable: true })
- .content()
- .Title({ text: "Make an input here and send it to the server..." })
- .Label({ text: "Name" })
- .Input({ value: client._bind_edit(this.name) })
- .Button({ text: "Send", press: client._event("BUTTON_POST") });
+ 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: `xmlns:core`, v: `sap.ui.core` })
+ // SimpleForm and its content aggregation live in sap.ui.layout.form
+ .a({ n: `xmlns:form`, v: `sap.ui.layout.form` });
+
+ const form = view
+ .ele({ n: `Shell` })
+ .ele({ n: `Page` })
+ .a({ n: `title`, v: `abap2UI5 - Hello World` })
+ .ele({ n: `SimpleForm`, ns: `form` })
+ .a({ n: `editable`, b: true })
+ .ele({ n: `content`, ns: `form` });
+
+ form
+ .tag({ n: `Title`, ns: `core` })
+ .a({ n: `text`, v: `Enter a value and send it to the server...` })
+ .tag({ n: `Label` })
+ .a({ n: `text`, v: `Name` })
+ .tag({ n: `Input` })
+ .a({ n: `value`, v: client._bind_edit(this.name) })
+ .tag({ n: `Button` })
+ .a({ n: `text`, v: `Send` })
+ .a({ n: `press`, v: client._event(`BUTTON_POST`) });
client.view_display(view.stringify());
-
- } else if (client.check_on_event("BUTTON_POST")) {
+ } else if (client.check_on_event(`BUTTON_POST`)) {
client.message_box_display(`Your name is ${this.name}`);
}
}
@@ -37,6 +52,41 @@ class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app {
module.exports = z2ui5_cl_ui5_app_hi_world;
```
+::: tip The two import lines are the one thing you write differently
+This file lives *inside* the core package, so it reaches its neighbours by relative path. Your own app sits outside the package and imports through its exports map:
+
+```js
+const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
+const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
+```
+
+Everything below those two lines is identical.
+:::
+
+## The view it renders
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Two details of the builder are visible here and are worth taking with you:
+
+- **`.ele()` descends, `.tag()` stays.** `Shell`, `Page`, `SimpleForm` and `content` are containers, so they get `.ele()`. The four controls inside `content` are leaves added to the *same* parent, so they get `.tag()` — and `.a()` after a `.tag()` sets the attribute on that last child, not on `content`.
+- **You name the namespace.** `SimpleForm`, its `content` aggregation and `Title` are not in `sap.m`. An unprefixed `` would resolve to `sap/m/SimpleForm.js` and the view would fail to *load*. → [Namespaces are yours to declare](../api/view-builder#namespaces)
+
## What happens
| Phase | What runs |
@@ -44,7 +94,7 @@ module.exports = z2ui5_cl_ui5_app_hi_world;
| **Initial load** | Frontend POSTs an empty body. Server has no `S_FRONT.ID`, falls back to the startup app. User clicks the "Hello World" link. |
| **App start** | New `app_start=z2ui5_cl_ui5_app_hi_world` starts. `check_on_init() === true`, view is rendered. |
| **User types** | Two-way binding via `client._bind_edit(this.name)` — value flows into the XX delta. |
-| **User clicks "Send"** | Frontend sends `S_FRONT.EVENT = "BUTTON_POST"` + XX delta with `name`. Server applies the delta to `this.name` and calls `main()`. |
+| **User clicks "Send"** | Frontend sends `S_FRONT.EVENT = "BUTTON_POST"` + XX delta with `NAME`. Server applies the delta to `this.name` and calls `main()`. |
| **`check_on_event("BUTTON_POST")`** | True → `message_box_display(...)` with the current name. |
## Launch
@@ -63,7 +113,7 @@ https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_ui5_app_hi_world
- **One file = one app.** Class names match file names.
- **Two phases.** `check_on_init()` for the initial view, `check_on_event(...)` for events.
-- **Reference-equality bindings.** `client._bind_edit(this.name)` finds the path `/XX/name` itself.
+- **Reference-equality bindings.** `client._bind_edit(this.name)` finds the path `/XX/NAME` itself.
- **Pure JavaScript.** No manifest, no component, no OData layer.
→ Continue with [**Selection Screen**](./selection-screen) for a richer form with various control types.
diff --git a/docs/examples/list.md b/docs/examples/list.md
index b318df6..f763abe 100644
--- a/docs/examples/list.md
+++ b/docs/examples/list.md
@@ -6,8 +6,8 @@ A list with a selection-change event that reacts to a row selection. Shows the p
```js
// srv/app/my_list.js
-const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
-const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
+const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
+const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
class my_list extends z2ui5_if_app {
@@ -25,27 +25,28 @@ class my_list extends z2ui5_if_app {
{ title: "row_05", descr: "Description 5", icon: "sap-icon://account", info: "completed", selected: false },
];
- const view = z2ui5_cl_xml_view.factory();
- const page = view.Shell().Page({
- title: "abap2UI5 - List",
- navButtonPress: client._event_nav_app_leave(),
- showNavButton: client.check_app_prev_stack(),
- });
-
- page.List({
- headerText: "Items",
- items: client._bind_edit(this.t_tab),
- mode: "SingleSelectMaster",
- selectionChange: client._event("SELCHANGE"),
- })
- .StandardListItem({
- title: "{title}",
- description: "{descr}",
- icon: "{icon}",
- info: "{info}",
- press: client._event("ITEM_PRESS"),
- selected: "{selected}",
- });
+ 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` });
+
+ const page = view.ele(`Shell`).ele(`Page`)
+ .a({ n: `title`, v: `abap2UI5 - List` })
+ .a({ n: `navButtonPress`, v: client._event_nav_app_leave() })
+ .a({ n: `showNavButton`, b: client.check_app_prev_stack() });
+
+ page.ele(`List`)
+ .a({ n: `headerText`, v: `Items` })
+ .a({ n: `items`, v: client._bind_edit(this.t_tab) })
+ .a({ n: `mode`, v: `SingleSelectMaster` })
+ .a({ n: `selectionChange`, v: client._event(`SELCHANGE`) })
+ .tag(`StandardListItem`)
+ .a({ n: `title`, v: `{TITLE}` })
+ .a({ n: `description`, v: `{DESCR}` })
+ .a({ n: `icon`, v: `{ICON}` })
+ .a({ n: `info`, v: `{INFO}` })
+ .a({ n: `press`, v: client._event(`ITEM_PRESS`) })
+ .a({ n: `selected`, v: `{SELECTED}` });
client.view_display(view.stringify());
@@ -68,18 +69,21 @@ module.exports = my_list;
### Bindings for aggregation slots
```js
-.List({
- items: client._bind_edit(this.t_tab),
- ...
-})
-.StandardListItem({
- title: "{title}", // ← path relative to the item
- description: "{descr}",
- selected: "{selected}",
-});
+page.ele(`List`)
+ .a({ n: `items`, v: client._bind_edit(this.t_tab) })
+ .tag(`StandardListItem`)
+ .a({ n: `title`, v: `{TITLE}` }) // ← path relative to the item
+ .a({ n: `description`, v: `{DESCR}` })
+ .a({ n: `selected`, v: `{SELECTED}` });
```
-`items` gets the top-level binding (`{/XX/t_tab}`). Inside `StandardListItem`, all paths are **relative to the item** — `{title}` refers to `t_tab[N].title`.
+`items` gets the top-level binding (`{/XX/T_TAB}`). Inside `StandardListItem`, all paths are **relative to the item** — `{TITLE}` refers to `t_tab[N].title`.
+
+::: tip Why the uppercase paths
+Model paths are uppercased on the way out (`this.t_tab` → `/XX/T_TAB`, the column `title` → `{TITLE}`) — the abap2UI5 wire format, where component names are ABAP identifiers. The write-back maps them onto your real, lowercase properties case-insensitively, so `this.t_tab[0].title` is what you read in `main()`. Write the relative paths uppercase and they will match.
+:::
+
+There is no `items` element in the chain: `items` is the default aggregation of `List`, so a child added with `.tag()` lands in it. An aggregation that is *not* the default one (a `Table`'s `columns`, a `Page`'s `footer`) is written out as an element of its own.
### `selectionChange` vs. `press`
diff --git a/docs/examples/selection-screen.md b/docs/examples/selection-screen.md
index efcb64b..1d57cde 100644
--- a/docs/examples/selection-screen.md
+++ b/docs/examples/selection-screen.md
@@ -6,8 +6,8 @@ A classic input mask with combobox, DatePicker, checkbox, switch — comparable
```js
// srv/app/selection_screen.js
-const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
-const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
+const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
+const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
class selection_screen extends z2ui5_if_app {
@@ -85,63 +85,104 @@ class selection_screen extends z2ui5_if_app {
}
render(client) {
- const view = z2ui5_cl_xml_view.factory();
- const page = view.Shell().Page({
- title: "abap2UI5 - Selection Screen",
- navButtonPress: client._event_nav_app_leave(),
- showNavButton: client.check_app_prev_stack(),
- });
+ 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: `xmlns:core`, v: `sap.ui.core` })
+ .a({ n: `xmlns:form`, v: `sap.ui.layout.form` })
+ .a({ n: `xmlns:l`, v: `sap.ui.layout` });
+
+ const page = view.ele(`Shell`).ele(`Page`)
+ .a({ n: `title`, v: `abap2UI5 - Selection Screen` })
+ .a({ n: `navButtonPress`, v: client._event_nav_app_leave() })
+ .a({ n: `showNavButton`, b: client.check_app_prev_stack() });
// 1) pull out the path to s_screen → manually build sub-paths
const screenPath = client._bind_edit(this.s_screen, { path: true });
- const screen = (k) => `{${screenPath}/${k}}`;
-
- const grid = page.Grid({ defaultSpan: "L6 M12 S12" }).content();
-
- const sf1 = grid.SimpleForm({ title: "Input", editable: true }).content();
- sf1.Label({ text: "Color (with suggestions)" });
- sf1.Input({
- value: screen("colour"),
- placeholder: "Enter your favorite color",
- suggestionItems: client._bind(this.t_suggestions),
- showSuggestion: true,
- }).get().suggestionItems().ListItem({ text: "{value}", additionalText: "{descr}" });
-
- const sf2 = grid.SimpleForm({ title: "Time Inputs", editable: true }).content();
- sf2.Label({ text: "Date" }).DatePicker({ value: screen("date") });
- sf2.Label({ text: "Date / Time" }).DateTimePicker({ value: screen("date_time") });
- sf2.Label({ text: "Time Start / End" });
- sf2.TimePicker({ value: screen("time_start") });
- sf2.TimePicker({ value: screen("time_end") });
-
- const content = page.Grid({ defaultSpan: "L12 M12 S12" })
- .content()
- .SimpleForm({ title: "Selection", editable: true })
- .content();
-
- content.Label({ text: "Active" });
- content.CheckBox({ selected: screen("check_is_active"), text: "Active", enabled: true });
-
- content.Label({ text: "Combo" });
- content.ComboBox({
- selectedKey: screen("combo_key"),
- items: client._bind(this.t_combo),
- }).Item({ key: "{key}", text: "{text}" });
-
- content.Label({ text: "Segmented" });
- content.SegmentedButton({ selectedKey: screen("segment_key") })
- .items()
- .SegmentedButtonItem({ key: "BLUE", icon: "sap-icon://accept", text: "blue" })
- .SegmentedButtonItem({ key: "GREEN", icon: "sap-icon://add-favorite", text: "green" })
- .SegmentedButtonItem({ key: "BLACK", icon: "sap-icon://attachment", text: "black" });
-
- content.Label({ text: "Switch 1" }).Switch({ state: screen("check_switch_01") });
- content.Label({ text: "Switch 2" }).Switch({ state: screen("check_switch_02") });
-
- const footer = page.footer().OverflowToolbar();
- footer.ToolbarSpacer();
- footer.Button({ text: "Clear", press: client._event("BUTTON_CLEAR"), type: "Reject", icon: "sap-icon://delete" });
- footer.Button({ text: "Send", press: client._event("BUTTON_SEND"), type: "Success" });
+ const screen = (k) => `{${screenPath}/${k.toUpperCase()}}`;
+
+ const grid = page.ele({ n: `Grid`, ns: `l` })
+ .a({ n: `defaultSpan`, v: `L6 M12 S12` })
+ .ele({ n: `content`, ns: `l` });
+
+ const sf1 = grid.ele({ n: `SimpleForm`, ns: `form` })
+ .a({ n: `title`, v: `Input` })
+ .a({ n: `editable`, b: true })
+ .ele({ n: `content`, ns: `form` });
+
+ sf1.tag(`Label`).a({ n: `text`, v: `Color (with suggestions)` });
+ sf1.ele(`Input`)
+ .a({ n: `value`, v: screen(`colour`) })
+ .a({ n: `placeholder`, v: `Enter your favorite color` })
+ .a({ n: `suggestionItems`, v: client._bind(this.t_suggestions) })
+ .a({ n: `showSuggestion`, b: true })
+ .ele(`suggestionItems`)
+ .tag({ n: `ListItem`, ns: `core` })
+ .a({ n: `text`, v: `{VALUE}` })
+ .a({ n: `additionalText`, v: `{DESCR}` });
+
+ const sf2 = grid.ele({ n: `SimpleForm`, ns: `form` })
+ .a({ n: `title`, v: `Time Inputs` })
+ .a({ n: `editable`, b: true })
+ .ele({ n: `content`, ns: `form` });
+
+ sf2.tag(`Label`).a({ n: `text`, v: `Date` })
+ .tag(`DatePicker`).a({ n: `value`, v: screen(`date`) })
+ .tag(`Label`).a({ n: `text`, v: `Date / Time` })
+ .tag(`DateTimePicker`).a({ n: `value`, v: screen(`date_time`) })
+ .tag(`Label`).a({ n: `text`, v: `Time Start / End` })
+ .tag(`TimePicker`).a({ n: `value`, v: screen(`time_start`) })
+ .tag(`TimePicker`).a({ n: `value`, v: screen(`time_end`) });
+
+ const content = page.ele({ n: `Grid`, ns: `l` })
+ .a({ n: `defaultSpan`, v: `L12 M12 S12` })
+ .ele({ n: `content`, ns: `l` })
+ .ele({ n: `SimpleForm`, ns: `form` })
+ .a({ n: `title`, v: `Selection` })
+ .a({ n: `editable`, b: true })
+ .ele({ n: `content`, ns: `form` });
+
+ content.tag(`Label`).a({ n: `text`, v: `Active` })
+ .tag(`CheckBox`)
+ .a({ n: `selected`, v: screen(`check_is_active`) })
+ .a({ n: `text`, v: `Active` })
+ .a({ n: `enabled`, b: true });
+
+ content.tag(`Label`).a({ n: `text`, v: `Combo` });
+ content.ele(`ComboBox`)
+ .a({ n: `selectedKey`, v: screen(`combo_key`) })
+ .a({ n: `items`, v: client._bind(this.t_combo) })
+ .tag({ n: `Item`, ns: `core` })
+ .a({ n: `key`, v: `{KEY}` })
+ .a({ n: `text`, v: `{TEXT}` });
+
+ content.tag(`Label`).a({ n: `text`, v: `Segmented` });
+ const seg = content.ele(`SegmentedButton`)
+ .a({ n: `selectedKey`, v: screen(`segment_key`) });
+ seg.tag(`SegmentedButtonItem`)
+ .a({ n: `key`, v: `BLUE` }).a({ n: `icon`, v: `sap-icon://accept` }).a({ n: `text`, v: `blue` })
+ .tag(`SegmentedButtonItem`)
+ .a({ n: `key`, v: `GREEN` }).a({ n: `icon`, v: `sap-icon://add-favorite` }).a({ n: `text`, v: `green` })
+ .tag(`SegmentedButtonItem`)
+ .a({ n: `key`, v: `BLACK` }).a({ n: `icon`, v: `sap-icon://attachment` }).a({ n: `text`, v: `black` });
+
+ content.tag(`Label`).a({ n: `text`, v: `Switch 1` })
+ .tag(`Switch`).a({ n: `state`, v: screen(`check_switch_01`) })
+ .tag(`Label`).a({ n: `text`, v: `Switch 2` })
+ .tag(`Switch`).a({ n: `state`, v: screen(`check_switch_02`) });
+
+ const footer = page.ele(`footer`).ele(`OverflowToolbar`);
+ footer.tag(`ToolbarSpacer`);
+ footer.tag(`Button`)
+ .a({ n: `text`, v: `Clear` })
+ .a({ n: `press`, v: client._event(`BUTTON_CLEAR`) })
+ .a({ n: `type`, v: `Reject` })
+ .a({ n: `icon`, v: `sap-icon://delete` });
+ footer.tag(`Button`)
+ .a({ n: `text`, v: `Send` })
+ .a({ n: `press`, v: client._event(`BUTTON_SEND`) })
+ .a({ n: `type`, v: `Success` });
client.view_display(view.stringify());
}
@@ -160,11 +201,11 @@ const screenPath = client._bind_edit(this.s_screen, { path: true });
// "give me the bare path,
// not wrapped in {...}"
-const screen = (k) => `{${screenPath}/${k}}`;
-// screen("colour") === "{/XX/s_screen/colour}"
+const screen = (k) => `{${screenPath}/${k.toUpperCase()}}`;
+// screen("colour") === "{/XX/S_SCREEN/COLOUR}"
```
-`_bind_edit(this.s_screen)` returns the path to the entire structure object via reference equality. With `{ path: true }` you get it unwrapped — and can then append sub-paths yourself.
+`_bind_edit(this.s_screen)` returns the path to the entire structure object via reference equality. With `{ path: true }` you get it unwrapped — and can then append sub-paths yourself. The sub-path is uppercased because model paths are: the framework writes `s_screen` into the model as `S_SCREEN` and maps the delta back onto your lowercase property when the roundtrip returns.
This saves you from **binding every sub-field individually** — a single lookup, many bindings.
diff --git a/docs/examples/static-xml-view.md b/docs/examples/static-xml-view.md
index 9fd8f1d..ceb3e1a 100644
--- a/docs/examples/static-xml-view.md
+++ b/docs/examples/static-xml-view.md
@@ -89,6 +89,19 @@ This keeps the wire format encapsulated and you stay safe across updates.
## Hybrid: view builder + static XML snippets
-Sometimes you only want to load **a piece** of the view from XML — e.g. a static footer. The builder has the `xml_load(...)` method (see `z2ui5_cl_xml_view.js`) which embeds an XML fragment into the running view.
+Sometimes you only want a **piece** of the view to come from a file — a static footer, say. The builder has no "embed this XML here" method: it renders a tree of elements it built itself, and `stringify()` is the only way out of it. So the seam is the string, not the builder:
+
+```js
+// … build the dynamic part with the builder, and mark the seam with an
+// element you can find again in the output:
+page.tag(`Text`).a({ n: `text`, v: `__FOOTER__` });
+
+const footer = fs.readFileSync(path.join(__dirname, "footer.xml"), "utf8");
+client.view_display(
+ view.stringify().replace(``, footer),
+);
+```
+
+Build the dynamic part with the builder, put a placeholder element where the static part goes, and splice the file in before `view_display`. Nothing in the framework has to know the difference — the frontend renderer sees one XML string either way.
→ You're now through all the examples. Continue to the [**API reference**](../api/client) for full method listings.
diff --git a/docs/guide/data-binding.md b/docs/guide/data-binding.md
index bc81031..3cfbbc7 100644
--- a/docs/guide/data-binding.md
+++ b/docs/guide/data-binding.md
@@ -5,10 +5,12 @@ Data binding is the trick that lets cap2UI5 work without model boilerplate. This
## The two bindings
```js
-client._bind(value) // → "{/path}" one-way (read-only on the frontend)
-client._bind_edit(value) // → "{/XX/path}" two-way (frontend can write back)
+client._bind(value) // → "{/PATH}" one-way (read-only on the frontend)
+client._bind_edit(value) // → "{/XX/PATH}" two-way (frontend can write back)
```
+Paths come back **uppercased** — `this.username` binds as `/XX/USERNAME`. That is the abap2UI5 wire format (ABAP component names are uppercase), and the delta is mapped back onto your lowercase property when the roundtrip returns. Write literal, relative paths uppercase and they will line up.
+
Both methods take a **value**, look in `client.oApp` (= your app instance) to find **which property that value is**, and return a UI5 binding path.
```js
@@ -17,8 +19,8 @@ class my_app extends z2ui5_if_app {
username = "Alice";
async main(client) {
- const path1 = client._bind(this.username); // → "{/username}"
- const path2 = client._bind_edit(this.username); // → "{/XX/username}"
+ const path1 = client._bind(this.username); // → "{/USERNAME}"
+ const path2 = client._bind_edit(this.username); // → "{/XX/USERNAME}"
}
}
```
@@ -68,17 +70,17 @@ A cap2UI5 response contains a `MODEL` object that is set as the **default model*
```json
{
"MODEL": {
- "users": [/* ... */], // ← one-way bindings (top level)
- "title": "Hello",
+ "USERS": [/* ... */], // ← one-way bindings (top level)
+ "TITLE": "Hello",
"XX": {
- "username": "Alice", // ← two-way bindings (XX namespace)
- "is_active": true
+ "USERNAME": "Alice", // ← two-way bindings (XX namespace)
+ "IS_ACTIVE": true
}
}
}
```
-When the user types in an `Input`, UI5 writes the value back to `/XX/username`. On the next roundtrip the frontend sends an **XX delta** with all changed values to the server. The server engine (`z2ui5_cl_ui5_srv_model.main_json_to_attri`) applies this delta to the deserialized app instance **before** `main()` is called — meaning: in `main()`, `this.username` is already the new value the user typed.
+When the user types in an `Input`, UI5 writes the value back to `/XX/USERNAME`. On the next roundtrip the frontend sends an **XX delta** with all changed values to the server. The server engine (`z2ui5_cl_ui5_srv_model.main_json_to_attri`) applies this delta to the deserialized app instance **before** `main()` is called — meaning: in `main()`, `this.username` is already the new value the user typed.
## Options
@@ -88,8 +90,9 @@ client._bind_edit(value, opts);
| Option | Meaning |
|---|---|
-| `path: true` | returns the **raw path** (`"/XX/username"`), not wrapped in `{...}` |
+| `path: true` | returns the **raw path** (`"/XX/USERNAME"`), not wrapped in `{...}` |
| `path: "myField"` | skips the reference lookup and uses the given path |
+| `name: "s_screen-city"` | resolves a member *inside* a bound structure by name |
| `custom_mapper: ".fmt"` | formatter function name → output: `{path: '...', formatter: '.fmt'}` |
| `custom_mapper_back: ".fmtBack"` | reverse formatter (two-way only) |
| `view: "POPUP"` | target view — rarely needed |
@@ -99,12 +102,12 @@ Examples:
```js
// raw path for relative bindings (table items)
const tabPath = client._bind_edit(this.users, { path: true });
-// "/XX/users"
+// "/XX/USERS"
// inside the table item structure, fields are relative:
-view.Table({ items: client._bind_edit(this.users) })
- .Column()
- .Text({ text: "{name}" }); // ← '{name}' relative to the item
+const tab = page.ele(`Table`).a({ n: `items`, v: client._bind_edit(this.users) });
+tab.ele(`items`).ele(`ColumnListItem`).ele(`cells`)
+ .tag(`Text`).a({ n: `text`, v: `{NAME}` }); // ← '{NAME}' relative to the item
```
## Local bindings
@@ -112,7 +115,7 @@ view.Table({ items: client._bind_edit(this.users) })
Sometimes you want a view-internal variable that should _not_ live as an app property:
```js
-client._bind_local(initialValue); // → "{/__local_3}"
+client._bind_local(initialValue); // → "{/__local_3}" (the one path that is not uppercased)
```
Creates an anonymous path with the given initial value. Useful for visual helper state that the server never needs.
@@ -122,7 +125,9 @@ Creates an anonymous path with the given initial value. Useful for visual helper
Bindings are one half; the other is `_event`:
```js
-view.Button({ text: "Save", press: client._event("BUTTON_SAVE") });
+page.tag(`Button`)
+ .a({ n: `text`, v: `Save` })
+ .a({ n: `press`, v: client._event(`BUTTON_SAVE`) });
```
`_event(name)` builds the UI5 press handler string that sends the event back to the server via the roundtrip. The actual `BUTTON_SAVE` then shows up in `client.get().EVENT`.
@@ -134,8 +139,9 @@ view.Button({ text: "Save", press: client._event("BUTTON_SAVE") });
When the event should run **only on the frontend** (no server roundtrip):
```js
-view.Button({
- press: client._event_client(client.cs_event.OPEN_NEW_TAB, ["https://sap.com"])
+page.tag(`Button`).a({
+ n: `press`,
+ v: client._event_client(client.cs_event.OPEN_NEW_TAB, [`https://sap.com`]),
});
```
@@ -170,19 +176,36 @@ class profile extends z2ui5_if_app {
}
render(client) {
- const view = z2ui5_cl_xml_view.factory();
- view.Page({ title: "Profile" })
- .SimpleForm({ editable: true })
- .content()
- .Label({ text: "First name" }).Input({ value: client._bind_edit(this.first_name) })
- .Label({ text: "Last name" }).Input({ value: client._bind_edit(this.last_name) })
- .Label({ text: "Email" }).Input({
- value: client._bind_edit(this.email),
- valueState: client._bind(this.validation.email_state),
- valueStateText: client._bind(this.validation.email_text)
- })
- .Label({ text: "Active" }).CheckBox({ selected: client._bind_edit(this.active) })
- .Button({ text: "Save", press: client._event("SAVE"), type: "Emphasized" });
+ 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: `xmlns:form`, v: `sap.ui.layout.form` });
+
+ const form = view.ele(`Shell`).ele(`Page`)
+ .a({ n: `title`, v: `Profile` })
+ .ele({ n: `SimpleForm`, ns: `form` })
+ .a({ n: `editable`, b: true })
+ .ele({ n: `content`, ns: `form` });
+
+ const validation = client._bind(this.validation, { path: true });
+
+ form.tag(`Label`).a({ n: `text`, v: `First name` })
+ .tag(`Input`).a({ n: `value`, v: client._bind_edit(this.first_name) })
+ .tag(`Label`).a({ n: `text`, v: `Last name` })
+ .tag(`Input`).a({ n: `value`, v: client._bind_edit(this.last_name) })
+ .tag(`Label`).a({ n: `text`, v: `Email` })
+ .tag(`Input`)
+ .a({ n: `value`, v: client._bind_edit(this.email) })
+ .a({ n: `valueState`, v: `{${validation}/EMAIL_STATE}` })
+ .a({ n: `valueStateText`, v: `{${validation}/EMAIL_TEXT}` })
+ .tag(`Label`).a({ n: `text`, v: `Active` })
+ .tag(`CheckBox`).a({ n: `selected`, v: client._bind_edit(this.active) })
+ .tag(`Button`)
+ .a({ n: `text`, v: `Save` })
+ .a({ n: `press`, v: client._event(`SAVE`) })
+ .a({ n: `type`, v: `Emphasized` });
+
client.view_display(view.stringify());
}
}
diff --git a/docs/guide/ecosystem.md b/docs/guide/ecosystem.md
index 9e75594..1d24ead 100644
--- a/docs/guide/ecosystem.md
+++ b/docs/guide/ecosystem.md
@@ -1,11 +1,14 @@
# The cap2UI5 Ecosystem
-cap2UI5 is not one repository. It is a chain of six, and almost every file you
-can see is generated by the one before it. That matters the moment you want to
-change something or report a bug: editing the obvious place usually means
-editing a build output, and your change disappears on the next sync.
+cap2UI5 is not one repository. It is a chain of six — plus the upstream project
+they all descend from — and almost every file you can see is generated by the
+one before it. That matters the moment you want to change something or report a
+bug: editing the obvious place usually means editing a build output, and your
+change disappears on the next sync.
-## The six repositories
+## The repositories
+
+The first row is the upstream project; the six below it are the cap2UI5 chain.
| Repository | What it is | Hand-written? |
|---|---|---|
diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md
index 03df662..fd8ccbc 100644
--- a/docs/guide/getting-started.md
+++ b/docs/guide/getting-started.md
@@ -65,8 +65,8 @@ Create a new file `my_first_app.js` in `srv/app/`:
```js
// srv/app/my_first_app.js
-const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
-const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
+const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
+const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
class my_first_app extends z2ui5_if_app {
@@ -88,20 +88,26 @@ class my_first_app extends z2ui5_if_app {
}
render(client) {
- const view = z2ui5_cl_xml_view.factory()
- .Shell()
- .Page({ title: "My first cap2UI5 app" })
- .SimpleForm({ editable: true })
- .content()
- .Label({ text: "Your name" })
- .Input({ value: client._bind_edit(this.who) })
- .Label({ text: "Clicks" })
- .Text({ text: client._bind(this.count) })
- .Button({
- text: "Click me",
- press: client._event("CLICK"),
- type: "Emphasized"
- });
+ 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: `xmlns:form`, v: `sap.ui.layout.form` });
+
+ const form = view.ele(`Shell`).ele(`Page`)
+ .a({ n: `title`, v: `My first cap2UI5 app` })
+ .ele({ n: `SimpleForm`, ns: `form` })
+ .a({ n: `editable`, b: true })
+ .ele({ n: `content`, ns: `form` });
+
+ form.tag(`Label`).a({ n: `text`, v: `Your name` })
+ .tag(`Input`).a({ n: `value`, v: client._bind_edit(this.who) })
+ .tag(`Label`).a({ n: `text`, v: `Clicks` })
+ .tag(`Text`).a({ n: `text`, v: client._bind(this.count) })
+ .tag(`Button`)
+ .a({ n: `text`, v: `Click me` })
+ .a({ n: `press`, v: client._event(`CLICK`) })
+ .a({ n: `type`, v: `Emphasized` });
client.view_display(view.stringify());
}
@@ -121,7 +127,7 @@ Open [http://localhost:4004/z2ui5/webapp/index.html?app_start=my_first_app](http
## What you just built
-In about 25 lines of JS you built a **stateful UI5 app** that:
+In one file you built a **stateful UI5 app** that:
- Two-way-binds `who` to an input field (you type, the server receives it)
- Persists `count` across roundtrips — the click counter even survives a browser refresh, because the server stores the app instance in the database
diff --git a/docs/guide/lifecycle.md b/docs/guide/lifecycle.md
index 57349a1..20cfed9 100644
--- a/docs/guide/lifecycle.md
+++ b/docs/guide/lifecycle.md
@@ -177,11 +177,17 @@ class search_form extends z2ui5_if_app {
}
render(client) {
- z2ui5_cl_xml_view.factory()
- .Page({ title: "Search" })
- .Input({ value: client._bind_edit(this.search) })
- .Button({ press: client._event("DO_SEARCH") })
- .Table({ items: client._bind(this.results) });
+ 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` });
+
+ view.ele(`Shell`).ele(`Page`).a({ n: `title`, v: `Search` })
+ .tag(`Input`).a({ n: `value`, v: client._bind_edit(this.search) })
+ .tag(`Button`).a({ n: `press`, v: client._event(`DO_SEARCH`) })
+ .tag(`Table`).a({ n: `items`, v: client._bind(this.results) });
+
+ client.view_display(view.stringify());
}
}
```
@@ -190,8 +196,8 @@ Fields you expose via bindings **must be direct properties** of the app. `_bind_
```js
const path = client._bind_edit(this.deep, { path: true });
-// path = "/XX/deep"
-.Input({ value: `{${path}/path/field}` })
+// path = "/XX/DEEP" ← model paths are uppercased
+page.tag(`Input`).a({ n: `value`, v: `{${path}/PATH/FIELD}` });
```
→ Full explanation in [Data Binding](./data-binding).
diff --git a/docs/guide/popups.md b/docs/guide/popups.md
index 572db23..c0e5730 100644
--- a/docs/guide/popups.md
+++ b/docs/guide/popups.md
@@ -47,32 +47,43 @@ client.message_box_display(
## Popup (dialog)
-A **popup** is a second XML view overlaid as a modal dialog. You build it with `factory_popup()`:
+A **popup** is a second view overlaid as a modal dialog. It is a *fragment*, not a view, so the root element is `core:FragmentDefinition` instead of `mvc:View` — everything else is the ordinary builder:
```js
-const dialog = z2ui5_cl_xml_view.factory_popup()
- .Dialog({
- title: "Edit User",
- afterClose: client._event("CLOSE_DIALOG"),
- contentWidth: "30em",
- });
-
-dialog.content()
- .SimpleForm({ editable: true })
- .content()
- .Label({ text: "Name" })
- .Input({ value: client._bind_edit(this.user_name) })
- .Label({ text: "Role" })
- .Input({ value: client._bind_edit(this.user_role) });
-
-dialog.endButton()
- .Button({ text: "Save", type: "Emphasized", press: client._event("SAVE_USER") });
-dialog.beginButton()
- .Button({ text: "Cancel", press: client._event("CANCEL_DIALOG") });
-
-client.popup_display(dialog.stringify());
+const popup = z2ui5_cl_ui5_view_builder.factory()
+ .ele({ n: `FragmentDefinition`, ns: `core` })
+ .a({ n: `xmlns`, v: `sap.m` })
+ .a({ n: `xmlns:core`, v: `sap.ui.core` })
+ .a({ n: `xmlns:form`, v: `sap.ui.layout.form` });
+
+const dialog = popup.ele(`Dialog`)
+ .a({ n: `title`, v: `Edit User` })
+ .a({ n: `afterClose`, v: client._event(`CLOSE_DIALOG`) })
+ .a({ n: `contentWidth`, v: `30em` });
+
+dialog.ele({ n: `SimpleForm`, ns: `form` })
+ .a({ n: `editable`, b: true })
+ .ele({ n: `content`, ns: `form` })
+ .tag(`Label`).a({ n: `text`, v: `Name` })
+ .tag(`Input`).a({ n: `value`, v: client._bind_edit(this.user_name) })
+ .tag(`Label`).a({ n: `text`, v: `Role` })
+ .tag(`Input`).a({ n: `value`, v: client._bind_edit(this.user_role) });
+
+dialog.ele(`endButton`).tag(`Button`)
+ .a({ n: `text`, v: `Save` })
+ .a({ n: `type`, v: `Emphasized` })
+ .a({ n: `press`, v: client._event(`SAVE_USER`) });
+dialog.ele(`beginButton`).tag(`Button`)
+ .a({ n: `text`, v: `Cancel` })
+ .a({ n: `press`, v: client._event(`CANCEL_DIALOG`) });
+
+client.popup_display(popup.stringify());
```
+::: warning `stringify()` renders the whole tree, always
+`stringify()` starts at the root of the tree, not at the node you call it on — `dialog.stringify()` and `popup.stringify()` produce the same string. Keep the root in a variable and hand *that* to `popup_display`, so the intent is visible.
+:::
+
In subsequent roundtrips:
- `client.popup_close()` → closes the dialog (frontend action)
@@ -87,16 +98,25 @@ Very similar, but anchored to a UI5 control in the main view:
client.popover_display(view.stringify(), "buttonId");
```
-The `OPEN_BY_ID` is the ID of a control in the main view next to which the popover appears. The builder even has a convenience helper for this:
+The second argument is the ID of a control in the main view next to which the popover appears. There is no convenience helper for a "please confirm" popover — build it like any other fragment:
```js
-view._z2ui5().approve_popover({
- placement: "Right",
- text: "Really save?",
- btn_type: "Emphasized",
- btn_txt: "Yes",
- btn_event: client._event("CONFIRM"),
-});
+const popover = z2ui5_cl_ui5_view_builder.factory()
+ .ele({ n: `FragmentDefinition`, ns: `core` })
+ .a({ n: `xmlns`, v: `sap.m` })
+ .a({ n: `xmlns:core`, v: `sap.ui.core` });
+
+popover.ele(`Popover`)
+ .a({ n: `placement`, v: `Right` })
+ .a({ n: `showHeader`, b: false })
+ .tag(`Text`).a({ n: `text`, v: `Really save?` })
+ .a({ n: `class`, v: `sapUiSmallMargin` })
+ .tag(`Button`)
+ .a({ n: `text`, v: `Yes` })
+ .a({ n: `type`, v: `Emphasized` })
+ .a({ n: `press`, v: client._event(`CONFIRM`) });
+
+client.popover_display(popover.stringify(), "saveButtonId");
```
## Nested views
@@ -158,13 +178,25 @@ async main(client) {
}
show_confirm_dialog(client) {
- const view = z2ui5_cl_xml_view.factory_popup();
- view.Dialog({ title: "Confirmation", contentWidth: "20em" })
- .content()
- .Text({ text: "Really delete entry?" });
- view.endButton().Button({ text: "Delete", type: "Reject", press: client._event("DELETE_CONFIRMED") });
- view.beginButton().Button({ text: "Cancel", press: client._event("DELETE_CANCELLED") });
- client.popup_display(view.stringify());
+ const popup = z2ui5_cl_ui5_view_builder.factory()
+ .ele({ n: `FragmentDefinition`, ns: `core` })
+ .a({ n: `xmlns`, v: `sap.m` })
+ .a({ n: `xmlns:core`, v: `sap.ui.core` });
+
+ const dialog = popup.ele(`Dialog`)
+ .a({ n: `title`, v: `Confirmation` })
+ .a({ n: `contentWidth`, v: `20em` });
+
+ dialog.tag(`Text`).a({ n: `text`, v: `Really delete entry?` });
+ dialog.ele(`endButton`).tag(`Button`)
+ .a({ n: `text`, v: `Delete` })
+ .a({ n: `type`, v: `Reject` })
+ .a({ n: `press`, v: client._event(`DELETE_CONFIRMED`) });
+ dialog.ele(`beginButton`).tag(`Button`)
+ .a({ n: `text`, v: `Cancel` })
+ .a({ n: `press`, v: client._event(`DELETE_CANCELLED`) });
+
+ client.popup_display(popup.stringify());
}
```
diff --git a/docs/guide/project-structure.md b/docs/guide/project-structure.md
index c433957..ca54d55 100644
--- a/docs/guide/project-structure.md
+++ b/docs/guide/project-structure.md
@@ -113,34 +113,31 @@ core/ # npm package "abap2UI5"
│ ├── 02/z2ui5_cl_ui5_client.js # the client class (your API)
│ ├── 02/z2ui5_cl_ui5_srv_bind.js # _bind / _bind_edit implementation
│ ├── 02/… # action, model, event services
- │ └── 03/z2ui5_cl_ui5f_index_html.js # bootstrap HTML as a JS module
+ │ ├── 03/z2ui5_cl_ui5f_index_html.js # bootstrap HTML as a JS module
+ │ └── 04/ # the apps the framework ships
+ │ ├── z2ui5_cl_ui5_app_start.js # built-in launcher
+ │ ├── z2ui5_cl_ui5_app_hi_world.js # mini example
+ │ └── z2ui5_cl_ui5_user_exit.js # config hook (theme, CSP, headers)
├── 02/ # public API
│ ├── z2ui5_if_app.js # base class for apps
- │ ├── z2ui5_cl_ui5_http_handler.js # CDS action adapter
- │ ├── z2ui5_cl_xml_view.js # view builder
- │ └── z2ui5_cl_xml_view_cc.js # custom control decorator
- ├── 01/04/ # the apps the framework ships
- │ ├── z2ui5_cl_ui5_app_start.js # built-in launcher
- │ ├── z2ui5_cl_ui5_app_hi_world.js # mini example
- │ └── z2ui5_cl_ui5_user_exit.js # config hook (theme, CSP, headers)
- ├── 99/ # add-ons
- │ └── 02/z2ui5_cl_pop_*.js # popup helpers
+ │ ├── z2ui5_if_client.js # the client contract (constants)
+ │ ├── z2ui5_cl_ui5_http_handler.js # CDS action adapter
+ │ └── z2ui5_cl_ui5_view_builder.js # view builder
├── engine.js # platform-neutral surface (roundtrip, bootstrap, ports)
└── register-apps.js # convenience hook for external app repos
```
-The numbering `00/`, `01/`, `02/` mirrors the abap2UI5 layering (see [Where cap2UI5 comes from](./where-it-comes-from)):
+The numbering `00/`, `01/`, `02/` mirrors the abap2UI5 layering (see [Where cap2UI5 comes from](./where-it-comes-from)) — those three are all there is:
- **`00/`** — pure utilities, no dependencies into the system
-- **`01/`** — core plumbing (persistence, handler, binding engine, HTML bootstrap)
+- **`01/`** — core plumbing (persistence, handler, binding engine, HTML bootstrap) plus the shipped apps in `01/04/`
- **`02/`** — everything app developers **import directly**
-- **`99/`** — add-ons (utility classes, popup helpers)
As an app developer you almost always need exactly two imports, via the package's exports:
```js
-const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
-const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
+const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
+const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
```
(The package is named `abap2UI5` and linked into the project via `"abap2UI5": "file:./core"` — a real, vendored dependency. Requires like `require("abap2UI5/z2ui5_if_app")` resolve through the exports map in `core/package.json`, and external app repos can depend on the same package the same way.)
diff --git a/docs/guide/server-driven-ui.md b/docs/guide/server-driven-ui.md
deleted file mode 100644
index 5806c01..0000000
--- a/docs/guide/server-driven-ui.md
+++ /dev/null
@@ -1,90 +0,0 @@
-# Server-Driven UI, Explained
-
-cap2UI5 is built on an idea that most CAP developers have never worked with directly: **the server decides what the UI looks like**. This page explains that idea from scratch — no abap2UI5 or frontend knowledge required — and shows which gap it closes in the CAP world.
-
-## How a "normal" UI5 app works
-
-In a classic UI5/Fiori app (and in every React/Angular/Vue app, for that matter), the **browser owns the UI**:
-
-1. The browser downloads your app: views, controllers, `manifest.json`, i18n files.
-2. The JavaScript in the browser renders the screens.
-3. Whenever data is needed, the browser calls your CAP service via **OData**.
-4. UI state (which tab is open, what the user typed, which step of the wizard you're on) lives in the browser — in models, controllers, and component state.
-
-This is called a **single-page application (SPA)**. The server is "just" a data supplier. Consequences for you as a CAP developer:
-
-- You maintain **a second project** (the frontend) with its own build, its own `package.json`, its own release cycle.
-- Every piece of data the UI needs must be **exposed as an OData service** — even throwaway state that has nothing to do with your data model.
-- Logic gets **split across two worlds**: validation, visibility rules, and flow control exist once in the backend and once (at least partly) in the frontend.
-
-## What "server-side rendering" means
-
-Server-side rendering (SSR) is the opposite idea: **the server produces the UI**. It's actually the older model — PHP, JSP, and Rails apps have always worked this way: every click sends a request, the server builds the next page, the browser just displays it.
-
-The trade is simple:
-
-| | Client renders (SPA) | Server renders |
-|---|---|---|
-| UI logic lives | in the browser | on the server |
-| Interaction | often local, instant | one request per interaction |
-| Frontend project | yes, full build & tooling | none (or minimal) |
-| State | split browser/server | in one place, on the server |
-| Offline capable | possible | no |
-
-Classical SSR fell out of fashion for business apps because full-page HTML reloads feel clunky. But there is a modern middle ground — and that's where cap2UI5 lives.
-
-## Server-driven UI: the modern middle ground
-
-cap2UI5 (like abap2UI5, its [origin](./where-it-comes-from)) uses a pattern called **server-driven UI**:
-
-- The browser loads a **generic, static UI5 app** exactly once. It's always the same app, no matter what you build — think of it as a "UI5 player".
-- On every interaction, the frontend sends **one POST request** to the server with the event that happened and the values the user changed.
-- The server — your CAP application — runs your app class, builds the **view as XML** plus a **JSON data model**, and sends both back.
-- The UI5 player renders whatever it receives.
-
-```
-Browser CAP server
-┌───────────────────────┐ ┌──────────────────────────────┐
-│ generic UI5 app │ │ your app class (plain JS) │
-│ ("player", static, │──── POST ───▶│ main(client) { │
-│ loaded once) │ event + │ ...build view, set data │
-│ │ changed │ } │
-│ renders XML + model │◀── response ─│ → view XML + JSON model │
-└───────────────────────┘ └──────────────────────────────┘
-```
-
-So it is **not** classical SSR (no HTML pages are re-sent), and **not** an SPA (the browser doesn't own your UI logic). The browser renders; the server decides *what* to render. All your logic — views, events, state, flow — lives in **one place, in one language, in your CAP project**.
-
-::: tip In one sentence
-You write a JavaScript class in `srv/`, and a ready-made UI5 frontend renders whatever that class tells it to — one roundtrip per interaction.
-:::
-
-## The gap cap2UI5 closes
-
-For UIs on top of CAP you had, until now, exactly two options:
-
-1. **Fiori Elements** — you annotate your CDS entities and get generated List Reports and Object Pages. Fantastic for standard CRUD, but rigid: as soon as your UI is a wizard, a dashboard, an admin tool, or "it depends on what the user clicked", you fight the annotations.
-2. **Freestyle UI5** — total freedom, but you pay full price: a separate frontend project, XML views, controllers, `manifest.json`, a second build pipeline, and duplicated state handling.
-
-There was **nothing in between**: no lightweight way to get a free-form UI5 UI out of a few lines of backend code.
-
-In the ABAP world the exact same gap existed — and the open-source project [abap2UI5](https://github.com/abap2UI5/abap2UI5) closed it there, very successfully: UI5 apps written purely in ABAP classes, no frontend artifacts to deploy. **cap2UI5 brings that same solution to CAP**: UI5 apps written purely in JavaScript classes inside your CAP project.
-
-| | Fiori Elements | **cap2UI5** | Freestyle UI5 |
-|---|---|---|---|
-| Effort to first screen | low (if standard) | **minutes** | high |
-| Flexibility | annotation-limited | **free-form logic in JS** | unlimited |
-| Extra frontend project | generated `app/` folder | **none** | full project |
-| Best for | standard CRUD lists | **tools, wizards, admin UIs** | pixel-perfect products |
-
-## What this costs you
-
-Server-driven UI is a trade-off, not magic. Be aware of:
-
-- **Every interaction is a roundtrip.** A button click travels to the server and back. In a LAN or on BTP this is typically 50–200 ms — fine for business apps, wrong for offline apps or 60-fps interactions.
-- **You're inside the UI5 control set.** Everything UI5 ships (tables, forms, charts, dialogs, …) is available; a fully custom design system is not the target.
-- **Read-heavy filtering of huge lists** is better served by OData bindings and Fiori Elements.
-
-For the typical internal tool, prototype, or workflow UI — the everyday work of a CAP team — the trade is heavily in your favor. The [Why cap2UI5?](./why-cap2ui5) page makes that case in detail.
-
-→ Next: [**Where cap2UI5 comes from**](./where-it-comes-from) — the abap2UI5 story, or jump straight to the [**Quickstart**](./getting-started).
diff --git a/docs/guide/vs-abap2ui5.md b/docs/guide/vs-abap2ui5.md
index bde76c1..a422986 100644
--- a/docs/guide/vs-abap2ui5.md
+++ b/docs/guide/vs-abap2ui5.md
@@ -1,14 +1,33 @@
# cap2UI5 vs. abap2UI5
-cap2UI5 is a **JavaScript port** of the [abap2UI5](https://github.com/abap2UI5/abap2UI5) framework. If you know one, you know the other 90%. This page shows the commonalities, the small differences, and why a second implementation exists in the first place. (New to abap2UI5 entirely? Start with [Where cap2UI5 comes from](./where-it-comes-from).)
+cap2UI5 is a **JavaScript port** of the [abap2UI5](https://github.com/abap2UI5/abap2UI5) framework. If you know one, you know the other 90%. This page shows the commonalities, the differences, and why a second implementation exists in the first place. (New to abap2UI5 entirely? Start with [Where cap2UI5 comes from](./where-it-comes-from).)
## Commonalities
- **Identical frontend bundle.** cap2UI5 pulls the `app/webapp/` directory from the abap2UI5 repo via the automated [sync pipeline](./where-it-comes-from#how-the-port-actually-works). That means: same UI5 bundle, same custom controls, same `app/z2ui5/webapp/core/actions/` handlers, same index HTML boot pattern.
- **Identical wire protocol.** `POST /rest/root/z2ui5` with `{ S_FRONT, XX, MODEL }` — the frontend cannot tell whether ABAP or Node.js is responding.
-- **Identical developer API.** Class names, methods, patterns (`check_on_init`, `_bind_edit`, `_event`, `nav_app_call`) are 1:1.
+- **The same API, method for method.** Class names, methods and patterns (`check_on_init`, `_event`, `nav_app_call`, the view builder chain) carry over one to one; an ABAP method call and its JS counterpart differ only in language idiom.
- **Identical custom control set.** `geolocation`, `chartjs`, `file_uploader`, … all run identically on the frontend — the server only has to render the XML correctly.
+::: warning Same shape — but not the same release, and not the same class set
+Two qualifications on "identical", both worth knowing before you copy an upstream sample.
+
+**cap2UI5 carries one pinned framework release, and it is 1.142.0.** The number is in the shipped code: `static version` on `z2ui5_if_app` (`core/srv/z2ui5/02/z2ui5_if_app.js`). Upstream has moved on, and one of the moves changes what an example means:
+
+- **Here, on 1.142.0**, `_bind()` and `_bind_edit()` are two different bindings — one-way and two-way. Every example on this site uses them that way, because that is what the shipped `z2ui5_cl_ui5_srv_bind` does.
+- **Upstream, from 1.143.0 on**, the split is gone: `_bind_edit()` is an alias of `_bind()`, and `custom_mapper_back` / `custom_filter_back` are still accepted but no longer evaluated. See abap2UI5's [deprecations page](https://abap2ui5.github.io/docs/resources/deprecations.html).
+
+**cap2UI5 does not ship upstream's frozen legacy package at all.** abap2UI5 keeps its `src/99` for one reason — abapGit installs a repository, not a folder, so deleting those objects would break existing installations. An npm package has no such installed base, so since 2026-08 nothing from `src/99` enters the port. Concretely, these exist upstream and do **not** exist here:
+
+| upstream (frozen, still shipped) | cap2UI5 |
+|---|---|
+| `z2ui5_cl_xml_view`, `z2ui5_cl_xml_view_cc` | not carried — the only builder is `z2ui5_cl_ui5_view_builder` |
+| the built-in popups `z2ui5_cl_pop_*` | not carried — upstream's own successor is the separate [popups add-on](https://github.com/abap2UI5-addons/popups) |
+| the retired `z2ui5_cl_util*` of `99/01` | not carried (the `z2ui5_cl_util` here is the transpiler runtime, an unrelated class of the same name) |
+
+So an old abap2UI5 sample that still compiles upstream can fail here on the import alone — and that is deliberate, not a gap waiting to be filled.
+:::
+
## Differences
### Language & type system
@@ -45,7 +64,7 @@ abap2UI5 apps are written in SAP GUI / ADT. cap2UI5 apps are written in **VS Cod
## Which should I choose?
-That **doesn't** depend on which framework is "better" — both are 1:1 equivalents. It depends on **which server stack fits your project**:
+That **doesn't** depend on which framework is "better" — they solve the same problem the same way. It depends on **which server stack fits your project**:
- You have an **existing ABAP system** and don't want a second platform → **abap2UI5**.
- You're building a **new cloud application** on BTP / Cloud Foundry / Kyma → **cap2UI5**.
@@ -54,7 +73,7 @@ That **doesn't** depend on which framework is "better" — both are 1:1 equivale
## Code comparison
-ABAP version (`z2ui5_cl_ui5_app_hi_world.clas.abap`):
+ABAP version — the same app on current abap2UI5, built with `z2ui5_cl_ui5_view_builder`:
```abap
CLASS z2ui5_cl_ui5_app_hi_world DEFINITION PUBLIC.
@@ -66,17 +85,20 @@ ENDCLASS.
CLASS z2ui5_cl_ui5_app_hi_world IMPLEMENTATION.
METHOD z2ui5_if_app~main.
IF client->check_on_init( ).
- DATA(view) = z2ui5_cl_xml_view=>factory( ).
- view->shell( )->page( title = 'Hello World'
- )->simple_form( editable = abap_true
- )->content(
- )->title( text = 'Make an input here and send it to the server...'
- )->label( text = 'Name'
- )->input( value = client->_bind_edit( name )
- )->button( text = 'Send'
- press = client->_event( 'BUTTON_POST' ) ).
+ DATA(view) = z2ui5_cl_ui5_view_builder=>factory( ).
+ view->ele( n = `View` ns = `mvc`
+ )->a( n = `xmlns` v = `sap.m`
+ )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc` ).
+
+ DATA(page) = view->ele( `Shell`
+ )->ele( `Page` )->a( n = `title` v = `Hello World` ).
+
+ page->tag( `Input` )->a( n = `value` v = client->_bind( name )
+ )->tag( `Button` )->a( n = `text` v = `Send`
+ )->a( n = `press` v = client->_event( `BUTTON_POST` ) ).
+
client->view_display( view->stringify( ) ).
- ELSEIF client->check_on_event( 'BUTTON_POST' ).
+ ELSEIF client->check_on_event( `BUTTON_POST` ).
client->message_box_display( |Your name is { name }| ).
ENDIF.
ENDMETHOD.
@@ -92,15 +114,18 @@ class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app {
async main(client) {
if (client.check_on_init()) {
- const view = z2ui5_cl_xml_view.factory()
- .Shell()
- .Page({ title: "Hello World" })
- .SimpleForm({ editable: true })
- .content()
- .Title({ text: "Make an input here and send it to the server..." })
- .Label({ text: "Name" })
- .Input({ value: client._bind_edit(this.name) })
- .Button({ text: "Send", press: client._event("BUTTON_POST") });
+ 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` });
+
+ const page = view.ele(`Shell`).ele(`Page`).a({ n: `title`, v: `Hello World` });
+
+ page.tag(`Input`).a({ n: `value`, v: client._bind_edit(this.name) })
+ .tag(`Button`)
+ .a({ n: `text`, v: `Send` })
+ .a({ n: `press`, v: client._event(`BUTTON_POST`) });
+
client.view_display(view.stringify());
} else if (client.check_on_event("BUTTON_POST")) {
@@ -110,7 +135,7 @@ class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app {
}
```
-The structure is **identical**. Only the language idioms differ.
+The structure is the same, method for method. The two places the languages part company are visible above: ABAP names its arguments (`n = … v = …`) where JavaScript passes one object, and the binding call differs — `_bind` upstream, `_bind_edit` here, because on the pinned 1.142.0 that is still the two-way one. Everything else maps line by line, which is exactly what makes machine transpilation of the samples possible.
## How the two stay in sync
@@ -128,7 +153,7 @@ A jest suite gates every sync — only a green build is committed.
Because the wire format and API are compatible, migrating an existing abap2UI5 app to cap2UI5 is mechanical:
-1. Rewrite the ABAP class as a JS class (the mapping is 1:1) — or let the transpiler do a first pass: `npm run transpile -- path/to/z2ui5_cl_my_app.clas.abap --stdout` in a [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) checkout emits JavaScript, marking unsupported statements as `// TODO(abap2js)` comments instead of dropping them
+1. Rewrite the ABAP class as a JS class — method for method, with the two caveats above: a view built on the retired `z2ui5_cl_xml_view` has to be rebuilt on `z2ui5_cl_ui5_view_builder`, and two-way binding is `_bind_edit` on the release cap2UI5 pins. Or let the transpiler do a first pass: `npm run transpile -- path/to/z2ui5_cl_my_app.clas.abap --stdout` in a [builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js) checkout emits JavaScript, marking unsupported statements as `// TODO(abap2js)` comments instead of dropping them
2. Convert data access from OpenSQL to CDS queries
3. Convert external calls from `cl_http_client` to `fetch`/`cds.connect.to`
4. Drop the file into `srv/app/` (or a [registered app folder](./project-structure#srv-app))
diff --git a/docs/guide/what-is-cap2ui5.md b/docs/guide/what-is-cap2ui5.md
index e5e49b4..67fb294 100644
--- a/docs/guide/what-is-cap2ui5.md
+++ b/docs/guide/what-is-cap2ui5.md
@@ -2,7 +2,7 @@
**cap2UI5** lets you build complete SAPUI5 applications **inside your CAP backend (Node.js)** — as plain JavaScript classes. No separate frontend project, no hand-written XML views, no `manifest.json`, no second build pipeline. One class in `srv/` = one app.
-It is the CAP/Node.js twin of [abap2UI5](https://github.com/abap2UI5/abap2UI5), a popular open-source framework from the ABAP world. Never heard of abap2UI5? That's expected — it lives on the other side of the SAP fence. The short version: it lets ABAP developers write UI5 apps purely in ABAP, and it's been very successful at that. cap2UI5 brings the exact same concept to CAP. The full story is on [Where cap2UI5 comes from](./where-it-comes-from).
+It is the CAP/Node.js twin of [abap2UI5](https://github.com/abap2UI5/abap2UI5), a popular open-source framework from the ABAP world. Never heard of abap2UI5? That's expected — it lives on the other side of the SAP fence. The short version: it lets ABAP developers write UI5 apps purely in ABAP, and it's been very successful at that. cap2UI5 brings the exact same concept to CAP. The full story, including how the two stay in sync, is on [Where cap2UI5 comes from](./where-it-comes-from).
## The core idea
@@ -18,28 +18,51 @@ In classical UI5 development you maintain **two worlds in parallel**:
That works — but every small workflow passes through three layers (service → OData → controller → view), and the "small admin UI" you wanted to build in an afternoon starts with an hour of project setup.
-cap2UI5 turns this around: **your CAP backend builds the view** and exchanges state with the frontend automatically. The frontend is a finished, generic UI5 app that you never touch — on every roundtrip it receives view XML plus a JSON model from the server and renders it. This pattern is called **server-driven UI**; if it's new to you, read [Server-Driven UI, Explained](./server-driven-ui) first — it also covers which gap this closes between Fiori Elements and freestyle UI5.
+cap2UI5 turns this around: **your CAP backend builds the view** and exchanges state with the frontend automatically.
+
+## Server-driven UI, in one section {#server-driven-ui}
+
+Most CAP developers have never worked with this pattern directly, so it is worth naming precisely.
+
+In a classic UI5/Fiori app — and in every React/Angular/Vue app — the **browser owns the UI**. It downloads views, controllers, `manifest.json` and i18n files, renders the screens, and calls your CAP service via OData whenever it needs data. UI state (which tab is open, what the user typed, which wizard step you're on) lives in the browser. That is a **single-page application**, and the server is "just" a data supplier.
+
+The opposite idea is **server-side rendering**: every click sends a request, the server builds the next page, the browser displays it. Older than the SPA — PHP, JSP and Rails always worked this way — and it fell out of fashion for business apps because full-page reloads feel clunky.
+
+**Server-driven UI is the middle ground**, and it is what cap2UI5 (like abap2UI5) does:
+
+- The browser loads a **generic, static UI5 app** exactly once. It is always the same app, no matter what you build — think of it as a "UI5 player".
+- On every interaction the frontend sends **one POST request** with the event that happened and the values the user changed.
+- The server — your CAP application — runs your app class, builds the **view as XML** plus a **JSON data model**, and sends both back.
+- The player renders whatever it receives.
```
-┌─────────────────────────────────────────────────────────────┐
-│ Browser ──── generic UI5 frontend (from abap2UI5) ─────── │
-│ ▲ │
-│ │ POST /rest/root/z2ui5 { S_FRONT, XX, MODEL } │
-│ ▼ │
-│ CAP server ─── your app class → main(client) ──────────── │
-│ └─ z2ui5_cl_xml_view.factory().Page().Input()... │
-│ └─ client.view_display(view.stringify()) │
-│ └─ state persisted in CDS entity z2ui5_t_01 │
-└─────────────────────────────────────────────────────────────┘
+Browser CAP server
+┌───────────────────────┐ ┌──────────────────────────────┐
+│ generic UI5 app │ │ your app class (plain JS) │
+│ ("player", static, │──── POST ───▶│ main(client) { │
+│ loaded once) │ event + │ ...build view, set data │
+│ │ changed │ } │
+│ renders XML + model │◀── response ─│ → view XML + JSON model │
+└───────────────────────┘ └──────────────────────────────┘
```
+So it is **not** classical SSR (no HTML pages are re-sent) and **not** an SPA (the browser does not own your UI logic). The browser renders; the server decides *what* to render. Views, events, state and flow live in one place, in one language, in your CAP project.
+
+| | Client renders (SPA) | Server-driven (cap2UI5) |
+|---|---|---|
+| UI logic lives | in the browser | on the server |
+| Interaction | often local, instant | one roundtrip per interaction |
+| Frontend project | yes, full build & tooling | none |
+| State | split browser/server | in one place |
+| Offline capable | possible | no |
+
## What you write
A cap2UI5 app is **a single JavaScript class** extending `z2ui5_if_app`:
```js
-const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
-const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
+const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");
+const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
class my_hello_world extends z2ui5_if_app {
@@ -48,11 +71,17 @@ class my_hello_world extends z2ui5_if_app {
async main(client) {
if (client.check_on_init()) {
// first call: render the view
- const view = z2ui5_cl_xml_view.factory()
- .Shell()
- .Page({ title: "Hello World" })
- .Input({ value: client._bind_edit(this.name) })
- .Button({ text: "Send", press: client._event("BUTTON_POST") });
+ 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` });
+
+ view.ele(`Shell`).ele(`Page`).a({ n: `title`, v: `Hello World` })
+ .tag(`Input`).a({ n: `value`, v: client._bind_edit(this.name) })
+ .tag(`Button`)
+ .a({ n: `text`, v: `Send` })
+ .a({ n: `press`, v: client._event(`BUTTON_POST`) });
+
client.view_display(view.stringify());
} else if (client.check_on_event("BUTTON_POST")) {
@@ -70,13 +99,44 @@ That's the whole app. No `manifest.json`, no `Component.js`, no controller file,
`z2ui5_cl_ui5_view_builder`, `check_on_init`, `_bind_edit` — the naming comes from abap2UI5's ABAP conventions and is kept intentionally, so every abap2UI5 sample and doc maps 1:1 to cap2UI5. It looks unusual in JS at first; you get used to it within an hour.
:::
+## The gap this closes {#the-gap}
+
+For UIs on top of CAP you had, until now, exactly two options:
+
+1. **Fiori Elements** — annotate your CDS entities and get generated List Reports and Object Pages. Excellent for standard CRUD, rigid as soon as your UI is a wizard, a dashboard or "it depends on what the user clicked".
+2. **Freestyle UI5** — total freedom, full price: a separate frontend project, XML views, controllers, `manifest.json`, a second build pipeline, duplicated state handling.
+
+There was **nothing in between** — no lightweight way to get a free-form UI5 UI out of a few lines of backend code. The same gap existed in the ABAP world, and abap2UI5 closed it there. cap2UI5 closes it for CAP.
+
+| | Fiori Elements | **cap2UI5** | Freestyle UI5 |
+|---|---|---|---|
+| Effort to first screen | low (if standard) | **minutes** | high |
+| Flexibility | annotation-limited | **free-form logic in JS** | unlimited |
+| Extra frontend project | generated `app/` folder | **none** | full project |
+| Best for | standard CRUD lists | **tools, wizards, admin UIs** | pixel-perfect products |
+
+→ The honest, detailed comparison is on [cap2UI5 vs. Fiori Elements](./vs-fiori-elements).
+
## What cap2UI5 is _not_
- **Not a UI5 replacement.** It *uses* UI5, in its full breadth — Page, Table, SimpleForm, charts, file upload, camera, geolocation. Only the view *definition* moves to the server.
- **Not a replacement for CAP services.** Your `srv/*.cds` services keep running unchanged. cap2UI5 is one additional REST action alongside them; OData consumers never see it.
-- **Not classical server-side rendering.** The server sends view XML + a JSON delta, not finished HTML pages — see [Server-Driven UI, Explained](./server-driven-ui).
+- **Not classical server-side rendering.** The server sends view XML + a JSON delta, not finished HTML pages.
- **Not a big framework dependency.** It's a pattern plus a vendored library package (`core/`) that travels inside your CAP project.
+## When is cap2UI5 the right choice?
+
+✅ **Internal tools, admin backends, workflow apps** — quickly assembled, one developer is enough, no frontend build setup.
+✅ **Migration and data-maintenance UIs** — you're writing CAP services anyway and need a small UI on top.
+✅ **Prototyping** — from idea to clickable UI in minutes; try it in the [browser playground](./playground) right now.
+✅ **Wizards and state-heavy flows** — the next screen depends on previous inputs? That's an `if` statement here, not an annotation puzzle.
+
+And what it costs, plainly:
+
+❌ **Every interaction is a roundtrip.** A click travels to the server and back — typically 50–200 ms on a LAN or on BTP. Fine for business apps, wrong for offline apps or 60-fps interactions.
+❌ **High-volume read-only lists** with complex OData filtering — Fiori Elements is better there, because the filtering stays in the OData driver.
+❌ **Pixel-perfect custom design systems** — you are inside the UI5 control set.
+
## The moving parts
| Piece | Where | Who touches it? |
@@ -87,15 +147,4 @@ That's the whole app. No `manifest.json`, no `Component.js`, no controller file,
The frontend is **wire-format compatible** with abap2UI5: the browser cannot tell whether ABAP or Node.js answers. Every upstream frontend improvement flows into cap2UI5 automatically via a [sync pipeline](./where-it-comes-from#how-the-port-actually-works).
-## When is cap2UI5 the right choice?
-
-✅ **Internal tools, admin backends, workflow apps** — quickly assembled, one developer is enough, no frontend build setup.
-✅ **Migration and data-maintenance UIs** — you're writing CAP services anyway and need a small UI on top.
-✅ **Prototyping** — from idea to clickable UI in minutes; try it in the [browser playground](./playground) right now.
-✅ **Wizards and state-heavy flows** — the next screen depends on previous inputs? That's an `if` statement here, not an annotation puzzle.
-
-❌ **High-volume read-only lists** with complex OData filtering — Fiori Elements is better there.
-❌ **Offline apps** — cap2UI5 is server-driven, one roundtrip per interaction.
-❌ **Pixel-perfect custom design systems** — the view builder maps the UI5 standard.
-
-→ Continue with [**Why cap2UI5?**](./why-cap2ui5) for the technical case, or jump straight to the [**Quickstart**](./getting-started).
+→ Continue with [**Why cap2UI5?**](./why-cap2ui5) for the case in your own project's terms, or jump straight to the [**Quickstart**](./getting-started).
diff --git a/docs/guide/where-it-comes-from.md b/docs/guide/where-it-comes-from.md
index 38f1d02..74a2bd5 100644
--- a/docs/guide/where-it-comes-from.md
+++ b/docs/guide/where-it-comes-from.md
@@ -6,19 +6,9 @@ If you're a CAP developer, chances are you've never heard of **abap2UI5** — it
[abap2UI5](https://github.com/abap2UI5/abap2UI5) is a popular open-source community project from the SAP/ABAP ecosystem. Its promise: **write complete SAPUI5 apps purely in ABAP classes** — no JavaScript, no XML views to deploy, no separate frontend artifacts, no BSP/UI5 repository uploads. One ABAP class = one app.
-It works with the server-driven UI pattern described in [Server-Driven UI, Explained](./server-driven-ui):
+It works with the [server-driven UI pattern](./what-is-cap2ui5#server-driven-ui), with an ABAP class in place of the JavaScript one: a generic UI5 frontend is served to the browser once, every interaction is one HTTP roundtrip, and the ABAP class builds the view as XML, binds data and handles events.
-- A small, **generic UI5 frontend** is served to the browser once.
-- Every user interaction is one HTTP roundtrip to the ABAP backend.
-- The ABAP class builds the view as XML, binds data, handles events — and the frontend renders it.
-
-The project became successful because it removed an entire deployment and tooling layer for internal tools and utility apps. Over the years it grew a large sample collection ([abap2UI5-samples](https://github.com/abap2UI5/samples)), add-ons, and a community — all documented at [abap2UI5.org](https://www.abap2ui5.org).
-
-## The same gap exists in CAP
-
-The problem abap2UI5 solved in ABAP — *"I just need a small UI and I don't want to maintain a whole frontend project for it"* — exists identically in the CAP world: between annotation-generated Fiori Elements and full freestyle UI5 there was nothing lightweight.
-
-**cap2UI5 is the abap2UI5 concept, ported to CAP/Node.js.** Instead of an ABAP class on NetWeaver, you write a JavaScript class in your CAP project's `srv/` folder. Everything else — the pattern, the API, even the frontend — is the same.
+The project became successful because it removed an entire deployment and tooling layer for internal tools and utility apps — the same gap that exists on the CAP side. Over the years it grew a large sample collection ([abap2UI5-samples](https://github.com/abap2UI5/samples)), add-ons, and a community, all documented at [abap2UI5.org](https://www.abap2ui5.org). **cap2UI5 is that concept ported to CAP/Node.js**: instead of an ABAP class on NetWeaver, a JavaScript class in your CAP project's `srv/` folder.
## How the port actually works
@@ -61,6 +51,7 @@ The transpiler (**abap2js**, built on the open-source ABAP parser [@abaplint/cor
- **The frontend is battle-tested.** You're running the exact UI5 app that thousands of abap2UI5 installations use — every upstream bugfix and new custom control (charts, camera, geolocation, …) flows in automatically.
- **The wire format is identical.** The frontend cannot tell whether ABAP or Node.js is answering. That's why the whole ecosystem of abap2UI5 knowledge, samples, and patterns applies 1:1.
- **The naming is inherited.** `z2ui5_cl_ui5_view_builder`, `check_on_init`, `_bind_edit` — these names come from ABAP conventions (`z` = customer namespace, `cl` = class, `if` = interface). They look unusual in JavaScript, but they keep the two worlds mappable line-by-line: any abap2UI5 sample can be ported (or auto-transpiled) to cap2UI5 mechanically.
+- **Not everything upstream ships comes along.** The port carries one pinned framework release and deliberately leaves upstream's frozen legacy package behind — see [cap2UI5 vs. abap2UI5](./vs-abap2ui5) for what that means when you copy an older sample.
- **Hundreds of ready samples.** The `core/srv/app/samples/` folder ships the transpiled abap2UI5 demo apps (`z2ui5_cl_smp_app_*`) — a huge, browsable cookbook. Try them in the [browser playground](./playground) without installing anything.
- **You still write normal JavaScript.** The sync pipeline is a maintainer concern. As an app developer you just `require` two classes and write a JS class — see the [Quickstart](./getting-started).
diff --git a/docs/guide/why-cap2ui5.md b/docs/guide/why-cap2ui5.md
index 3b0d83e..81dacdb 100644
--- a/docs/guide/why-cap2ui5.md
+++ b/docs/guide/why-cap2ui5.md
@@ -1,6 +1,6 @@
# Why cap2UI5?
-This page is aimed at **CAP developers** who need to deliver UI5 apps but are tired of the tooling overhead, duplicated data modeling and XML maintenance. It assumes you've read [Server-Driven UI, Explained](./server-driven-ui) — the short version: between annotation-driven Fiori Elements and full freestyle UI5 projects there was no lightweight middle ground in the CAP world, and cap2UI5 fills exactly that gap.
+[What is cap2UI5?](./what-is-cap2ui5) explains the pattern and the gap it closes. This page is the concrete case: what changes **in your project** when a UI stops being a second project — for CAP developers who are tired of the tooling overhead, the duplicated data modeling and the XML maintenance.
## The problem in the classical world
@@ -87,10 +87,10 @@ After every roundtrip the entire instance is **persisted automatically in the CD
This is the core pattern that makes cap2UI5 (and abap2UI5) lightweight code in the first place:
```js
-.Input({ value: client._bind_edit(this.name) })
+form.tag(`Input`).a({ n: `value`, v: client._bind_edit(this.name) });
```
-`_bind_edit(this.name)` looks at your app instance to find **which property corresponds to the passed value** and returns the path as a UI5 binding expression `{/XX/name}`. When the user types, the value flows back through the delta into `this.name`. No manual mapping, no property strings, no sync code.
+`_bind_edit(this.name)` looks at your app instance to find **which property corresponds to the passed value** and returns the path as a UI5 binding expression `{/XX/NAME}`. When the user types, the value flows back through the delta into `this.name`. No manual mapping, no property strings, no sync code.
→ Details under [Data Binding](./data-binding).
@@ -137,11 +137,7 @@ The UI5 bundle is loaded once. After that every roundtrip returns only **a bit o
## Where it gets unfair
-cap2UI5 doesn't solve every problem. Specifically:
-
-- **Offline scenarios**: every interaction is a roundtrip. If you need to be offline-capable, write Fiori Elements or a classical UI5 setup.
-- **Pixel designs outside UI5 standard**: the view builder knows `sap.m`, `sap.ui.layout`, `sap.tnt`, plus the z2ui5 custom controls. You can include your own foreign JS libraries — but with significantly more work.
-- **Read-heavy lists** with live search filter over millions of rows: every filter change is a server roundtrip — that doesn't scale as well as OData bindings, which the frontend driver filters locally in the JSONModel.
+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#3-persistence-caveat)) 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 b5880a6..f538829 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -27,7 +27,7 @@ features:
details: The proven abap2UI5 concept, ported to CAP — same frontend, same protocol, kept in sync automatically by an ABAP→JS transpiler pipeline.
icon: 🔗
- title: Automatic data binding
- details: client._bind_edit(this.field) finds the property by reference equality on the app instance. Two-way binding without model boilerplate.
+ details: client._bind(this.field) finds the property by reference equality on the app instance — no model boilerplate. On the pinned 1.142.0 core, _bind_edit is the two-way variant.
icon: 🔄
- title: Native CAP integration
details: The roundtrip runs as a CDS REST action. CAP services, OData connections, auth, destinations — everything remains available.
diff --git a/docs/public/.gitkeep b/docs/public/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md
index 4c7c518..b449443 100644
--- a/docs/reference/architecture.md
+++ b/docs/reference/architecture.md
@@ -170,12 +170,12 @@ The `core/srv/z2ui5/` library mirrors **abap2UI5's layered model**:
```
00 — Pure utilities (no framework dependencies)
+├─ 00/cl_abap_* ABAP runtime shims the transpiled code needs
├─ 01/z2ui5_cl_ajson_* JSON tree (the ajson port)
├─ 02/z2ui5_cl_srt_* Serialization helpers
├─ 03/z2ui5_cl_util RTTI, class lookup, URL builder
├─ 03/z2ui5_cl_util_http Request/response facade
-├─ 03/01/z2ui5_cl_util_db|_ext DB + platform-specific extras
-└─ 03/02/z2ui5_cl_util_api Context, conversions, UUIDs
+└─ 03/02/z2ui5_cl_util_api* Context, conversions, UUIDs
01 — Core
├─ 01/z2ui5_cl_ui5_srv_draft Serialize / deserialize / DB
@@ -189,22 +189,21 @@ The `core/srv/z2ui5/` library mirrors **abap2UI5's layered model**:
├─ 02/z2ui5_if_ui5_types internal type containers
└─ 03/z2ui5_cl_ui5f_index_html bootstrap HTML as a JS module
-02 — Public API (app developer imports)
-├─ z2ui5_if_app Base class for your apps
-├─ z2ui5_cl_ui5_http_handler CDS action adapter
-├─ z2ui5_cl_xml_view View Builder
-└─ z2ui5_cl_xml_view_cc Custom control decorator
-
01/04 — The apps the framework ships
├─ z2ui5_cl_ui5_app_start Built-in launcher
├─ z2ui5_cl_ui5_app_hi_world Mini example
+├─ z2ui5_cl_ui5_app_select Value-help app
+├─ z2ui5_cl_ui5_app_error Error view
└─ z2ui5_cl_ui5_user_exit Config hook (theme, CSP, security headers)
-99 — Add-ons
-└─ 02/z2ui5_cl_pop_* Pop helpers
+02 — Public API (app developer imports)
+├─ z2ui5_if_app Base class for your apps
+├─ z2ui5_if_client The client contract (cs_event / cs_view constants)
+├─ z2ui5_cl_ui5_http_handler CDS action adapter
+└─ z2ui5_cl_ui5_view_builder View Builder
```
-The layering is **no accident** — it's the abap2UI5 convention, ported to JS. If you read into one of these files, you'll find the same layout in the abap2UI5 repo.
+Those three numbers are the whole tree: `core/srv/z2ui5/` has `00/`, `01/` and `02/` and nothing else. The layering is **no accident** — it's the abap2UI5 convention, ported to JS. If you read into one of these files, you'll find the same layout in the abap2UI5 repo.
## Wire-format compatibility
diff --git a/docs/reference/protocol.md b/docs/reference/protocol.md
index 6717984..cb74978 100644
--- a/docs/reference/protocol.md
+++ b/docs/reference/protocol.md
@@ -124,7 +124,7 @@ One-way entries land directly under `MODEL`; two-way entries under `MODEL.XX`. T
**Request:**
```json
{ "value": {
- "S_FRONT": { "ID": "", "EVENT": "", "ORIGIN": "...", "PATHNAME": "/rest/root/z2ui5", "SEARCH": "?app_start=hello_world" },
+ "S_FRONT": { "ID": "", "EVENT": "", "ORIGIN": "...", "PATHNAME": "/rest/root/z2ui5", "SEARCH": "?app_start=z2ui5_cl_ui5_app_hi_world" },
"XX": {},
"MODEL": {}
}}
@@ -134,13 +134,13 @@ One-way entries land directly under `MODEL`; two-way entries under `MODEL.XX`. T
```json
{
"S_FRONT": {
- "APP": "hello_world",
+ "APP": "z2ui5_cl_ui5_app_hi_world",
"ID": "abc-123",
"PARAMS": {
- "S_VIEW": { "XML": "......" }
+ "S_VIEW": { "XML": "......" }
}
},
- "MODEL": { "XX": { "name": "" } }
+ "MODEL": { "XX": { "NAME": "" } }
}
```
@@ -150,7 +150,7 @@ One-way entries land directly under `MODEL`; two-way entries under `MODEL.XX`. T
```json
{ "value": {
"S_FRONT": { "ID": "abc-123", "EVENT": "BUTTON_POST", "T_EVENT_ARG": [] },
- "XX": { "name": "Alice" },
+ "XX": { "NAME": "Alice" },
"MODEL": {}
}}
```
@@ -159,13 +159,13 @@ One-way entries land directly under `MODEL`; two-way entries under `MODEL.XX`. T
```json
{
"S_FRONT": {
- "APP": "hello_world",
+ "APP": "z2ui5_cl_ui5_app_hi_world",
"ID": "def-456",
"PARAMS": {
"S_MSG_BOX": { "TEXT": "Your name is Alice", "TYPE": "information" }
}
},
- "MODEL": { "XX": { "name": "Alice" } }
+ "MODEL": { "XX": { "NAME": "Alice" } }
}
```
diff --git a/package.json b/package.json
index 2e43ed1..015aeda 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,18 @@
"private": true,
"type": "module",
"description": "Documentation for the cap2UI5 framework",
+ "homepage": "https://cap2ui5.github.io/docs/",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/cap2UI5/docs.git"
+ },
+ "bugs": {
+ "url": "https://github.com/cap2UI5/docs/issues"
+ },
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
"scripts": {
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
diff --git a/scripts/verify-refs.mjs b/scripts/verify-refs.mjs
index 679bc32..2296368 100644
--- a/scripts/verify-refs.mjs
+++ b/scripts/verify-refs.mjs
@@ -23,6 +23,15 @@
* exist in a cap2UI5 checkout
* 2. every `?app_start=` names a class that actually resolves
* 3. every z2ui5 class named in backticks exists somewhere in the app
+ * 4. every `require("abap2UI5/…")` in a FENCED CODE BLOCK resolves through
+ * the exports map of core/package.json, and onto a file that exists
+ *
+ * Check 4 exists because the first three did not see the largest defect this
+ * site ever had. Fenced blocks were skipped wholesale as "examples, not
+ * claims" — but an example is the one claim every reader copies, and thirteen
+ * pages went on teaching `z2ui5_cl_xml_view` for months after the class was
+ * gone. A retired API is not visible in prose; it is visible in the import
+ * line above the example, and that line is a claim about the package.
*
* Usage:
* CAP2UI5_DIR=/path/to/cap2UI5 node scripts/verify-refs.mjs
@@ -68,8 +77,46 @@ for (const f of files) {
if (b.endsWith(".js")) classes.set(b.slice(0, -3).toLowerCase(), f);
}
+// ---- the core package's exports map ---------------------------------------
+// `require("abap2UI5/x")` does not resolve to a path, it resolves through the
+// "exports" block of core/package.json — so a subpath that is not in that map
+// is a broken import even when a file of that name exists somewhere in the
+// tree, and a subpath that IS in the map still has to land on a real file.
+const CORE_PKG = path.join(APP, "core", "package.json");
+const EXPORTS = fs.existsSync(CORE_PKG)
+ ? JSON.parse(fs.readFileSync(CORE_PKG, "utf8")).exports || {}
+ : null;
+
+/**
+ * Resolve `abap2UI5/` (or bare `abap2UI5`) the way node does:
+ * an exact key wins, otherwise the pattern key with the longest prefix.
+ * Returns the repo-relative path it lands on, or null when nothing matches.
+ */
+function resolveExport(subpath) {
+ if (!EXPORTS) return null;
+ const key = subpath ? `./${subpath}` : `.`;
+ const inApp = (target) => `core/${target.replace(/^\.\//, "")}`;
+
+ if (typeof EXPORTS[key] === "string") return inApp(EXPORTS[key]);
+
+ let best = null;
+ for (const [k, target] of Object.entries(EXPORTS)) {
+ const star = k.indexOf("*");
+ if (star === -1 || typeof target !== "string") continue;
+ const pre = k.slice(0, star);
+ const post = k.slice(star + 1);
+ if (!key.startsWith(pre) || !key.endsWith(post)) continue;
+ if (key.length < pre.length + post.length) continue;
+ if (best && pre.length <= best.pre.length) continue;
+ best = { pre, post, target, star: key.slice(pre.length, key.length - post.length) };
+ }
+ if (!best) return null;
+ return inApp(best.target.replace(`*`, best.star));
+}
+
if (LIST) {
console.log(`${files.size} paths, ${classes.size} classes in ${APP}`);
+ console.log(`exports map: ${EXPORTS ? Object.keys(EXPORTS).length + " subpaths" : "NOT FOUND"}`);
const samples = [...classes.keys()].filter((c) => c.startsWith("z2ui5_cl_smp_")).sort();
console.log(`samples (${samples.length}): ${samples.join(", ")}`);
process.exit(0);
@@ -96,7 +143,14 @@ const add = (file, line, msg) =>
const PATH_ROOTS = ["core/", "srv/", "db/", "app/", "test/"];
const PATH_RE = /`([A-Za-z0-9_@./-]+\/[A-Za-z0-9_@./-]+)`/g;
const APP_START_RE = /app_start=([a-z0-9_]+)/gi;
-const CLASS_RE = /`(z2ui5_(?:cl|if|cx)_[a-z0-9_]+)`/gi;
+// A backticked span that STARTS with a z2ui5 class name. It deliberately does
+// not require the closing backtick to follow the identifier: the docs write
+// `z2ui5_cl_xml_view.js`, `z2ui5_cl_util.register_app_dir(dir)` and
+// `z2ui5_cl_xml_view=>factory( )`, and demanding a bare identifier meant every
+// one of those mentions was invisible to this checker.
+const CLASS_RE = /`(z2ui5_(?:cl|if|cx)_[a-z0-9_]+)(?![a-z0-9_])/gi;
+// require("abap2UI5"), require("abap2UI5/z2ui5_if_app"), … in a code fence.
+const REQUIRE_RE = /require\(\s*["'`]abap2UI5(?:\/([^"'`]+))?["'`]\s*\)/g;
// Tokens the docs use that are not claims about this repository — other
// repos' paths, files the reader creates, placeholder class names. Each is
@@ -129,14 +183,36 @@ for (const file of markdownFiles(DOCS)) {
const text = fs.readFileSync(file, "utf8");
const lines = text.split("\n");
- // Fenced code blocks are examples, not claims about the repository —
- // they legitimately show paths a reader will create.
+ // Fenced code blocks show paths a reader will create, so the path check
+ // stays out of them — but the imports and the `?app_start=` URLs in an
+ // example are claims about the package like any other, and are checked.
let inFence = false;
lines.forEach((line, i) => {
if (/^\s*```/.test(line)) { inFence = !inFence; return; }
- if (inFence) return;
const n = i + 1;
+ if (inFence) {
+ for (const m of line.matchAll(REQUIRE_RE)) {
+ const sub = m[1] || ``;
+ const spec = `abap2UI5${sub ? `/${sub}` : ``}`;
+ if (/[*…]/.test(sub)) continue; // a shape, not an import
+ if (IGNORE.has(spec.toLowerCase())) continue;
+ if (!EXPORTS) continue; // no core package to check against
+ const target = resolveExport(sub);
+ if (!target) {
+ add(file, n, `require("${spec}") has no match in the core exports map`);
+ } else if (!files.has(target)) {
+ add(file, n, `require("${spec}") resolves to ${target}, which does not exist`);
+ }
+ }
+ for (const m of line.matchAll(APP_START_RE)) {
+ const cls = m[1].toLowerCase();
+ if (classes.has(cls) || IGNORE.has(cls)) continue;
+ add(file, n, `?app_start names a class that does not exist: ${m[1]}`);
+ }
+ return;
+ }
+
for (const m of line.matchAll(PATH_RE)) {
const p = m[1].replace(/^\.\//, "").replace(/\/$/, "");
if (!PATH_ROOTS.some((r) => (p + "/").startsWith(r))) continue;
@@ -155,6 +231,7 @@ for (const file of markdownFiles(DOCS)) {
for (const m of line.matchAll(CLASS_RE)) {
const cls = m[1].toLowerCase();
+ if (line[m.index + m[0].length] === "*") continue; // glob: a family, not a class
if (classes.has(cls) || IGNORE.has(cls)) continue;
// Interfaces are not always separate files, and abstract/ABAP-only
// names appear when contrasting with abap2UI5 — only flag cl_ classes,