From a8bd5638a855abfaad0e8a0518991ffe9564cd74 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 01:57:31 +0000 Subject: [PATCH 1/4] Refuse a module in srv/apps that samples.json does not list samples.test.mjs and the consumer test take the samples from the directory, the differential test from scripts/samples.json. A module that is in the directory but not in the file - left behind when a sample is dropped from the list, or written by hand without an entry - is therefore packed, served and started like any other, and never held to its ABAP original. generate (and its --check, the CI gate) now names such a module and exits 1, as it already does for a sample listed as generated and handwritten at once. Two error paths say what is wrong instead of failing on the read: a listed sample the checkout at the pin does not have (a mistyped name) is reported as missing and counted as a failure rather than ending the run with ENOENT, and originals.mjs says how to clone the checkout when there is none, as generate.mjs does. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NVQ3JnU7R1QFQVgSoMK9RE --- CHANGELOG.md | 5 +++++ scripts/generate.mjs | 15 +++++++++++++++ scripts/originals.mjs | 5 +++++ 3 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67222b1..d58243d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ versions [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- `npm run generate` (and its `--check`, the CI gate) refuses a module in + `srv/apps/` that `scripts/samples.json` does not list - such a module is + packed, served and started by the tests, but never held to its original by + the differential test, which reads `samples.json` - and names a listed + sample the checkout at the pin does not have instead of failing on the read. - The differential test transpiles the originals against open-abap-core at the commit `@abap2ui5/node-runtime` records in its `package.json` (`abap2ui5.openAbapCore`) instead of HEAD, so the original runs on the diff --git a/scripts/generate.mjs b/scripts/generate.mjs index 596fad9..a6222c4 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -50,11 +50,26 @@ if (overlap.length) { console.error(`generate: ${overlap.join(", ")} listed as generated AND handwritten in scripts/samples.json`); process.exit(1); } +// A module in srv/apps that samples.json does not list is packed, served and +// started by the tests like any other - but never held to an original by the +// differential test, which reads samples.json. So it is listed, or it goes. +const orphans = fs.readdirSync(path.join(ROOT, "srv", "apps")) + .filter((f) => /^z2ui5_cl_smp_app_\d+\.js$/.test(f)).map((f) => f.replace(/\.js$/, "")) + .filter((n) => !generated.includes(n) && !(n in handwritten)); +if (orphans.length) { + console.error(`generate: srv/apps/${orphans.join(".js, srv/apps/")}.js not listed in scripts/samples.json - list it, or delete it`); + process.exit(1); +} let failed = 0; let changed = 0; for (const name of generated) { const file = path.join(src, `${name}.clas.abap`); + if (!fs.existsSync(file)) { + console.error(`missing: src/${name}.clas.abap is not in abap2UI5/samples@${pin.slice(0, 7)} - is the name right?`); + failed++; + continue; + } let code; try { ({ code } = abap2js(fs.readFileSync(file, "utf8"), { diff --git a/scripts/originals.mjs b/scripts/originals.mjs index a52d895..0c3cca1 100644 --- a/scripts/originals.mjs +++ b/scripts/originals.mjs @@ -30,6 +30,11 @@ const pin = fs.readFileSync(path.join(ROOT, "ABAP2UI5_SAMPLES_PIN"), "utf8").tri const { generated, handwritten } = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "samples.json"), "utf8")); const names = [...generated, ...Object.keys(handwritten)]; +if (!fs.existsSync(path.join(checkout, "src"))) { + console.error(`originals: no abap2UI5/samples checkout at ${checkout} - clone it and check out ${pin}:\n` + + ` git clone https://github.com/abap2UI5/samples ${checkout} && git -C ${checkout} checkout ${pin}`); + process.exit(1); +} const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); if (head !== pin) throw new Error(`${checkout} is at ${head}, ABAP2UI5_SAMPLES_PIN says ${pin}`); From e25254c94a399177043b42bf99ad97fe5c6fd5cb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:20:59 +0000 Subject: [PATCH 2/4] Move the pin to abap2UI5/samples 2e998ef and follow the two reworked originals The pin was six commits behind abap2UI5/samples' main. Of the 71 samples here, two originals changed since: Z2UI5_CL_SMP_APP_173 was reworked into a metadata-driven table and form (template:with, a nested template:repeat, template:if/elseif/else) and is translated again by abap2js, as before. Z2UI5_CL_SMP_APP_176 gained a button that changes the layout table and re-renders only the nested view - through ASSIGN mt_layout[ fname = `AGE` ] TO FIELD-SYMBOL( ), which abap2js refuses - so it moves to the handwritten list and follows its original's diff by hand, written as abap2js would write it: the row is read as a copy and written back whole, as Z2UI5_CL_SMP_APP_011 does for its LOOP ... REFERENCE INTO. Both pass the differential test against their transpiled originals: 71 samples, now 240 roundtrips (the new TOGGLE_AGE event), no difference. The README's status and table follow - abap2UI5/samples has 138 samples at this commit; the nine new ones (531 to 539) are not translated here yet. abap2js translates 533 to 537 of them and refuses 531, 532, 538 and 539. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NVQ3JnU7R1QFQVgSoMK9RE --- ABAP2UI5_SAMPLES_PIN | 2 +- CHANGELOG.md | 8 ++ README.md | 17 +-- scripts/samples.json | 2 +- srv/apps/z2ui5_cl_smp_app_173.js | 171 +++++++++++++++++++++++++++---- srv/apps/z2ui5_cl_smp_app_176.js | 85 +++++++++------ 6 files changed, 224 insertions(+), 61 deletions(-) diff --git a/ABAP2UI5_SAMPLES_PIN b/ABAP2UI5_SAMPLES_PIN index 20724f9..d3f02ce 100644 --- a/ABAP2UI5_SAMPLES_PIN +++ b/ABAP2UI5_SAMPLES_PIN @@ -1 +1 @@ -cdd5c8b09a4ecdc51943b827ff6711d6c3af13a5 +2e998ef1c90a1350e4ce1677bba59122fc8dac96 diff --git a/CHANGELOG.md b/CHANGELOG.md index d58243d..49eca3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ versions [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- The pin moves to abap2UI5/samples `2e998ef` (2026-09-30). `Z2UI5_CL_SMP_APP_173` + is translated again from its reworked original - a metadata-driven table + and form with `template:with`, nested `template:repeat` and + `template:if/elseif/else`. `Z2UI5_CL_SMP_APP_176` gained a button that + re-renders only the nested view, through an `ASSIGN mt_layout[ … ] TO + FIELD-SYMBOL( )` abap2js refuses, so it is ported by hand from here on and + listed under `handwritten`. abap2UI5/samples has 138 samples at that + commit; the nine new ones (531 to 539) are not translated yet. - `npm run generate` (and its `--check`, the CI gate) refuses a module in `srv/apps/` that `scripts/samples.json` does not list - such a module is packed, served and started by the tests, but never held to its original by diff --git a/README.md b/README.md index 9b0a010..23a6ab9 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ original's comments and texts, all of them, as they are. `client->check_app_prev_stack( )` is `client.check_app_prev_stack()`. > [!NOTE] -> **Status: 71 of the 129 samples.** 69 are generated by abap2js from +> **Status: 71 of the 138 samples.** 68 are generated by abap2js from > abap2UI5/samples at the commit in [`ABAP2UI5_SAMPLES_PIN`](ABAP2UI5_SAMPLES_PIN), -> two are ported by hand; the [differential test](#the-differential-test) holds +> three are ported by hand; the [differential test](#the-differential-test) holds > every one of them to its ABAP original. ## Add them to your project @@ -120,8 +120,8 @@ open e.g. . | Popup - Element Binding to the Selected Row | [`Z2UI5_CL_SMP_APP_470`](srv/apps/z2ui5_cl_smp_app_470.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_470.clas.abap) | | Popup - Navigate between Dialogs (NavContainer) | [`Z2UI5_CL_SMP_APP_170`](srv/apps/z2ui5_cl_smp_app_170.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_170.clas.abap) | | Table - Editable Cells, Add and Delete Rows | [`Z2UI5_CL_SMP_APP_011`](srv/apps/z2ui5_cl_smp_app_011.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_011.clas.abap) | -| Templating - Build Columns Dynamically (template:repeat) | [`Z2UI5_CL_SMP_APP_173`](srv/apps/z2ui5_cl_smp_app_173.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_173.clas.abap) | -| Templating - Dynamic Content in a Nested View | [`Z2UI5_CL_SMP_APP_176`](srv/apps/z2ui5_cl_smp_app_176.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_176.clas.abap) | +| Templating - Metadata-Driven Table and Form | [`Z2UI5_CL_SMP_APP_173`](srv/apps/z2ui5_cl_smp_app_173.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_173.clas.abap) | +| Templating - Dynamic Content in a Nested View | [`Z2UI5_CL_SMP_APP_176`](srv/apps/z2ui5_cl_smp_app_176.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_176.clas.abap) | | Tree - Inside a Dialog | [`Z2UI5_CL_SMP_APP_462`](srv/apps/z2ui5_cl_smp_app_462.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_462.clas.abap) | | Tree - Nested ABAP Table in a sap.m.Tree | [`Z2UI5_CL_SMP_APP_460`](srv/apps/z2ui5_cl_smp_app_460.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_460.clas.abap) | @@ -140,9 +140,10 @@ is made: - **`handwritten`** - a class abap2js refuses, ported by hand the way abap2js would write it, with the construct it refuses as the reason: `Z2UI5_CL_SMP_APP_011` (`LOOP AT t_tab REFERENCE INTO` writes through the - row, and `DELETE t_tab WHERE`) and `Z2UI5_CL_SMP_APP_488` - (`ASSIGN … ->* TO FIELD-SYMBOL( )`). A sample moves to `generated` when - abap2js learns the construct. + row, and `DELETE t_tab WHERE`), `Z2UI5_CL_SMP_APP_176` + (`ASSIGN mt_layout[ … ] TO FIELD-SYMBOL( )` writes through the row) and + `Z2UI5_CL_SMP_APP_488` (`ASSIGN … ->* TO FIELD-SYMBOL( )`). A sample moves + to `generated` when abap2js learns the construct. The checkout is `--samples `, else `$ABAP2UI5_SAMPLES`, else `../abap2UI5-samples`, and it has to be at the pin - `@origin` in a module @@ -172,7 +173,7 @@ it beside the translation in one cap2UI5 server. Both are started, every event the original's first view wires is fired on a fresh start of each, and what comes back is compared: the XML of every view slot, the model the original sends, every other action and which app has the screen - today 71 samples in -239 roundtrips, without a difference. One is known and left on purpose: +240 roundtrips, without a difference. One is known and left on purpose: `Z2UI5_CL_SMP_APP_122` prints a number with `CONV string( )`, which keeps ABAP's trailing sign position (`"0 "`) and JavaScript's `String( )` does not. diff --git a/scripts/samples.json b/scripts/samples.json index 573bbaf..2f8f332 100644 --- a/scripts/samples.json +++ b/scripts/samples.json @@ -19,7 +19,6 @@ "z2ui5_cl_smp_app_167", "z2ui5_cl_smp_app_170", "z2ui5_cl_smp_app_173", - "z2ui5_cl_smp_app_176", "z2ui5_cl_smp_app_186", "z2ui5_cl_smp_app_189", "z2ui5_cl_smp_app_202", @@ -73,6 +72,7 @@ ], "handwritten": { "z2ui5_cl_smp_app_011": "LOOP AT t_tab REFERENCE INTO writes through the row, and DELETE t_tab WHERE (abap2js: neither supported yet)", + "z2ui5_cl_smp_app_176": "ASSIGN mt_layout[ fname = ... ] TO FIELD-SYMBOL( ) writes through the row (abap2js: ASSIGN not supported yet)", "z2ui5_cl_smp_app_488": "ASSIGN ls_get-r_event_data->* TO FIELD-SYMBOL( ) (abap2js: not supported yet)" } } diff --git a/srv/apps/z2ui5_cl_smp_app_173.js b/srv/apps/z2ui5_cl_smp_app_173.js index 616d06b..0fd7f6e 100644 --- a/srv/apps/z2ui5_cl_smp_app_173.js +++ b/srv/apps/z2ui5_cl_smp_app_173.js @@ -1,7 +1,15 @@ -// @keywords template repeat runtime generated columns if then else -// @summary Builds the columns of a table at runtime with template:repeat, including the if/then/else the templating language brings. +// @keywords xml templating xmlpreprocessor template:repeat template:if template:elseif template:with meta model metamodel metadata driven dynamic columns form nested repeat startindex length +// @summary XML templating driven by a meta model: table columns built with template:repeat, a form generated from a field catalogue with nested template:repeat, template:with and template:if/elseif/else, and a template:if that re-renders on a switch. // @docs https://abap2ui5.github.io/docs/cookbook/view/xml_templating // @origin abap2UI5/samples src/z2ui5_cl_smp_app_173.clas.abap +// +// The view is expanded by the UI5 XMLPreprocessor before its controls exist. +// Its input is a meta model: data ABOUT the view - which columns a table has, +// which fields a form has and of which type - instead of the data it shows. +// A UI5 app would hand the preprocessor an OData meta model; here the meta +// model is plain ABAP data, bound like any attribute and read under the +// template> model name. The rows the controls then display stay ordinary +// runtime bindings against the default model. import { defineApp, t, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; const ty_s_data = { @@ -16,19 +24,47 @@ const ty_s_layout = { visible: "", }; +const ty_s_field = { + name: "", + label: "", + type: "", +}; + +const ty_s_group = { + title: "", + t_field: t.table(ty_s_field), +}; + +const ty_s_meta = { + entity: "", + t_group: t.table(ty_s_group), +}; + +const ty_s_detail = { + name: "", + date: "", + city: "", + age: 0, + active: false, +}; + defineApp("Z2UI5_CL_SMP_APP_173", class { - mv_flag = false; + mv_flag = false; mt_layout = t.table(ty_s_layout); mt_data = t.table(ty_s_data); + ms_meta = ty_s_meta; + ms_detail = ty_s_detail; view_display() { - // the template model is the view model, so what the repeat and the if - // read are bound attributes - their paths are composed from the bind - // call, never written by hand + // the template model is the view model, so what the repeat, the with and + // the if read are bound attributes - their paths are composed from the + // bind call, never written by hand const layout_path = `{template>${this.client._bind({ val: "mt_layout", path: true })}}`; const flag_path = `{template>${this.client._bind({ val: "mv_flag", path: true })}}`; + const meta_path = `template>${this.client._bind({ val: "ms_meta", path: true })}`; + const detail_path = `{${this.client._bind({ val: "ms_detail", path: true })}}`; let view = z2ui5_cl_ui5_view_builder.factory() .ele({ n: "View", ns: "mvc" }) @@ -37,23 +73,30 @@ defineApp("Z2UI5_CL_SMP_APP_173", class { .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:template", v: "http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1" }); view = view.ele("Shell") .ele("Page") - .a({ n: "title", v: "abap2UI5 - Templating - Build Columns Dynamically (template:repeat)" }) + .a({ n: "title", v: "abap2UI5 - Templating - Metadata-Driven Table and Form" }) .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }) .a({ n: "class", v: "sapUiContentPadding" }) .a({ n: "id", v: "page_main" }); view.tag("MessageStrip") - .a({ n: "text", v: "This sample builds table columns and cells dynamically from a layout table " + - "using template repeat, plus a template if/then/else that re-renders on a switch." }) + .a({ n: "text", v: "XML templating expands the view from a meta model before any control exists: " + + "the table columns come from a layout table (template:repeat), the form from a field catalogue " + + "(template:with, nested template:repeat, template:if/elseif/else), and the icon below re-renders on the switch." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); + // 1 - columns and cells from the layout table + view.tag("Title") + .a({ n: "text", v: "Table - columns from a layout table (template:repeat)" }) + .a({ n: "class", v: "sapUiSmallMarginTop" }); + view.ele("Table") .a({ n: "items", v: this.client._bind("mt_data") }) .ele("columns") @@ -77,8 +120,73 @@ defineApp("Z2UI5_CL_SMP_APP_173", class { .ele("ObjectIdentifier") .a({ n: "text", v: "{= '{' + ${L1>FNAME} + '}' }" }); - view.tag("Label") - .a({ n: "text", v: "IF Template (with re-rendering)" }); + // 2 - a form from the field catalogue of the meta model. template:with + // gives the meta model a short name, the outer repeat builds one title + // per group, the inner repeat one label and one control per field, and + // the if/elseif/else picks the control from the field's type + view.tag("Title") + .a({ n: "text", v: "Form - fields from a meta model (template:with, nested repeat, if/elseif/else)" }) + .a({ n: "class", v: "sapUiMediumMarginTop" }); + + const box = view.ele({ n: "with", ns: "template" }) + .a({ n: "path", v: meta_path }) + .a({ n: "var", v: "meta" }) + .ele("VBox") + .a({ n: "binding", v: detail_path }); + + // startIndex and length cut the list - the header shows the first two + // fields of the first group only + box.ele("ObjectHeader") + .a({ n: "title", v: "{meta>ENTITY}" }) + .ele("attributes") + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: "{path: 'meta>T_GROUP/0/T_FIELD', startIndex: 0, length: 2}" }) + .a({ n: "var", v: "head" }) + .tag("ObjectAttribute") + .a({ n: "title", v: "{head>LABEL}" }) + .a({ n: "text", v: "{= '{' + ${head>NAME} + '}' }" }); + + const field = box.ele({ n: "SimpleForm", ns: "form" }) + .a({ n: "editable", b: true }) + .a({ n: "layout", v: "ResponsiveGridLayout" }) + .ele({ n: "content", ns: "form" }) + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: "{meta>T_GROUP}" }) + .a({ n: "var", v: "group" }) + .tag({ n: "Title", ns: "core" }) + .a({ n: "text", v: "{group>TITLE}" }) + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: "{group>T_FIELD}" }) + .a({ n: "var", v: "field" }); + + field.tag("Label") + .a({ n: "text", v: "{field>LABEL}" }); + + field.ele({ n: "if", ns: "template" }) + .a({ n: "test", v: "{= ${field>TYPE} === 'BOOLEAN' }" }) + .ele({ n: "then", ns: "template" }) + .tag("Switch") + .a({ n: "state", v: "{= '{' + ${field>NAME} + '}' }" }) + .end() + .ele({ n: "elseif", ns: "template" }) + .a({ n: "test", v: "{= ${field>TYPE} === 'DATE' }" }) + .tag("DatePicker") + .a({ n: "value", v: "{= '{' + ${field>NAME} + '}' }" }) + .a({ n: "valueFormat", v: "yyyy-MM-dd" }) + .end() + .ele({ n: "elseif", ns: "template" }) + .a({ n: "test", v: "{= ${field>TYPE} === 'NUMBER' }" }) + .tag("StepInput") + .a({ n: "value", v: "{= '{' + ${field>NAME} + '}' }" }) + .end() + .ele({ n: "else", ns: "template" }) + .tag("Input") + .a({ n: "value", v: "{= '{' + ${field>NAME} + '}' }" }); + + // 3 - an if on a bound flag, re-rendered by the switch + view.tag("Title") + .a({ n: "text", v: "IF Template (with re-rendering)" }) + .a({ n: "class", v: "sapUiMediumMarginTop" }); view.tag("Switch") .a({ n: "state", v: this.client._bind("mv_flag") }) .a({ n: "change", v: this.client._event("CHANGE_FLAG") }); @@ -100,21 +208,44 @@ defineApp("Z2UI5_CL_SMP_APP_173", class { } - main(client) { + model_init() { - this.client = client; + this.mt_data = [ { name: "Theo", date: "01.01.2000", age: "5" }, + { name: "Lore", date: "01.01.2000", age: "1" } ]; - if (client.check_on_init()) { + this.mt_layout = [ { fname: "NAME", merge: "false", visible: "true" }, + { fname: "DATE", merge: "false", visible: "true" }, + { fname: "AGE", merge: "false", visible: "false" } ]; - this.mt_data = [ { name: "Theo", date: "01.01.2000", age: "5" }, - { name: "Lore", date: "01.01.2000", age: "1" } ]; + // the meta model - which fields the form has, in which group, of which + // type. Reorder a field, move it to the other group or change its type + // and the generated form follows, no view code changes + this.ms_meta = { + entity: "Person", + t_group: [ + { title: "General", + t_field: [ { name: "NAME", label: "Name", type: "STRING" }, + { name: "DATE", label: "Birth Date", type: "DATE" }, + { name: "CITY", label: "City", type: "STRING" } ] }, + { title: "Details", + t_field: [ { name: "AGE", label: "Age", type: "NUMBER" }, + { name: "ACTIVE", label: "Active", type: "BOOLEAN" } ] } ] }; - this.mt_layout = [ { fname: "NAME", merge: "false", visible: "true" }, - { fname: "DATE", merge: "false", visible: "true" }, - { fname: "AGE", merge: "false", visible: "false" } ]; + this.ms_detail = { name: "Lore", + date: "2000-01-01", + city: "Walldorf", + age: 26, + active: true }; - this.view_display(); + } + + main(client) { + this.client = client; + + if (client.check_on_init()) { + this.model_init(); + this.view_display(); } else if (client.check_on_navigated()) { this.view_display(); } else if (client.check_on_event("CHANGE_FLAG")) { diff --git a/srv/apps/z2ui5_cl_smp_app_176.js b/srv/apps/z2ui5_cl_smp_app_176.js index f2bb00c..fb3f005 100644 --- a/srv/apps/z2ui5_cl_smp_app_176.js +++ b/srv/apps/z2ui5_cl_smp_app_176.js @@ -1,5 +1,5 @@ -// @keywords template repeat runtime generated nested nest_view_display -// @summary XML templating inside a nested view: the generated content is built where the sub view is rendered. +// @keywords xml templating xmlpreprocessor template:repeat meta model metamodel metadata driven dynamic columns nested view nest_view_display re-render refresh +// @summary XML templating inside a nested view: the columns come from a layout table via template:repeat, and a change to that table re-renders only the nested view with nest_view_display while the main view stays on screen. // @docs https://abap2ui5.github.io/docs/cookbook/view/nested_views https://abap2ui5.github.io/docs/cookbook/view/xml_templating // @origin abap2UI5/samples src/z2ui5_cl_smp_app_176.clas.abap import { defineApp, t, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; @@ -43,6 +43,16 @@ defineApp("Z2UI5_CL_SMP_APP_176", class { this.view_display(); this.nest_view_display(); + } else if (client.check_on_event("TOGGLE_AGE")) { + + // templating ran once, when the nested view was built - a changed + // layout table only shows once the nested view is built again. The + // main view is not sent, it stays on screen as it is + // (a read of mt_layout is a copy: the AGE row is written back whole) + this.mt_layout = this.mt_layout.map((age) => (age.fname === "AGE" + ? { ...age, visible: age.visible === "true" ? "false" : "true" } : age)); + this.nest_view_display(); + } } @@ -60,17 +70,28 @@ defineApp("Z2UI5_CL_SMP_APP_176", class { .ele("Page") .a({ n: "title", v: "abap2UI5 - Templating - Dynamic Content in a Nested View" }) .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) - .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }) - .a({ n: "id", v: "test" }); + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); page.tag("MessageStrip") .a({ n: "text", v: "This sample renders a main view and then embeds a second view into it as " + "nested content via nest_view_display; the nested table builds its columns and cells " + - "at runtime with template:repeat over a layout table." }) + "at runtime with template:repeat over a layout table. The button changes that table and " + + "re-renders only the nested view." }) .a({ n: "type", v: "Information" }) .a({ n: "showIcon", b: true }) .a({ n: "class", v: "sapUiSmallMargin" }); + page.tag("Button") + .a({ n: "text", v: "Show / Hide Age Column" }) + .a({ n: "icon", v: "sap-icon://refresh" }) + .a({ n: "press", v: this.client._event("TOGGLE_AGE") }) + .a({ n: "class", v: "sapUiSmallMarginBegin" }); + + // the nested view goes into this box - a container of its own, so a + // re-render replaces the nested view and nothing else on the page + page.tag("VBox") + .a({ n: "id", v: "box_nest" }); + this.client.view_display(lo_view.stringify()); } @@ -85,38 +106,40 @@ defineApp("Z2UI5_CL_SMP_APP_176", class { const lo_view_nested = 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:template", v: "http://schemas.sap.com/sapui5/extension/sap.ui.core.template/1" }); - lo_view_nested.ele("Shell") - .ele("Page") - .a({ n: "title", v: "Nested View" }) - .ele("Table") - .a({ n: "items", v: this.client._bind("mt_data") }) - .ele("columns") - .ele({ n: "repeat", ns: "template" }) - .a({ n: "list", v: layout_path }) - .a({ n: "var", v: "LO" }) - .ele("Column") - .a({ n: "mergeDuplicates", v: "{LO>MERGE}" }) - .a({ n: "visible", v: "{LO>VISIBLE}" }) - .tag("Text") - .a({ n: "text", v: "{LO>TITLE}" }) - .end() + lo_view_nested.ele("Panel") + .a({ n: "headerText", v: "Nested View" }) + .a({ n: "class", v: "sapUiSmallMarginTop" }) + .ele("Table") + .a({ n: "items", v: this.client._bind("mt_data") }) + .ele("columns") + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: layout_path }) + .a({ n: "var", v: "LO" }) + .ele("Column") + .a({ n: "mergeDuplicates", v: "{LO>MERGE}" }) + .a({ n: "visible", v: "{LO>VISIBLE}" }) + .tag("Text") + .a({ n: "text", v: "{LO>TITLE}" }) .end() .end() - .ele("items") - .ele("ColumnListItem") - .ele("cells") - .ele({ n: "repeat", ns: "template" }) - .a({ n: "list", v: layout_path }) - .a({ n: "var", v: "LO2" }) - .ele("ObjectIdentifier") - .a({ n: "text", v: "{= '{' + ${LO2>FNAME} + '}' }" }); - - this.client.nest_view_display({ val: lo_view_nested.stringify(), id: "test", method_insert: "addContent", method_destroy: "removeAllContent" }); + .end() + .ele("items") + .ele("ColumnListItem") + .ele("cells") + .ele({ n: "repeat", ns: "template" }) + .a({ n: "list", v: layout_path }) + .a({ n: "var", v: "LO2" }) + .ele("ObjectIdentifier") + .a({ n: "text", v: "{= '{' + ${LO2>FNAME} + '}' }" }); + + this.client.nest_view_display({ val: lo_view_nested.stringify(), + id: "box_nest", + method_insert: "addItem", + method_destroy: "removeAllItems" }); } }); From 1f842fb0e7749c868e0664a071bb177bdcce2fa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:21:46 +0000 Subject: [PATCH 3/4] Ignore package-lock.json, which the repository does not keep The repository ships no lockfile on purpose - test.yml says so: the run tests what an install gets today, as cap2UI5 and cap2UI5/samples-stack do. But every `npm install` writes one, and it then sits in `git status` as an untracked file waiting to be committed by mistake. samples-stack ignores it already; this does the same. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NVQ3JnU7R1QFQVgSoMK9RE --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 744d4ac..09f3769 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ +# no lockfile, as in cap2UI5: CI tests what an install gets today (.github/workflows/test.yml) +package-lock.json *.sqlite *.sqlite-* *.log From 781cb21cb8d33a43e8e69f66bb37ab22bf4dbd85 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:27:49 +0000 Subject: [PATCH 4/4] Add the five new samples abap2js translates: 533 to 537 abap2UI5/samples added nine samples between the old pin and 2e998ef. abap2js translates five of them as they are, so they come in the generated way - listed in samples.json, written by `npm run generate`, held to their originals by the differential test (76 samples, 260 roundtrips, no difference): 535 colours controls by their data with CustomData writeToDom, 536 attaches custom data to controls and reads it back on an event, 537 shows what an event argument can compute in the browser, and two are helper apps of samples abap2js refuses - 533, the wizard with page transitions that 531 calls, and 534, the popup-as-app that 532 calls. Like 469 and 489, the helpers are listed with the app that calls them. 534 shows a dialog and no view of its own, so the start test lists it with 186 as a sample that displays no MAIN view - in ABAP as here. The other four wait for abap2js: 531 reads a constant of another class, 532 uses NEW outside the navigation, 538 VALUE #( OPTIONAL ), 539 SPLIT. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NVQ3JnU7R1QFQVgSoMK9RE --- CHANGELOG.md | 12 +- README.md | 11 +- scripts/samples.json | 7 +- srv/apps/z2ui5_cl_smp_app_533.js | 100 ++++++++++++++++ srv/apps/z2ui5_cl_smp_app_534.js | 41 +++++++ srv/apps/z2ui5_cl_smp_app_535.js | 198 +++++++++++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_536.js | 153 ++++++++++++++++++++++++ srv/apps/z2ui5_cl_smp_app_537.js | 141 ++++++++++++++++++++++ test/samples.test.mjs | 1 + 9 files changed, 658 insertions(+), 6 deletions(-) create mode 100644 srv/apps/z2ui5_cl_smp_app_533.js create mode 100644 srv/apps/z2ui5_cl_smp_app_534.js create mode 100644 srv/apps/z2ui5_cl_smp_app_535.js create mode 100644 srv/apps/z2ui5_cl_smp_app_536.js create mode 100644 srv/apps/z2ui5_cl_smp_app_537.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 49eca3f..cf96b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,16 @@ versions [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `template:if/elseif/else`. `Z2UI5_CL_SMP_APP_176` gained a button that re-renders only the nested view, through an `ASSIGN mt_layout[ … ] TO FIELD-SYMBOL( )` abap2js refuses, so it is ported by hand from here on and - listed under `handwritten`. abap2UI5/samples has 138 samples at that - commit; the nine new ones (531 to 539) are not translated yet. + listed under `handwritten`. +- Five of the nine samples abap2UI5/samples added since, translated by + abap2js: `Z2UI5_CL_SMP_APP_535` (CustomData writeToDom), + `Z2UI5_CL_SMP_APP_536` (custom data on controls), `Z2UI5_CL_SMP_APP_537` + (expressions, formatters and literals in `t_arg`), and two helper apps - + `Z2UI5_CL_SMP_APP_533`, the wizard with page transitions + `Z2UI5_CL_SMP_APP_531` calls, and `Z2UI5_CL_SMP_APP_534`, the popup-as-app + `Z2UI5_CL_SMP_APP_532` calls. 76 of abap2UI5's 138 samples. The other four + wait for abap2js: 531 reads a constant of another class, 532 uses `NEW` + outside the navigation, 538 `VALUE #( OPTIONAL … )`, 539 `SPLIT`. - `npm run generate` (and its `--check`, the CI gate) refuses a module in `srv/apps/` that `scripts/samples.json` does not list - such a module is packed, served and started by the tests, but never held to its original by diff --git a/README.md b/README.md index 23a6ab9..ea24079 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ original's comments and texts, all of them, as they are. `client->check_app_prev_stack( )` is `client.check_app_prev_stack()`. > [!NOTE] -> **Status: 71 of the 138 samples.** 68 are generated by abap2js from +> **Status: 76 of the 138 samples.** 73 are generated by abap2js from > abap2UI5/samples at the commit in [`ABAP2UI5_SAMPLES_PIN`](ABAP2UI5_SAMPLES_PIN), > three are ported by hand; the [differential test](#the-differential-test) holds > every one of them to its ABAP original. @@ -76,6 +76,7 @@ open e.g. . | CSS - Color Table Cells from the Backend | [`Z2UI5_CL_SMP_APP_305`](srv/apps/z2ui5_cl_smp_app_305.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_305.clas.abap) | | CSS - FlexBox Layouts with Custom Classes | [`Z2UI5_CL_SMP_APP_255`](srv/apps/z2ui5_cl_smp_app_255.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_255.clas.abap) | | CSS - Ship Your Own CSS with the View | [`Z2UI5_CL_SMP_APP_050`](srv/apps/z2ui5_cl_smp_app_050.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_050.clas.abap) | +| CSS - Style by Data with CustomData writeToDom | [`Z2UI5_CL_SMP_APP_535`](srv/apps/z2ui5_cl_smp_app_535.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_535.clas.abap) | | Control Behaviour - Aggregation Item by Index | [`Z2UI5_CL_SMP_APP_514`](srv/apps/z2ui5_cl_smp_app_514.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_514.clas.abap) | | Control Behaviour - Expand a Panel by ID (setExpanded) | [`Z2UI5_CL_SMP_APP_448`](srv/apps/z2ui5_cl_smp_app_448.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_448.clas.abap) | | Control Behaviour - Inline CSS on a Control (css) | [`Z2UI5_CL_SMP_APP_513`](srv/apps/z2ui5_cl_smp_app_513.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_513.clas.abap) | @@ -87,6 +88,8 @@ open e.g. . | Device - Device Model: Phone, Tablet, Desktop | [`Z2UI5_CL_SMP_APP_445`](srv/apps/z2ui5_cl_smp_app_445.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_445.clas.abap) | | Device - Frontend Info: UI5 Version, Theme, OS, Browser | [`Z2UI5_CL_SMP_APP_122`](srv/apps/z2ui5_cl_smp_app_122.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_122.clas.abap) | | Device - Geolocation from the Browser | [`Z2UI5_CL_SMP_APP_120`](srv/apps/z2ui5_cl_smp_app_120.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_120.clas.abap) | +| Event - Custom Data Attached to Controls | [`Z2UI5_CL_SMP_APP_536`](srv/apps/z2ui5_cl_smp_app_536.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_536.clas.abap) | +| Event - Expressions, Formatters and Literals in t_arg | [`Z2UI5_CL_SMP_APP_537`](srv/apps/z2ui5_cl_smp_app_537.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_537.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) | | Event - Keep the Last Keystroke with check_queue_last | [`Z2UI5_CL_SMP_APP_511`](srv/apps/z2ui5_cl_smp_app_511.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_511.clas.abap) | | Event - Link with preventDefault | [`Z2UI5_CL_SMP_APP_472`](srv/apps/z2ui5_cl_smp_app_472.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_472.clas.abap) | @@ -110,6 +113,7 @@ open e.g. . | Navigation - Data Loss Protection on Leaving | [`Z2UI5_CL_SMP_APP_279`](srv/apps/z2ui5_cl_smp_app_279.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_279.clas.abap) | | Navigation - Detail Page | [`Z2UI5_CL_SMP_APP_469`](srv/apps/z2ui5_cl_smp_app_469.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_469.clas.abap) | | Navigation - Return Data and Events to the Caller | [`Z2UI5_CL_SMP_APP_488`](srv/apps/z2ui5_cl_smp_app_488.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_488.clas.abap) | +| Navigation - Wizard Steps with Page Transitions (the app Z2UI5_CL_SMP_APP_531 calls) | [`Z2UI5_CL_SMP_APP_533`](srv/apps/z2ui5_cl_smp_app_533.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_533.clas.abap) | | Nested View - Basic Example (nest_view_display) | [`Z2UI5_CL_SMP_APP_065`](srv/apps/z2ui5_cl_smp_app_065.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_065.clas.abap) | | Nested View - Destroy and Target a Slot | [`Z2UI5_CL_SMP_APP_510`](srv/apps/z2ui5_cl_smp_app_510.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_510.clas.abap) | | Popover - Basic Example with Placement | [`Z2UI5_CL_SMP_APP_026`](srv/apps/z2ui5_cl_smp_app_026.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_026.clas.abap) | @@ -119,6 +123,7 @@ open e.g. . | 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) | | Popup - Element Binding to the Selected Row | [`Z2UI5_CL_SMP_APP_470`](srv/apps/z2ui5_cl_smp_app_470.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_470.clas.abap) | | Popup - Navigate between Dialogs (NavContainer) | [`Z2UI5_CL_SMP_APP_170`](srv/apps/z2ui5_cl_smp_app_170.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_170.clas.abap) | +| Popup - Popup-as-App: a Dialog and no View (the app Z2UI5_CL_SMP_APP_532 calls) | [`Z2UI5_CL_SMP_APP_534`](srv/apps/z2ui5_cl_smp_app_534.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_534.clas.abap) | | Table - Editable Cells, Add and Delete Rows | [`Z2UI5_CL_SMP_APP_011`](srv/apps/z2ui5_cl_smp_app_011.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_011.clas.abap) | | Templating - Metadata-Driven Table and Form | [`Z2UI5_CL_SMP_APP_173`](srv/apps/z2ui5_cl_smp_app_173.js) | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_173.clas.abap) | | Templating - Dynamic Content in a Nested View | [`Z2UI5_CL_SMP_APP_176`](srv/apps/z2ui5_cl_smp_app_176.js) - by hand | [abap](https://github.com/abap2UI5/samples/blob/main/src/z2ui5_cl_smp_app_176.clas.abap) | @@ -172,8 +177,8 @@ against the ABAP the runtime ships - renames it `ZABAP_SMP_APP_nnn` and serves it beside the translation in one cap2UI5 server. Both are started, every event the original's first view wires is fired on a fresh start of each, and what comes back is compared: the XML of every view slot, the model the original -sends, every other action and which app has the screen - today 71 samples in -240 roundtrips, without a difference. One is known and left on purpose: +sends, every other action and which app has the screen - today 76 samples in +260 roundtrips, without a difference. One is known and left on purpose: `Z2UI5_CL_SMP_APP_122` prints a number with `CONV string( )`, which keeps ABAP's trailing sign position (`"0 "`) and JavaScript's `String( )` does not. diff --git a/scripts/samples.json b/scripts/samples.json index 2f8f332..8c1e72a 100644 --- a/scripts/samples.json +++ b/scripts/samples.json @@ -68,7 +68,12 @@ "z2ui5_cl_smp_app_515", "z2ui5_cl_smp_app_516", "z2ui5_cl_smp_app_518", - "z2ui5_cl_smp_app_519" + "z2ui5_cl_smp_app_519", + "z2ui5_cl_smp_app_533", + "z2ui5_cl_smp_app_534", + "z2ui5_cl_smp_app_535", + "z2ui5_cl_smp_app_536", + "z2ui5_cl_smp_app_537" ], "handwritten": { "z2ui5_cl_smp_app_011": "LOOP AT t_tab REFERENCE INTO writes through the row, and DELETE t_tab WHERE (abap2js: neither supported yet)", diff --git a/srv/apps/z2ui5_cl_smp_app_533.js b/srv/apps/z2ui5_cl_smp_app_533.js new file mode 100644 index 0000000..bdeb4c7 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_533.js @@ -0,0 +1,100 @@ +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_533.clas.abap +// An app with screens of its own, called by z2ui5_cl_smp_app_531: Next and +// Previous rebuild ITS view, no other app is involved - so nothing tells the +// framework which way the screen moves. Next is forward by default; Previous +// says it with view_display( transition_back = abap_true ) and plays the +// step being left out in reverse. Done leaves the app with nav_app_leave( ), +// reversed too. This app is a hidden helper (never listed on its own in the +// overview). +import { defineApp, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; + +const steps = 3; + +defineApp("Z2UI5_CL_SMP_APP_533", class { + + step = 1; + + main(client) { + + this.client = client; + + if (client.check_on_navigated()) { + this.view_display(); + + } else if (client.check_on_event("NEXT")) { + + this.step = this.step + 1; + this.view_display(); + + } else if (client.check_on_event("PREVIOUS")) { + + this.step = this.step - 1; + this.view_display(true); + + } else if (client.check_on_event("DONE")) { + client.nav_app_leave(); + } + + } + + view_display(back) { + + const color = (this.step === 1 ? "#0a6ed1" + : this.step === 2 ? "#e9730c" + : "#107e3e"); + + 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:core", v: "sap.ui.core" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", t: `abap2UI5 - Wizard - Step ${this.step} of ${steps}` }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "id", v: "step" }) + .a({ n: "text", t: `Step ${this.step} of ${steps} - the same app, a new view each time. Next moves forward, ` + + `Previous says it goes back: view_display( transition_back = abap_true ).` }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + const content = page.ele("VBox") + .a({ n: "class", v: "sapUiSmallMargin" }); + + content.tag({ n: "Icon", ns: "core" }) + .a({ n: "src", v: "sap-icon://step" }) + .a({ n: "size", v: "6rem" }) + .a({ n: "color", t: color }) + .a({ n: "class", v: "sapUiMediumMarginTopBottom" }); + + content.tag("Button") + .a({ n: "id", v: "previous" }) + .a({ n: "text", v: "Previous - view_display( transition_back = abap_true )" }) + .a({ n: "icon", v: "sap-icon://navigation-left-arrow" }) + .a({ n: "enabled", b: (this.step > 1) }) + .a({ n: "class", v: "sapUiTinyMarginBottom" }) + .a({ n: "press", v: this.client._event("PREVIOUS") }); + content.tag("Button") + .a({ n: "id", v: "next" }) + .a({ n: "text", v: "Next - view_display( transition = slide )" }) + .a({ n: "icon", v: "sap-icon://navigation-right-arrow" }) + .a({ n: "enabled", b: (this.step < steps) }) + .a({ n: "type", v: "Emphasized" }) + .a({ n: "class", v: "sapUiTinyMarginBottom" }) + .a({ n: "press", v: this.client._event("NEXT") }); + content.tag("Button") + .a({ n: "id", v: "done" }) + .a({ n: "text", v: "Done - nav_app_leave( ): back to the caller" }) + .a({ n: "icon", v: "sap-icon://accept" }) + .a({ n: "press", v: this.client._event("DONE") }); + + this.client.view_display({ val: view.stringify(), transition: this.client.cs_transition.slide, transition_back: back }); + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_534.js b/srv/apps/z2ui5_cl_smp_app_534.js new file mode 100644 index 0000000..e95e8b4 --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_534.js @@ -0,0 +1,41 @@ +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_534.clas.abap +// A popup-as-app, called by the page z2ui5_cl_smp_app_532: nav_app_call( ) +// opens an app that shows a dialog and no view of its own, so its caller's +// page stays on screen. Closing hands back with nav_app_leave( ), the caller +// re-displays - and the page does NOT move: the display carries the app +// instance, and the frontend sees that the page on screen belongs to it. +// This app is a hidden helper (never listed on its own in the overview). +import { defineApp, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; + +defineApp("Z2UI5_CL_SMP_APP_534", class { + + main(client) { + + if (client.check_on_navigated()) { + + const popup = z2ui5_cl_ui5_view_builder.factory(); + const dialog = popup.ele({ n: "FragmentDefinition", ns: "core" }) + .a({ n: "xmlns", v: "sap.m" }) + .a({ n: "xmlns:core", v: "sap.ui.core" }) + .ele("Dialog") + .a({ n: "title", v: "A popup-as-app" }); + dialog.tag("Text") + .a({ n: "text", v: "This app shows a dialog and nothing else. Close it: the page behind it comes back " + + "without a transition - it never left." }) + .a({ n: "class", v: "sapUiSmallMargin" }); + dialog.ele("buttons") + .tag("Button") + .a({ n: "id", v: "close" }) + .a({ n: "text", v: "Close" }) + .a({ n: "press", v: client._event("CLOSE") }); + client.popup_display(popup.stringify()); + + } else if (client.check_on_event("CLOSE")) { + + client.popup_destroy(); + client.nav_app_leave(); + + } + + } +}); diff --git a/srv/apps/z2ui5_cl_smp_app_535.js b/srv/apps/z2ui5_cl_smp_app_535.js new file mode 100644 index 0000000..ee0733f --- /dev/null +++ b/srv/apps/z2ui5_cl_smp_app_535.js @@ -0,0 +1,198 @@ +// @keywords customdata writetodom data attribute selector marker test anchor +// @summary Writes bound values into the HTML DOM as data-* attributes with CustomData writeToDom, so a stylesheet colours controls by their data and tests find stable anchors. +// @origin abap2UI5/samples src/z2ui5_cl_smp_app_535.clas.abap +import { defineApp, t, z2ui5_cl_ui5_view_builder } from "@cap2ui5/cds-plugin"; + +const ty_s_order = { + id: "", + product: "", + status: "", +}; + +defineApp("Z2UI5_CL_SMP_APP_535", class { + + status = ""; + t_orders = t.table(ty_s_order); + + main(client) { + + this.client = client; + + if (client.check_on_init()) { + + this.status = "new"; + this.t_orders = [ + { id: "4711", product: "Notebook", status: "new" }, + { id: "4712", product: "Monitor", status: "shipped" }, + { id: "4713", product: "Keyboard", status: "delayed" }, + { id: "4714", product: "Mouse", status: "shipped" }, + { id: "4715", product: "Headset", status: "delayed" } ]; + this.view_display(); + + } else if (client.check_on_navigated()) { + this.view_display(); + } else if (client.check_on_event("NEXT")) { + this.status_next(); + } + + } + + status_next() { + + this.status = (this.status === "new" ? "shipped" + : this.status === "shipped" ? "delayed" + : "new"); + + } + + 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" }) + .a({ n: "xmlns:core", v: "sap.ui.core" }); + const page = view.ele("Shell") + .ele("Page") + .a({ n: "title", v: "abap2UI5 - CSS - Style by Data with CustomData writeToDom" }) + .a({ n: "showNavButton", b: this.client.check_app_prev_stack() }) + .a({ n: "navButtonPress", v: this.client._event_nav_app_leave() }); + + page.tag("MessageStrip") + .a({ n: "text", v: "A core:CustomData with writeToDom=\"true\" writes its key and value as a data-* attribute on the " + + "root element of its control. A stylesheet selects on that attribute to style controls by their data, " + + "and automated tests use it as a stable anchor that does not depend on generated IDs." }) + .a({ n: "type", v: "Information" }) + .a({ n: "showIcon", b: true }) + .a({ n: "class", v: "sapUiSmallMargin" }); + + // raw markup travels in the content attribute of a core:HTML leaf - the + // builder re-escapes it on stringify, so the literal markup is written here. + // Every rule selects on a data-* attribute, which exists only because the + // CustomData elements below carry writeToDom="true". The first rule is the + // one of the UI5 documentation, word for word + page.tag({ n: "HTML", ns: "core" }) + .a({ n: "content", v: "" }); + + // writeToDom needs the expanded notation: the app:key="value" shortcut + // creates a CustomData without the flag, so nothing reaches the DOM. The + // key must be a valid HTML ID - keep it lower case, browsers may lower it + page.ele("Panel") + .a({ n: "headerText", v: "A static value - the example of the documentation" }) + .a({ n: "class", v: "sapUiResponsiveMargin" }) + .a({ n: "width", v: "auto" }) + .ele("Button") + .a({ n: "text", v: "Renders as