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
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ nuisance, and nothing may ever resolve old commits of this repo —
`UPSTREAM_HEAD` and `run/input/UPSTREAM_COMMIT` are change-detection
tokens / upstream shas, not references into this repo's history.

The one place that *does* resolve a pin is `scripts/mirror-core.js`: it
fetches the `UPSTREAM_HEAD` sha from builder-abap2UI5-js and compares
committer dates against the freshly cloned HEAD. That stays within the
upstream policy because it is **ordering arbitration only** — the pin is
checked out solely when it is NEWER than HEAD (the fast-double-push race),
never to build from an older commit, and an unfetchable pin warns and falls
back to HEAD instead of failing. Keep both properties if you touch it.

## Rules

- Never edit the cap2UI5 app repo directly — change `src/` here and
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ in [`src/`](src/) plus the published core package (the npm package
> `src/srv/app/` or an external `Z2UI5_APP_DIRS` folder — never in the app
> repo's `srv/app/`, which is regenerated.)

cap2UI5 is organized as three repositories, from framework to finished app:
cap2UI5 is organized as six repositories, from framework to finished app:

| Repository | What it is |
|---|---|
| [`builder-abap2UI5-js`](https://github.com/cap2UI5/builder-abap2UI5-js) | abap2UI5 ported to JavaScript — generates the core package `abap2UI5` |
| [`builder-cap2UI5`](https://github.com/cap2UI5/builder-cap2UI5) (here) | generates the full CAP app from `src/` + the published core |
| [`cap2UI5`](https://github.com/cap2UI5/cap2UI5) | the finished, deployable CAP app (**generated**) |
| [`builder-cap2UI5-web`](https://github.com/cap2UI5/builder-cap2UI5-web) | browser build tooling — bundles the app into a static site |
| [`web-cap2UI5-build`](https://github.com/cap2UI5/web-cap2UI5-build) | the built static site (**generated**), served via GitHub Pages |
| [`docs`](https://github.com/cap2UI5/docs) | the VitePress documentation site |

The full documentation lives in [cap2UI5/docs](https://github.com/cap2UI5/docs).

## Build

Expand Down
2 changes: 1 addition & 1 deletion src/LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 oblomov-ai
Copyright (c) 2026 cap2UI5

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
118 changes: 85 additions & 33 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Bringing the [abap2UI5](https://github.com/abap2UI5/abap2UI5) concept to CAP/Nod
> [builder-cap2UI5](https://github.com/cap2UI5/builder-cap2UI5) for how the
> pipeline works. Review and test before relying on it.

📖 **Documentation:** [cap2UI5/docs](https://github.com/cap2UI5/docs)

#### Features
* XML View Generation - Create UI5 views programmatically in your backend
* Data Binding & Exchange - Seamless two-way data binding between frontend and backend
Expand Down Expand Up @@ -141,7 +143,8 @@ npm test
runs the jest suite: [`test/starter.test.js`](test/starter.test.js) boots the
real server via `cds.test()` and asserts all three layers end-to-end
(starter page + bootstrap HTML, roundtrip returning view XML, draft row
persisted per roundtrip), and [`test/z2ui5_cl_xml_view.test.js`](test/z2ui5_cl_xml_view.test.js)
persisted per roundtrip), and
[`test/view-builder-namespaces.test.js`](test/view-builder-namespaces.test.js)
covers the view builder.

## Security model
Expand Down Expand Up @@ -202,23 +205,44 @@ bundled samples and the framework classes live in the vendored core package
###### App
```js
// z2ui5_cl_ui5_app_hi_world.js — ships with the core package
const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");

class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app {
name = ``;

async main(client) {
if (client.check_on_init()) {
const view = z2ui5_cl_xml_view.factory()
.shell()
.page(`abap2UI5 - Hello World`)
.simple_form({ editable: true })
.content(`form`)
.title({ ns: `core`, text: `Enter a value and send it to the server...` })
.label(`Name`)
.input(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,
// not in the default sap.m namespace — an unprefixed <SimpleForm>
// resolves to sap.m.SimpleForm, which does not exist, and the view
// fails to LOAD rather than to render.
.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`)) {
client.message_box_display(`Your name is ${this.name}`);
Expand All @@ -228,6 +252,12 @@ class z2ui5_cl_ui5_app_hi_world extends z2ui5_if_app {

module.exports = z2ui5_cl_ui5_app_hi_world;
```

The view builder writes raw XML: `ele()` opens an element and descends into
it, `tag()` adds a leaf and stays put, `a()` sets an attribute on whatever
was last opened, and `end()` climbs back to the parent. Nothing is mapped
for you — every element names its namespace and every attribute is spelled
exactly as UI5 expects it (`showIcon`, not `showicon`).
###### Demo
<img width="500" height="393" alt="image" src="https://github.com/user-attachments/assets/3acd8c43-3733-40b0-a6f9-27ae6beba6e7" />

Expand All @@ -247,12 +277,22 @@ module.exports = z2ui5_cl_ui5_app_hi_world;
```js
// srv/app/z2ui5_cl_app_read_odata.js — ships with the project
const cds = require("@sap/cds");
const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");

class z2ui5_cl_app_read_odata extends z2ui5_if_app {
customers = [];

// Table, Column, ColumnListItem, Text and Input all live in sap.m, so they
// ride the default namespace declared on the root.
_view() {
return 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` });
}

async main(client) {
if (client.check_on_init()) {
// The remote demo service may be unreachable (offline, proxy, service
Expand All @@ -263,32 +303,44 @@ class z2ui5_cl_app_read_odata extends z2ui5_if_app {
SELECT.from(`Customers`).columns(`CompanyName`, `ContactName`).limit(20)
);
} catch (e) {
const view = z2ui5_cl_xml_view.factory();
view.shell()
.page(`abap2UI5 - Table with Data Fetched via Remote OData`)
.message_strip({
text: `Remote Northwind service not reachable: ${e.message}`,
type: `Error`,
showicon: true,
class: `sapUiSmallMargin`,
});
const view = this._view();
view
.ele({ n: `Shell` })
.ele({ n: `Page` })
.a({ n: `title`, v: `abap2UI5 - Table with Data Fetched via Remote OData` })
.tag({ n: `MessageStrip` })
.a({ n: `text`, v: `Remote Northwind service not reachable: ${e.message}` })
.a({ n: `type`, v: `Error` })
.a({ n: `showIcon`, b: true })
.a({ n: `class`, v: `sapUiSmallMargin` });
client.view_display(view.stringify());
client.message_box_display(`Remote Northwind service not reachable: ${e.message}`, `error`);
return;
}

const view = z2ui5_cl_xml_view.factory();
const tab = view.shell()
.page(`abap2UI5 - Table with Data Fetched via Remote OData`)
.table({ items: client._bind_edit(this.customers) });
tab.columns()
.column().text(`CompanyName`).get_parent()
.column().text(`ContactName`);
tab.items()
.column_list_item()
.cells()
.input({ value: `{COMPANYNAME}`, enabled: true })
.input({ value: `{CONTACTNAME}`, enabled: true });
const view = this._view();
const tab = view
.ele({ n: `Shell` })
.ele({ n: `Page` })
.a({ n: `title`, v: `abap2UI5 - Table with Data Fetched via Remote OData` })
.ele({ n: `Table` })
.a({ n: `items`, v: client._bind_edit(this.customers) });

const columns = tab.ele({ n: `columns` });
columns.ele({ n: `Column` }).tag({ n: `Text` }).a({ n: `text`, v: `CompanyName` });
columns.ele({ n: `Column` }).tag({ n: `Text` }).a({ n: `text`, v: `ContactName` });

tab
.ele({ n: `items` })
.ele({ n: `ColumnListItem` })
.ele({ n: `cells` })
.tag({ n: `Input` })
.a({ n: `value`, v: `{COMPANYNAME}` })
.a({ n: `enabled`, b: true })
.tag({ n: `Input` })
.a({ n: `value`, v: `{CONTACTNAME}` })
.a({ n: `enabled`, b: true });

client.view_display(view.stringify());
}
}
Expand Down
20 changes: 15 additions & 5 deletions src/srv/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,25 @@ exports (the RTTI lookup is name-based):

```js
// srv/app/z2ui5_cl_app_my_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 z2ui5_cl_app_my_app extends z2ui5_if_app {
async main(client) {
if (client.check_on_init()) {
const view = z2ui5_cl_xml_view.factory()
.Page({ title: "My App" })
.Button({ text: "Hello" });
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({ n: `Shell` })
.ele({ n: `Page` })
.a({ n: `title`, v: `My App` })
.tag({ n: `Button` })
.a({ n: `text`, v: `Hello` });

client.view_display(view.stringify());
}
}
Expand Down
68 changes: 46 additions & 22 deletions src/srv/app/z2ui5_cl_app_read_odata.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
const cds = require("@sap/cds");
const z2ui5_cl_xml_view = require("abap2UI5/z2ui5_cl_xml_view");
const z2ui5_cl_ui5_view_builder = require("abap2UI5/z2ui5_cl_ui5_view_builder");
const z2ui5_if_app = require("abap2UI5/z2ui5_if_app");

class z2ui5_cl_app_read_odata extends z2ui5_if_app {
customers = [];

// The builder writes raw XML — every element names its namespace and every
// attribute is spelled exactly as UI5 expects it (`showIcon`, not
// `showicon`). Table, Column, ColumnListItem, Text and Input all live in
// sap.m, so they ride the default namespace declared on the root.
_view() {
return 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` });
}

async main(client) {
if (client.check_on_init()) {
// The remote demo service may be unreachable (offline, proxy, service
Expand All @@ -15,32 +27,44 @@ class z2ui5_cl_app_read_odata extends z2ui5_if_app {
SELECT.from(`Customers`).columns(`CompanyName`, `ContactName`).limit(20)
);
} catch (e) {
const view = z2ui5_cl_xml_view.factory();
view.shell()
.page(`abap2UI5 - Table with Data Fetched via Remote OData`)
.message_strip({
text: `Remote Northwind service not reachable: ${e.message}`,
type: `Error`,
showicon: true,
class: `sapUiSmallMargin`,
});
const view = this._view();
view
.ele({ n: `Shell` })
.ele({ n: `Page` })
.a({ n: `title`, v: `abap2UI5 - Table with Data Fetched via Remote OData` })
.tag({ n: `MessageStrip` })
.a({ n: `text`, v: `Remote Northwind service not reachable: ${e.message}` })
.a({ n: `type`, v: `Error` })
.a({ n: `showIcon`, b: true })
.a({ n: `class`, v: `sapUiSmallMargin` });
client.view_display(view.stringify());
client.message_box_display(`Remote Northwind service not reachable: ${e.message}`, `error`);
return;
}

const view = z2ui5_cl_xml_view.factory();
const tab = view.shell()
.page(`abap2UI5 - Table with Data Fetched via Remote OData`)
.table({ items: client._bind_edit(this.customers) });
tab.columns()
.column().text(`CompanyName`).get_parent()
.column().text(`ContactName`);
tab.items()
.column_list_item()
.cells()
.input({ value: `{COMPANYNAME}`, enabled: true })
.input({ value: `{CONTACTNAME}`, enabled: true });
const view = this._view();
const tab = view
.ele({ n: `Shell` })
.ele({ n: `Page` })
.a({ n: `title`, v: `abap2UI5 - Table with Data Fetched via Remote OData` })
.ele({ n: `Table` })
.a({ n: `items`, v: client._bind_edit(this.customers) });

const columns = tab.ele({ n: `columns` });
columns.ele({ n: `Column` }).tag({ n: `Text` }).a({ n: `text`, v: `CompanyName` });
columns.ele({ n: `Column` }).tag({ n: `Text` }).a({ n: `text`, v: `ContactName` });

tab
.ele({ n: `items` })
.ele({ n: `ColumnListItem` })
.ele({ n: `cells` })
.tag({ n: `Input` })
.a({ n: `value`, v: `{COMPANYNAME}` })
.a({ n: `enabled`, b: true })
.tag({ n: `Input` })
.a({ n: `value`, v: `{CONTACTNAME}` })
.a({ n: `enabled`, b: true });

client.view_display(view.stringify());
}
}
Expand Down