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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ jobs:
# 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.
# pass this job exists to prevent. Hence `check:ci` below, which passes
# --require-checkout: if this step is ever removed, renamed or fails to
# land the tree, the job goes red instead of quietly checking nothing.
- name: Check out cap2UI5 (reference target)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -49,8 +51,10 @@ jobs:
- 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
# reported even when the site would have built fine around it. Same two
# steps as the `npm run check` AGENTS.md tells a contributor to run —
# the :ci variant only refuses to skip the reference check.
- name: npm run check:ci
run: npm run check:ci
env:
CAP2UI5_DIR: .cap2ui5-ref
20 changes: 19 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,25 @@ VitePress build. It is also what CI runs, on every pull request

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.
proves only that the site builds. That leniency is right on a laptop and wrong
in CI, which does check cap2UI5 out — so CI runs `npm run check:ci`, the same
two steps with `verify-refs --require-checkout`, and a missing checkout is a
failure there rather than a silent pass.

## Generated pages

`docs/guide/samples.md` is **generated** — do not edit it by hand. It comes
from `scripts/gen-samples.mjs`, which reads the sample gallery's own catalogue
table out of `z2ui5_cl_smp_app_000` in a cap2UI5 checkout:

```
CAP2UI5_DIR=/path/to/cap2UI5 npm run gen:samples # rewrite the page
node scripts/gen-samples.mjs --check # fail if it is out of date
```

The result is committed like any other page, so the docs build without a
checkout and a reviewer sees the diff a sync causes. Regenerate it when the
sample set changes.

Exceptions — placeholder class names, paths in other repos — go in
`docs/.verify-refs-ignore`, **with a reason**. An unexplained entry there is
Expand Down
460 changes: 460 additions & 0 deletions ROADMAP.md

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions docs/.vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export default defineConfig({
cleanUrls: true,
lastUpdated: true,

// Absolute URLs in sitemap.xml, same host as SITE_URL above — a search
// engine reads the file from the deployed origin, so a relative base is
// not enough to name a page.
sitemap: {
hostname: `${SITE_URL}/`
},

ignoreDeadLinks: [
/^https?:\/\/localhost/,
/\/LICENSE$/
Expand Down Expand Up @@ -76,7 +83,8 @@ export default defineConfig({
{ text: 'Why cap2UI5?', link: '/guide/why-cap2ui5' },
{ text: 'Try It in the Browser', link: '/guide/playground' },
{ text: 'Quickstart', link: '/guide/getting-started' },
{ text: 'Project Structure', link: '/guide/project-structure' }
{ text: 'Project Structure', link: '/guide/project-structure' },
{ text: 'Sample Catalogue', link: '/guide/samples' }
]
},
{
Expand All @@ -95,21 +103,24 @@ export default defineConfig({
text: 'Working With It',
items: [
{ text: 'Developer Tools', link: '/guide/devtools' },
{ text: 'The User Exit', link: '/guide/user-exit' },
{ text: 'Troubleshooting', link: '/guide/troubleshooting' }
]
},
{
text: 'Comparison',
items: [
{ text: 'cap2UI5 vs. Fiori Elements', link: '/guide/vs-fiori-elements' },
{ text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' }
{ text: 'cap2UI5 vs. abap2UI5', link: '/guide/vs-abap2ui5' },
{ text: 'Migrating from abap2UI5', link: '/guide/migration-from-abap2ui5' }
]
},
{
text: 'Background',
items: [
{ text: 'Where cap2UI5 Comes From', link: '/guide/where-it-comes-from' },
{ text: 'The Ecosystem', link: '/guide/ecosystem' }
{ text: 'The Ecosystem', link: '/guide/ecosystem' },
{ text: 'Roadmap', link: '/guide/roadmap' }
]
}
],
Expand Down
3 changes: 2 additions & 1 deletion docs/api/view-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Views are built in JavaScript and rendered to UI5 XML. There is one builder:
`z2ui5_cl_ui5_view_builder`. It is generic — it knows XML elements and
attributes, not the UI5 control catalogue — which is why the transpiler
targets it and why all 106 bundled samples are written against it.
targets it and why every one of the
[bundled samples](../guide/samples) is written against it.

Source: [`core/srv/z2ui5/02/z2ui5_cl_ui5_view_builder.js`](https://github.com/cap2UI5/cap2UI5/blob/main/core/srv/z2ui5/02/z2ui5_cl_ui5_view_builder.js).

Expand Down
175 changes: 175 additions & 0 deletions docs/guide/migration-from-abap2ui5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# Migrating from abap2UI5

An abap2UI5 app and a cap2UI5 app are the same app in two languages. The wire
protocol is identical, the frontend is byte-identical, and the API is the same
method for method — so migration is mechanical rather than a redesign. What
changes is the language, the data access, and the outbound calls.

This page is the per-construct mapping. For the wider comparison — why a
second implementation exists, which one to pick — see
[cap2UI5 vs. abap2UI5](./vs-abap2ui5).

## The five steps

1. **Rewrite the ABAP class as a JS class**, method for method. Or let the
transpiler do a first pass: in a
[builder-abap2UI5-js](https://github.com/cap2UI5/builder-abap2UI5-js)
checkout, `npm run transpile -- path/to/z2ui5_cl_my_app.clas.abap --stdout`
emits JavaScript and marks what it could not translate as
`// TODO(abap2js)` comments instead of dropping it.
2. **Rebuild the view** if it was built on the retired `z2ui5_cl_xml_view` —
the one builder here is `z2ui5_cl_ui5_view_builder`.
3. **Convert data access** from OpenSQL to CDS queries.
4. **Convert outbound calls** from `cl_http_client` to `fetch` or
`cds.connect.to`.
5. **Drop the file into** [`srv/app/`](./project-structure#srv-app) and run it.
The same static frontend renders it without changes.

## Construct by construct

### The app class

| abap2UI5 | cap2UI5 |
|---|---|
| `CLASS … DEFINITION PUBLIC` / `INTERFACES z2ui5_if_app` | `class … extends z2ui5_if_app` |
| `METHOD z2ui5_if_app~main` | `async main(client)` |
| `DATA name TYPE string` (class attribute) | `name = ""` (class field) |
| `me->name` | `this.name` |
| the class name is the ADT/CCDIR object name | the class name **must** match the file name — the RTTI lookup is name-based |

App state is the instance's own fields in both worlds: whatever you assign to
`this` survives the roundtrip, because the framework serialises the instance
into the draft. See [App Lifecycle](./lifecycle).

### Views

| abap2UI5 | cap2UI5 |
|---|---|
| `z2ui5_cl_ui5_view_builder=>factory( )` | `z2ui5_cl_ui5_view_builder.factory()` |
| `z2ui5_cl_xml_view` (frozen upstream) | **not carried** — rebuild on `z2ui5_cl_ui5_view_builder` |
| `z2ui5_cl_xml_view_cc` (custom-control decorator) | **not carried** — the `z2ui5.cc` namespace is declared on the view like any other |
| the built-in popups `z2ui5_cl_pop_*` | **not carried** — see [Popups & Toasts](./popups) |
| `view->ele( n = 'View' ns = 'mvc' )` — named arguments | `view.ele({ n: "View", ns: "mvc" })` — one object |
| `)->a( n = 'title' v = 'Hello' )` chained with `->` | `.a({ n: "title", v: "Hello" })` chained with `.` |
| `view->stringify( )` | `view.stringify()` |

Named arguments are the one place every line changes shape. ABAP names each
argument (`n = … v = …`); JavaScript passes a single object literal with the
same keys. Positional shorthand survives where ABAP had it: `view->ele( 'Page' )`
becomes `view.ele("Page")`. The methods themselves —
[`ele`, `tag`, `a`, `end`, `stringify`](../api/view-builder#methods) — are
unchanged.

The absent classes are absent on purpose, not by omission: cap2UI5 does not
carry abap2UI5's frozen `src/99` package at all
([why](./vs-abap2ui5#commonalities)). An old sample that still compiles
upstream can therefore fail here on the import alone.

### Binding

| abap2UI5 | cap2UI5 |
|---|---|
| `client->_bind( name )` | `client._bind(this.name)` |
| `client->_bind_edit( name )` | `client._bind_edit(this.name)` |
| `client->_event( 'BUTTON_POST' )` | `client._event("BUTTON_POST")` |

::: warning `_bind` vs `_bind_edit` — check which release your source targets
cap2UI5 pins abap2UI5 **1.142.0** (`static version` on `z2ui5_if_app`, in
`core/srv/z2ui5/02/z2ui5_if_app.js`). On that release the two calls are two
different bindings:

- `_bind()` is **one-way** — the value is rendered, and changes in the browser
are not read back.
- `_bind_edit()` is **two-way** — the value is rendered *and* the frontend
writes it back into the attribute on the next roundtrip. An `Input` whose
value you intend to read needs this one.

Upstream merged the two in **1.143.0**: from there on `_bind_edit()` is an
alias of `_bind()`, and everything is two-way. So an app written against
1.143.0 or later that relies on `_bind()` writing back **will silently not
write back here** — the field simply stays at its old value. Migrating such an
app means changing those calls to `_bind_edit()`.

Coming the other way, from an older app: `_bind_edit()` keeps working on both
releases, which makes it the safe choice while the two releases coexist.
:::

In both languages the binding call receives the attribute and the framework
works out *which* attribute that was. In JavaScript it does so by value
identity, so pass `this.name` itself — a copy, a literal or an expression can
resolve to a different attribute holding the same value. See
[Data Binding](./data-binding#reference-equality) for what to do when two
fields legitimately share a value.

### Data structures

| abap2UI5 | cap2UI5 |
|---|---|
| `TYPES: BEGIN OF ty_s_row … END OF ty_s_row` | no declaration — a plain object `{ … }` |
| `DATA s_row TYPE ty_s_row` | `s_row = { carrid: "", connid: "" }` |
| `DATA t_tab TYPE STANDARD TABLE OF ty_s_row` | `t_tab = []` |
| `APPEND s_row TO t_tab` | `t_tab.push(s_row)` |
| `READ TABLE t_tab INTO s_row INDEX 1` | `s_row = t_tab[0]` |
| `LOOP AT t_tab INTO s_row` | `for (const s_row of t_tab)` |
| `CLEAR` / `IS INITIAL` | `= ""` / `= []` / a falsy check |
| field names are case-insensitive, upper-cased in the model | **object keys are case-sensitive** — keep them lower-case and consistent |

The last row is the one that bites. ABAP does not care whether you wrote
`CARRID` or `carrid`; JavaScript does, and a binding path that disagrees with
the object key by one character renders empty rather than failing.

### Data access

| abap2UI5 | cap2UI5 |
|---|---|
| `SELECT * FROM scarr INTO TABLE @DATA(t)` | `const t = await SELECT.from("my.Carriers")` |
| `SELECT SINGLE … WHERE id = @lv_id` | `await SELECT.one.from("my.Carriers").where({ id })` |
| `SELECT … UP TO 100 ROWS` | `.limit(100)` |
| `INSERT`/`UPDATE`/`DELETE` | `INSERT.into(…)`, `UPDATE(…)`, `DELETE.from(…)` |
| `sy-subrc` after the statement | an empty array / `undefined`, or a thrown error |
| implicit client handling, authorisation checks | CAP's `@requires` / `@restrict` on the service |

CDS queries are **asynchronous**. That is the one structural change migration
forces: a method that reads data becomes `async`, and every caller up to
`main` has to `await` it. `main` is already `async` in cap2UI5, which is why
the chain terminates cleanly.

### Outbound calls

| abap2UI5 | cap2UI5 |
|---|---|
| `cl_http_client=>create_by_url( … )` | `await fetch(url, { … })` |
| `client->send( )` / `client->receive( )` | the awaited `fetch` promise |
| `lo_response->get_cdata( )` | `await res.text()` |
| `/ui2/cl_json=>deserialize( )` | `await res.json()` |
| RFC / service consumer / destination | `await cds.connect.to("service")` |
| SM59 destination | a CAP destination or a plain URL |

See [External OData Call](../examples/external-odata) for a worked example.

### Everything else

| abap2UI5 | cap2UI5 |
|---|---|
| `sy-uname` | the identity provider — `engine.set_identity(…)`, see [Configuration](../reference/configuration#identity) |
| `sy-datum` / `sy-uzeit` | `new Date()` |
| `MESSAGE`/`cx_root` exceptions | `throw new Error(…)` / `try…catch` |
| `z2ui5_cl_ui5_user_exit` (config exit) | the same class, same two hooks — see [The User Exit](./user-exit) |
| DB table `Z2UI5_T_01` | CDS entity `cap2ui5.z2ui5_t_01`, see [Database Model](../reference/database) |
| transport / abapGit pull | `npm install`, `cds build`, `cf deploy` |

## What does not need migrating

- **The frontend.** cap2UI5 serves the same `app/z2ui5/webapp/` UI5 bundle
abap2UI5 does, custom controls included. Nothing you built on the frontend
side changes.
- **The wire protocol.** `POST /rest/root/z2ui5` with the same
`{ S_FRONT, XX, MODEL }` payload. A frontend cannot tell the two backends
apart, which is why the same app can be deployed in both worlds during a
transition.
- **The mental model.** Roundtrips, the draft chain, `check_on_init` /
`check_on_event` / `check_on_navigated`, `nav_app_call` — all identical.

→ Next: the [**Sample Catalogue**](./samples) — the whole abap2UI5 demo
collection, already transpiled, is the largest set of before/after pairs
available.
14 changes: 8 additions & 6 deletions docs/guide/playground.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_ui5_app_hi_world
https://cap2ui5.github.io/web-cap2UI5-build/?app_start=z2ui5_cl_smp_app_000
```

The `z2ui5_cl_smp_app_*` samples are the transpiled abap2UI5 demo collection — a browsable cookbook of tables, forms, popups, charts, and more.
The `z2ui5_cl_smp_app_*` samples are the transpiled abap2UI5 demo collection — a browsable cookbook of tables, forms, popups, charts, and more. The [**Sample Catalogue**](./samples) lists every one of them with a link straight into the playground.

## Wait — didn't you say the backend renders the view?

Yes, and that's exactly what makes this playground fun: cap2UI5's backend is **plain JavaScript with no hard CAP dependency in the hot path**. So the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) build bundles the entire backend — framework core plus all sample apps — into a single ~1.2 MB JS file and loads it **into the browser tab**:
Yes, and that's exactly what makes this playground fun: cap2UI5's backend is **plain JavaScript with no hard CAP dependency in the hot path**. So the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) build bundles the entire backend — framework core plus all sample apps — into a single ~520 KB JS file (~115 KB gzipped, which is what a visitor actually downloads) and loads it **into the browser tab**:

```
Browser tab
Expand All @@ -37,21 +37,23 @@ Browser tab

The frontend doesn't know the difference — it POSTs to the same endpoint and gets the same responses. Every roundtrip that would normally hit your CAP server is answered inside the tab.

For comparison: abap2UI5 has the same kind of twin ([abap2UI5-web](https://github.com/abap2UI5/abap2UI5-web)), but it has to ship an ABAP runtime and a WASM database to the browser (~12 MB). Because cap2UI5's backend is already JavaScript, the bundle here is roughly a tenth of that.
For comparison: abap2UI5 has the same kind of twin ([abap2UI5-web](https://github.com/abap2UI5/abap2UI5-web)), but it has to ship an ABAP runtime and a WASM database to the browser (~12 MB). Because cap2UI5's backend is already JavaScript, the whole site here — bundle, webapp, everything — is about 1.1 MB, roughly a tenth of that; the backend bundle alone is a twentieth.

## What it's good for

- **Kicking the tires** before you install anything.
- **Browsing the sample apps** as a live catalog next to their source in [`core/srv/app/samples/`](https://github.com/cap2UI5/cap2UI5/tree/main/core/srv/app/samples).
- **Browsing the sample apps** as a live catalog next to their source in [`core/srv/app/samples/`](https://github.com/cap2UI5/cap2UI5/tree/main/core/srv/app/samples) — the [Sample Catalogue](./samples) is the index.
- **Sharing a demo link** with colleagues.

## What it's *not*
## What it's *not* {#what-its-not}

- **Not a production topology.** cap2UI5's security model — UI logic and state stay on the server — obviously doesn't apply when the "server" is shipped to the client. It's a demo artifact.
- **Sessions live in the tab.** Reload = fresh state. On a real CAP server, drafts persist in the database.
- **Server-only features are off.** Samples that call external OData services (e.g. Northwind) or need real CAP services/destinations won't work here.
- **Internet still required** — UI5 itself loads from the SAP CDN.

The site is rebuilt automatically from the latest cap2UI5 sources (weekly and on demand) by the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) repository and published to [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build).
The site is rebuilt automatically from the latest cap2UI5 sources by the [builder-cap2UI5-web](https://github.com/cap2UI5/builder-cap2UI5-web) repository and published to [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build). The rebuild is **event-driven**: every push to builder-cap2UI5-web's `main` runs it, and cap2UI5's sync pipeline pushes a trigger commit there after each framework change — so the playground follows the framework rather than a calendar. A weekly cron remains as a safety net, and a manual run is always possible.

Every deployment is one commit in [web-cap2UI5-build](https://github.com/cap2UI5/web-cap2UI5-build), and its `BUILD_INFO.json` names the exact cap2UI5 commit the site was built from — so you can always tell what you are looking at.

→ Ready for the real thing? Head to the [**Quickstart**](./getting-started) — you'll have the same apps running on a local CAP server in five minutes.
Loading