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
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: test

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
# Until cap2ui5 0.2.0 is on npm, the samples run against cap2UI5's main:
# its plugin packed as `npm publish` packs it, and installed alone -
# which brings the other dependencies along, as the README says for a
# local checkout. Once 0.2.0 is published this goes, a package-lock.json
# pins it, and `cache: npm` and npm ci come back.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: cap2UI5/cap2UI5
path: .cap2ui5
persist-credentials: false
- run: npm pack --pack-destination "$RUNNER_TEMP"
working-directory: .cap2ui5/plugin
- run: npm install --no-save --no-audit --no-fund "$RUNNER_TEMP"/cap2ui5-*.tgz
- run: npm test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
*.sqlite
*.sqlite-*
*.log
.DS_Store
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
104 changes: 102 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,102 @@
# samples
Samples of cap2UI5
# cap2UI5 samples

The [abap2UI5 samples](https://github.com/abap2UI5/samples) as
[cap2UI5](https://github.com/cap2UI5/cap2UI5) apps: every sample a plain
JavaScript class in `srv/apps/`, in an ordinary CAP project - and line for
line the ABAP original. The client is abap2UI5's `z2ui5_if_client` by its own
names, the view is built with `z2ui5_cl_ui5_view_builder`, called the way ABAP
calls it: `client->check_app_prev_stack( )` is
`client.check_app_prev_stack()`.

> [!NOTE]
> **Status: first examples.** Nine of the 129 apps are ported, to agree on the
> conventions below before the rest follows. They need **cap2ui5 0.2.0**,
> which is not on npm yet (the client under its ABAP names and the view
> builder are in its `Unreleased` changelog). Until it is, a plain
> `npm install` answers `ETARGET`; install the plugin packed from a cap2UI5
> checkout instead, which brings the other dependencies along - CI does the
> same with cap2UI5's `main`:
>
> ```bash
> (cd ../cap2UI5 && scripts/assemble-runtime.sh --package 1.145.0 && npm install \
> && npm pack --workspace plugin --pack-destination /tmp)
> npm install --no-save /tmp/cap2ui5-*.tgz
> ```

## Run

```bash
npm install
npm run watch # cds watch - prints the address of every sample
npm test # every sample driven over the wire
```

Log in as `alice` with an empty password (CAP's mocked development user), then
open e.g. <http://localhost:4004/sap/bc/z2ui5?app_start=Z2UI5_CL_SMP_APP_493>.

## The samples so far

| Sample | App | ABAP original |
|---|---|---|
| Basics I - Hello World, the Smallest App | [`Z2UI5_CL_SMP_APP_493`](srv/apps/z2ui5_cl_smp_app_493.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_493.clas.abap) |
| Basics II - Data Binding: Input and Button | [`Z2UI5_CL_SMP_APP_494`](srv/apps/z2ui5_cl_smp_app_494.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_494.clas.abap) |
| Basics III - Lifecycle: Init, Event, Navigated | [`Z2UI5_CL_SMP_APP_495`](srv/apps/z2ui5_cl_smp_app_495.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_495.clas.abap) |
| Table - Editable Cells, Add and Delete Rows | [`Z2UI5_CL_SMP_APP_011`](srv/apps/z2ui5_cl_smp_app_011.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_011.clas.abap) |
| Event - Extra Arguments with t_arg | [`Z2UI5_CL_SMP_APP_167`](srv/apps/z2ui5_cl_smp_app_167.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_167.clas.abap) |
| Popup - Dialog inside a Dialog | [`Z2UI5_CL_SMP_APP_161`](srv/apps/z2ui5_cl_smp_app_161.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_161.clas.abap) |
| Navigation - Return Data and Events to the Caller | [`Z2UI5_CL_SMP_APP_488`](srv/apps/z2ui5_cl_smp_app_488.js) (+ [`_489`](srv/apps/z2ui5_cl_smp_app_489.js)) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_488.clas.abap) |
| Browser - Set the Tab Title | [`Z2UI5_CL_SMP_APP_125`](srv/apps/z2ui5_cl_smp_app_125.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_125.clas.abap) |

## How a sample is ported

**1:1, so that it is recognized at a glance.** Put the ABAP original and the
port side by side and every line has its counterpart:

- **One ABAP class, one file, the same name.** `z2ui5_cl_smp_app_493.clas.abap`
becomes `srv/apps/z2ui5_cl_smp_app_493.js`, registered as
`Z2UI5_CL_SMP_APP_493` - so `?app_start=` is the same on both sides, a
sample that calls another one keeps calling it by its name. The header
keeps the original's `@keywords`, `@summary` and `@docs`, and `@origin`
points back at it.
- **The same class.** The same attributes under the same names, the same
methods (`view_display( )`, `on_event( )`, …) in the same order, the same
dispatcher in `main( )`, `me->client = client` as `this.client = client`.
- **The same calls.** The client's methods and the view builder's are the ABAP
ones; a method's preferred parameter is its one positional argument and
parameters by name are one object with the ABAP names. The view keeps the
original's tree and layout: one call per line, four spaces per tree level,
`end()` in the column of the `ele()` it closes, the `v =` column aligned.
- **The same texts.** Only the page title says `cap2UI5 - …`, and a text that
names an ABAP construct (`z2ui5_if_app`, `client->…`) names the JavaScript
one.

| abap2UI5 | cap2UI5 |
|---|---|
| `DATA name TYPE string.` | `name = "";` - the initial value is the type |
| `DATA t_tab TYPE STANDARD TABLE OF ty_s_row WITH EMPTY KEY.` | `t_tab = t.table({ …one row… });` |
| `METHOD z2ui5_if_app~main.` | `main(client) {` |
| `me->client = client.` / `client->…` in a method | `this.client = client;` / `this.client.…` |
| `IF client->check_on_navigated( ).` | `if (client.check_on_navigated()) {` |
| `client->_bind( name )`, `client->_bind( s_result-product )` | `client._bind("name")`, `client._bind("s_result-product")` |
| `client->_bind( val = t_tab path = abap_true )` | `client._bind({ val: "t_tab", path: true })` |
| ``client->_event( val = `X` arg = `…` )`` | `client._event({ val: "X", arg: "…" })` |
| ``z2ui5_cl_ui5_view_builder=>factory( )->ele( n = `View` ns = `mvc` )`` | `z2ui5_cl_ui5_view_builder.factory().ele({ n: "View", ns: "mvc" })` |
| ``)->a( n = `title` v = `…` )``, `b = abap_true`, `t = …` | `.a({ n: "title", v: "…" })`, `b: true`, `t: …` |
| `client->view_display( view->stringify( ) ).` | `client.view_display(view.stringify());` |
| `z2ui5_if_client=>cs_event-set_title` | `z2ui5_if_client.cs_event.set_title` |
| `client->nav_app_call( NEW z2ui5_cl_smp_app_493( ) )` | `client.nav_app_call("Z2UI5_CL_SMP_APP_493")` |
| `client->nav_app_leave( event = … r_data = … )` | `client.nav_app_leave({ event: …, r_data: … })` |
| `ASSIGN client->get( )-r_event_data->* TO …` | `client.get().r_event_data` - the data itself |
| ``CASE client->get_event( ). WHEN `A` OR `B`.`` | `switch (client.get_event()) { case "A": case "B":` |

### What differs from ABAP

1. **A field is bound by its name**, `client._bind("name")`: ABAP's `_bind( )`
finds the attribute by reference, which a JavaScript value cannot carry.
2. **`this` reads plain copies.** Assign to write:
`this.t_tab = [...this.t_tab, row]` for `INSERT … INTO TABLE t_tab`. A
`push` on the copy is lost. Assigning an object replaces a structure, so
`this.s_result = {}` is `s_result = VALUE #( )`.
3. **Every field is part of the model.** ABAP keeps a `PROTECTED` attribute out
of it; a JavaScript field with an initial value is always bound. The client
is the exception: assigned in `main( )` and not declared, it stays out.
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "cap2ui5-samples",
"private": true,
"description": "The abap2UI5 samples as cap2UI5 apps: plain JavaScript classes in a CAP project, views built with ViewBuilder.",
"type": "module",
"engines": {
"node": ">=22"
},
"dependencies": {
"@cap-js/sqlite": "^3",
"@sap/cds": "^10",
"cap2ui5": "^0.2.0"
},
"devDependencies": {
"@cap-js/cds-test": "^1.0.2",
"fast-xml-parser": "^5.11.1"
},
"scripts": {
"start": "cds-serve",
"watch": "cds watch",
"test": "node --test test/*.test.mjs"
},
"cds": {
"requires": {
"db": {
"kind": "sqlite",
"credentials": {
"url": ":memory:"
}
}
}
}
}
151 changes: 151 additions & 0 deletions srv/apps/z2ui5_cl_smp_app_011.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// @keywords edit input add row delete multiselect toolbar
// @summary An editable table: input cells, adding and deleting rows, multi-select and a toolbar over them.
// @docs https://abap2ui5.github.io/docs/cookbook/model/tables https://abap2ui5.github.io/docs/tutorials/walkthrough/step-8 https://abap2ui5.github.io/docs/tutorials/walkthrough/step-10
// @origin abap2UI5/samples src/z2ui5_cl_smp_app_011.clas.abap
import { defineApp, t, z2ui5_cl_ui5_view_builder } from "cap2ui5";

defineApp("Z2UI5_CL_SMP_APP_011", class {

t_tab = t.table({
selkz: false,
title: "",
value: "",
descr: "",
icon: "",
info: "",
editable: false,
checkbox: false,
});

check_editable_active = false;

view_display() {

const view = z2ui5_cl_ui5_view_builder.factory()
.ele({ n: "View", ns: "mvc" })
.a({ n: "displayBlock", v: "true" })
.a({ n: "height", v: "100%" })
.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: "cap2UI5 - Table - Editable Cells, Add and Delete Rows" })
.a({ n: "showNavButton", b: this.client.check_app_prev_stack() })
.a({ n: "navButtonPress", v: this.client._event_nav_app_leave() })
.a({ n: "id", v: "test2" });

page.tag("MessageStrip")
.a({ n: "text", v: "A MultiSelect table whose input cells switch between display and edit mode via the " +
"toolbar, which also adds new rows and deletes the currently selected ones." })
.a({ n: "type", v: "Information" })
.a({ n: "showIcon", b: true })
.a({ n: "class", v: "sapUiSmallMargin" });

const tab = page.ele("Table")
.a({ n: "items", v: `{path: '${this.client._bind({ val: "t_tab", path: true })}', templateShareable: false}` })
.a({ n: "mode", v: "MultiSelect" })
.ele("headerToolbar")
.ele("OverflowToolbar")
.tag("Title")
.a({ n: "text", v: "title of the table" })
.tag("Button")
// shows an OverflowToolbar filling up - the press is a plain roundtrip
.a({ n: "press", v: this.client._event("BUTTON_TEST") })
.a({ n: "text", v: "test" })
.tag("ToolbarSpacer")
.tag("Button")
.a({ n: "press", v: this.client._event("BUTTON_DELETE") })
.a({ n: "text", v: "delete selected row" })
.a({ n: "icon", v: "sap-icon://delete" })
.tag("Button")
.a({ n: "press", v: this.client._event("BUTTON_ADD") })
.a({ n: "text", v: "add" })
.a({ n: "icon", v: "sap-icon://add" })
.tag("Button")
.a({ n: "press", v: this.client._event("BUTTON_EDIT") })
.a({ n: "text", t: this.check_editable_active ? "display" : "edit" })
.a({ n: "tooltip", v: "Switch the cells between display and edit mode" })
.a({ n: "icon", v: "sap-icon://edit" })
.end()
.end();

tab.ele("columns")
.ele("Column")
.tag("Text")
.a({ n: "text", v: "Title" })
.end()
.ele("Column")
.tag("Text")
.a({ n: "text", v: "Color" })
.end()
.ele("Column")
.tag("Text")
.a({ n: "text", v: "Info" })
.end()
.ele("Column")
.tag("Text")
.a({ n: "text", v: "Description" })
.end()
.ele("Column")
.tag("Text")
.a({ n: "text", v: "Checkbox" });

tab.ele("items")
.ele("ColumnListItem")
.a({ n: "selected", v: "{SELKZ}" })
.ele("cells")
.tag("Input")
.a({ n: "id", v: "test" })
.a({ n: "enabled", v: "{EDITABLE}" })
.a({ n: "value", v: "{TITLE}" })
.tag("Input")
.a({ n: "enabled", v: "{EDITABLE}" })
.a({ n: "value", v: "{VALUE}" })
.tag("Input")
.a({ n: "enabled", v: "{EDITABLE}" })
.a({ n: "value", v: "{INFO}" })
.tag("Input")
.a({ n: "enabled", v: "{EDITABLE}" })
.a({ n: "value", v: "{DESCR}" })
.tag("CheckBox")
.a({ n: "selected", v: "{CHECKBOX}" })
.a({ n: "enabled", v: "{EDITABLE}" });

this.client.view_display(view.stringify());

}

main(client) {

this.client = client;

if (client.check_on_init()) {

this.check_editable_active = false;
this.t_tab = [
{ title: "entry 01", value: "red", info: "completed", descr: "this is a description", checkbox: true },
{ title: "entry 02", value: "blue", info: "completed", descr: "this is a description", checkbox: true },
{ title: "entry 03", value: "green", info: "completed", descr: "this is a description", checkbox: true },
{ title: "entry 04", value: "orange", info: "completed", descr: "", checkbox: true },
{ title: "entry 05", value: "grey", info: "completed", descr: "this is a description", checkbox: true },
{ } ];

this.view_display();
} else if (client.check_on_navigated()) {
this.view_display();

} else if (client.check_on_event("BUTTON_EDIT")) {
this.check_editable_active = !this.check_editable_active;
// a read of t_tab is a copy: the changed rows are written back whole
this.t_tab = this.t_tab.map((row) => ({ ...row, editable: this.check_editable_active }));

} else if (client.check_on_event("BUTTON_DELETE")) {
this.t_tab = this.t_tab.filter((row) => !row.selkz);

} else if (client.check_on_event("BUTTON_ADD")) {

this.t_tab = [...this.t_tab, { editable: this.check_editable_active }];
}

}
});
57 changes: 57 additions & 0 deletions srv/apps/z2ui5_cl_smp_app_125.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @keywords document.title tab caption headline set_title
// @summary Sets the browser tab title from the app, so a bookmarked or duplicated window says which app it holds.
// @docs https://abap2ui5.github.io/docs/cookbook/browser_interaction/title
// @origin abap2UI5/samples src/z2ui5_cl_smp_app_125.clas.abap
import { defineApp, z2ui5_cl_ui5_view_builder, z2ui5_if_client } from "cap2ui5";

defineApp("Z2UI5_CL_SMP_APP_125", class {

title = "my title";

main(client) {

if (client.check_on_navigated()) {

const view = z2ui5_cl_ui5_view_builder.factory()
.ele({ n: "View", ns: "mvc" })
.a({ n: "displayBlock", v: "true" })
.a({ n: "height", v: "100%" })
.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 page = view.ele("Shell")
.ele("Page")
.a({ n: "title", v: "cap2UI5 - Browser - Set the Tab Title" })
.a({ n: "showNavButton", b: client.check_app_prev_stack() })
.a({ n: "navButtonPress", v: client._event_nav_app_leave() });

page.tag("MessageStrip")
.a({ n: "text", v: "Enter a title and press the button to run the set_title front-end action, which updates " +
"the browser tab title (document.title) without reloading the page." })
.a({ n: "type", v: "Information" })
.a({ n: "showIcon", b: true })
.a({ n: "class", v: "sapUiSmallMargin" });

page.ele({ n: "SimpleForm", ns: "form" })
.a({ n: "title", v: "Form Title" })
.a({ n: "editable", b: true })
.ele({ n: "content", ns: "form" })
.tag("Label")
.a({ n: "text", v: "title" })
.tag("Input")
.a({ n: "value", v: client._bind("title") })
.tag("Button")
.a({ n: "press", v: client._event("SET_TITLE") })
.a({ n: "text", v: "Set Title" });
client.view_display(view.stringify());

} else if (client.check_on_event("SET_TITLE")) {

client.follow_up_action({
val: z2ui5_if_client.cs_event.set_title,
t_arg: [this.title] });

}

}
});
Loading
Loading